Building My First Application with Zend, Part 1


The Zend tutorial is certainly useful for getting your toes wet and figuring out the basics.  Alas, like most such tutorials, though, it deals with a very basic application – single table, only a few fields, with just a few crud functions. While my partner figures out some other stuff with the application we're building, I've been charged with continuing to learn Zend, so I'm doing what I started to do that made me hate CakePHP – try converting something I already have in Mach-II to PHP with Zend.  So I'm going to try making a version of the Movie Shelves area of my personal site.

Since I never finished the Cake example, I didn't post about it here.  We'll see how well I do with this one 🙂  To start with, I popped the skeleton application into my site root, per the manual. I did it via downloading the ZIP, unpacking, uploading then running the composer commands. For some reason, the composer commands failed today when they worked fine yesterday.  The skeleton is still working, though, so I for now I'm just continuing on. 

For this area of the site, I have five tables.

Since everything really revolves around the movies table, and none of the individual elements are "independent", from my understanding this means I have one module, which I will call "MovieShelves".  Under that, within the src, I'll have five components (or whatever they are called), one for each table.  Hopefully, I understood the tutorial stuff correctly.  For now, my initial directory structure and empty files look something like this.

After doing the really basic stuff (i.e. setting up the DB), it was time to start with the formats section. So first I made the Module.php under MovieShelves (since I knew I would be editing it anyway, I did the parts from both the Modules and the DB section of the tutorial at once).

<?php
	namespace MovieShelves;
	
	use Zend\ModuleManager\Feature\AutoloaderProviderInterface;
 	use Zend\ModuleManager\Feature\ConfigProviderInterface;
	use Zend\Db\ResultSet\ResultSet;
	use Zend\Db\TableGateway\TableGateway;
	use MovieShelves\Formats\Model\Formats;
	use MovieShelves\Formats\Model\FormatsTable;
	
	class Module implements AutoloaderProviderInterface, ConfigProviderInterface {
		public function getAutoloaderConfig() {
			return array(
				'Zend\Loader\ClassMapAutoloader' => array(
						__DIR__ . '/autoload_classmap.php',
					),
				'Zend\Loader\StandardAutoloader' => array(
					'namespaces' => array(
						__NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
					),
				),
			);
		}
		
		public function getConfig() {
			return include __DIR__ . '/config/module.config.php';
		}
		
		public function getServiceConfig() {
			return array(
				'factories' => array(
					'MovieShelves\Formats\Model\FormatsTable' =>  function($sm) {
						$tableGateway = $sm->get('FormatsTableGateway');
						$table = new FormatsTable($tableGateway);
						return $table;
					},
					'FormatsTableGateway' => function ($sm) {
						$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
						$resultSetPrototype = new ResultSet();
						$resultSetPrototype->setArrayObjectPrototype(new Formats());
						return new TableGateway('formats', $dbAdapter, null, $resultSetPrototype);
					},
				),
			);
		}
	}
?>

Then I created my module.config.php under MovieShelves/config with my basic configuration as well as the router

<?php
	return array(
		'controllers' => array(
			'invokables' => array(
				'Formats\Controller\Formats' => 'Formats\Controller\FormatsController',
			),
		),
		'router' => array(
			'routes' => array(
				'formats' => array(
					'type' => 'segment',
					'options' =>  array(
						'route'		=> '/formats[/][:action][/:id]',
						'constraints'	=> array(
							'action'	=> '[a-zA-Z][a-zA-Z0-9_-]*',
                         		'id' 	=> '[0-9]+',
						),
						'defaults' =>  array(
							'controller' 	=>  'Formats\Controller\Formats',
							'action' 		=>  'index',
						),
					),
				),
			),
		),
		'view_manager' => array(
			'template_path_stack' => array(
				'formats' => __DIR__ . '/../view',
			),
		),
	);
?>

I added my new module, MovieShelves, to the application.config.php under the main config folder, then made the very basic "blah" controller.

<?php
	namespace Formats\Controller;
	
	use Zend\Mvc\Controller\AbstractActionController;
 	use Zend\View\Model\ViewModel;
	
	class FormatsController extends AbstractActionController {
		public function indexAction() {
		}
		
		public function addAction() {
		}
		
		public function editAction() {
		}
		
		public function deleteAction() {
		}
	}
?>

With that done, I threw in a simple line of "If I had content, I would show you the available formats!" in the index.phtml file then tried testing my new routes, specifically the formats/index route where I had my little message.  But I got a fatal error instead:

Fatal error: Class 'Formats\Controller\FormatsController' not found in /usr/local/zend/var/libraries/Zend_Framework_2/2.3.1/library/Zend/ServiceManager/AbstractPluginManager.php on line 170

Hmmm…okay.  After reading the docs some more and getting a little aggravated by the lack of better explanation on what some of the stuff it has you do is doing, I found this helpful post from Rob Allen on how to change the directory structure around from the "traditional" one (which is what I'm doing here). That helped me figure out the changes I needed to make to get things to work.  So in my FormatsController, I had to change the namespace to put it under it's parent properly:

namespace Formats\Controller;  →  namespace MovieShelves\Formats\Controller;

In Module.php, I had to change one line to correct my namespace/path pair format:

__NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,   →  __NAMESPACE__ => __DIR__ . '/src/,

And finally in module.config.php, I had to change the invoking aliasing to reflect these changes,

'Formats\Controller\Formats' => 'Formats\Controller\FormatsController',  →  'MovieShelves\Controller\Formats' => 'MovieShelves\Formats\Controller\FormatsController',

And then further below, the use of the reference has to be switched as well to match what we have in the invokes.

'controller' => 'Formats\Controller\Formats',  →  'controller' => 'MovieShelves\Controller\Formats',

With those changes, Zend was able to find my controller in my modified controller.  I honestly don't entirely understand the what/whys behind those changes, but they worked.  No more fatal error.  I get a proper page, still using the Zend skeleton template of course, displaying a Zend error message.

Okay. So it can't find my views.  For some reason, it's doing movie-shelves/formats, instead of what I expected which would have been formats/formats.  This is where the tutorial example on the Zend site is really aggravating.  They made their module name Album, but they also made a folder called Album containing the controller, forms, etc for an Album.  To me this makes no sense at all and it makes it much harder to figure out when Album is referring to the module/namespace and when it's referring to something else.  And if it is also supposed to be something like Album/src/Album, or A/src/A, then what's the point of the second A folder?

Anyway, this one was harder to figure out. After a lot of Googling, skimming various sections of the Zend docs, etc, I could not figure out how Zend was deciding where to look for the views or how to tell it "no no, go here".  I presume it's the ViewManager or ViewResolver, but I couldn't figure out how to reconfigure it. I did find that it basically looks for the template name as "module/controller/action" with the names made all lower case and words separated by dashes. So I was able to fix it by changing my view folder structure to add a movie-shelves folder, then moving the individual folders, i.e. formats, genres, etc under it. It did let me eliminate the redundant double folders at least.

I could also have changed the config to manually list every view, but that would add excessive overhead and greatly increase the likelihood of human error coming into play.  With that change, we have success!

Yay!!!   Now to make our model files. Again, following the Zend tutorial (the Album one), I first made the Formats.php

<?php
	namespace MovieShelves\Formats\Model;
	
	class Formats {
		public $formatid;
		public $formatlabel;
		
		public function exchangeArray($data) {
			$this->formatid = (!empty($data['formatid'])) ? $data['formatid'] : null;
			$this->formatlabel = (!empty($data['formatlabel'])) ? $data['formatlabel'] : null;
		}
	}
?>

And then FormatsTables.php

<?php
	namespace MovieShelves\Formats\Model;
	
	use Zend\Db\TableGateway\TableGateway;

	class FormatsTable {
		protected $tableGateway;
		
		public function __construct(TableGateway $tableGateway) {
			$this->tableGateway = $tableGateway;
		}
		
		public function fetchAll() {
			$resultSet = $this->tableGateway->select();
			return $resultSet;
		}	
		public function getFormats($id) {
			$id  = (int) $id;
			$rowset = $this->tableGateway->select(array('formatid' => $id));
			$row = $rowset->current();
			if (!$row) {
				throw new \Exception("Could not find row $id");
			}
			return $row;
		}
		
		public function saveFormats(Formats $formats) {
			$data = array(
				'formatlabel' => $formats->formatlabel,
				);
			
			$id = (int) $formats->id;
			if ($id == 0) {
				$this->tableGateway->insert($data);
			} else {
				if ($this->getFormats($id)) {
					$this->tableGateway->update($data, array('formatid' => $id));
				} else {
					throw new \Exception('Format ID does not exist');
				}
			}
		}
		
		public function deleteFormats($id) {
			$this->tableGateway->delete(array('formatid' => (int) $id));
		}
	}
?>

Again, both are really basic for now, just to get them going. The one difference from the tutorial way versus mine is that I had to change the namespaces to add the MovieShelves so it could find everything.  Until I did that, it was unable to instantiate the FormatsTable. So I ran that and got a new error.

This one at least I instantly knew the problem – Zend was presuming the table's name was "formats", so I had to figure out how to tell it to use the table name movieshelves_formats.  That was also easy to figure out after looking at the TableGateway docs.  I just had to change one line in Module.php

return new TableGateway(formats', $dbAdapter, null, $resultSetPrototype);  →  return new TableGateway('movieshelves_formats', $dbAdapter, null, $resultSetPrototype);

And finally, I threw the basic tutorial style format for showing the formats on the page in movie-shelves/formats/index.phtml.

<?php
	$title = 'Formats';
	$this->headTitle($title);
?>

<h1><?php echo $this->escapeHtml($title); ?></h1>
<p><a href="<?php echo $this->url('formats', array('action'=>'add'));?>">Add new format</a></p>

<table class="table">
<thead>
	<tr>
		<th>ID</th>
		<th>Label</th>
		<th> </th>
	</tr>
</thead>
<tbody>
	<?php foreach ($formats as $format) : ?>
	<tr>
		<td><?php echo $this->escapeHtml($format->formatid);?></td>
		<td><?php echo $this->escapeHtml($format->formatlabel);?></td>
		<td>
			<a href="<?php echo $this->url('formats', array('action'=>'edit', 'formatid' => $format->formatid));?>">Edit</a> ~ 
			<a href="<?php echo $this->url('formats', array('action'=>'delete', 'formatid' => $format->formatid));?>">Delete</a>
		</td>
	</tr>
	<?php endforeach; ?>
</tbody>
</table>

And when I run it…

Yippee!!  A successful page load while using the organization structure we are leaning towards and without having to follow some crazy rigid DB schema – instead I was able to just use the schema done to our conventions. 

Since this is a fairly long post, I'll cut here.  In part 2, I'll be working on redoing the layout template to my own design, then finish up the formats views.