Advancing My First Zend App, Part 1


Before I started with the expansion of the Zend app I did in the last series, I went through all the models and redid them in the manner I did the Movies one. I decided I liked it better for a few reasons:

  • Cleaner file structure – having a bunch of folders with just one file in them annoys me greatly
  • Faster, easier to read code with the more specific form definitions and the SQL query building and more flexible
  • Getter/Setter in entities is more familiar and, again, more specific and allows for greater flexibility and control
  • Cleaner separation of code chunks

One thing I didn't do with the movies (and thus subsequent models) was use more explicit declarations in the input filter versus the factory-backed one as I switched to in the forms.  I looked at doing it, but in that case the code became much more convoluted and confusing, so I'll stick with the factory-backed mechanism instead for filters. 🙂

I also went through and moved all "config" stuff from Module.php to the appropriate module.config.php which seemed to be more consistently viewed as the "best practice" so that Module itself is relatively dumb while the module.config has actual configuration stuff.  As a result, my Module.php now just has the getAutoloaderConfig() and getConfig() functions and everything else was converted to the appropriate options in the module config, like the view helpers and service manager.

<?php
	namespace MovieShelves;
	
	use Zend\ModuleManager\Feature\AutoloaderProviderInterface;
 	use Zend\ModuleManager\Feature\ConfigProviderInterface;
	
	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/',
					),
				),
			);
		}
		
		public function getConfig() {
			return include __DIR__ . '/config/module.config.php';
		}
	}
?>
<?php
	return array(
		'controllers' => array(
			'invokables' => array(
				'MovieShelves\Controller\Formats' => 'MovieShelves\Formats\FormatsController',
				'MovieShelves\Controller\Genres' => 'MovieShelves\Genres\GenresController',
				'MovieShelves\Controller\Studios' => 'MovieShelves\Studios\StudiosController',
				'MovieShelves\Controller\Series' => 'MovieShelves\Series\SeriesController',
				'MovieShelves\Controller\Movies' => 'MovieShelves\Movies\MoviesController',
			),
		),
		'router' => array(
			'routes' => array(
				'formats' => array(
					'type' => 'segment',
					'options' =>  array(
						'route'		=> '/formats[/][:action][/:formatid]',
						'constraints'	=> array(
							'action'	=> '[a-zA-Z][a-zA-Z0-9_-]*',
						 	'formatid' 	=> '[0-9]+',
						),
						'defaults' =>  array(
							'controller' 	=>  'MovieShelves\Controller\Formats',
							'action' 		=>  'index',
						),
					),
				),
				'genres' => array(
					'type' => 'segment',
					'options' =>  array(
						'route'		=> '/genres[/][:action][/:genreid]',
						'constraints'	=> array(
							'action'	=> '[a-zA-Z][a-zA-Z0-9_-]*',
						 	'genreid' 	=> '[0-9]+',
						),
						'defaults' =>  array(
							'controller' 	=>  'MovieShelves\Controller\Genres',
							'action' 		=>  'index',
						),
					),
				),
				'studios' => array(
					'type' => 'segment',
					'options' =>  array(
						'route'		=> '/studios[/][:action][/:studioid]',
						'constraints'	=> array(
							'action'	=> '[a-zA-Z][a-zA-Z0-9_-]*',
						 	'studioid' 	=> '[0-9]+',
						),
						'defaults' =>  array(
							'controller' 	=>  'MovieShelves\Controller\Studios',
							'action' 		=>  'index',
						),
					),
				),
				'series' => array(
					'type' => 'segment',
					'options' =>  array(
						'route'		=> '/series[/][:action][/:seriesid]',
						'constraints'	=> array(
							'action'	=> '[a-zA-Z][a-zA-Z0-9_-]*',
						 	'seriesid' 	=> '[0-9]+',
						),
						'defaults' =>  array(
							'controller' 	=>  'MovieShelves\Controller\Series',
							'action' 		=>  'index',
						),
					),
				),
				'movies' => array(
					'type' => 'segment',
					'options' =>  array(
						'route'		=> '/movies[/][:action][/:movieid]',
						'constraints'	=> array(
							'action'	=> '[a-zA-Z][a-zA-Z0-9_-]*',
						 	'movieid' 	=> '[0-9]+',
						),
						'defaults' =>  array(
							'controller' 	=>  'MovieShelves\Controller\Movies',
							'action' 		=>  'index',
						),
					),
				),
			),
		),
		'view_manager' => array(
			'template_map' => array(
				'layout/layout'	=> __DIR__ . '/../view/layout/layout.phtml',
			),
			'template_path_stack' => array(
				__DIR__ . '/../view',
			),
		),
		'view_helpers' => array(
			'invokables' => array(
				'formelementerrors' => 'Application\Helper\FormElementErrors',
				'formrow' => 'Application\Helper\FormRow',
				'form' => 'Application\Helper\Form',
			),
		),
		'service_manager' => array(  
			'factories' => array(
				// Formats
				'FormatsDAO' => function ($sm) {
					$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
					$dao = new MovieShelves\Formats\FormatsDAO($dbAdapter);
					
					return $dao;
				},
				'FormatsForm' => function ($sm) {
					$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
					$form = new MovieShelves\Formats\FormatsForm($dbAdapter);
					return $form;
				},
				// Genres
				'GenresDAO' => function ($sm) {
					$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
					$dao = new MovieShelves\Genres\GenresDAO($dbAdapter);
					
					return $dao;
				},
				'GenresForm' => function ($sm) {
					$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
					$form = new MovieShelves\Genres\GenresForm($dbAdapter);
					return $form;
				},
				// Studios
				'StudiosDAO' => function ($sm) {
					$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
					$dao = new MovieShelves\Studios\StudiosDAO($dbAdapter);
					
					return $dao;
				},
				'StudiosForm' => function ($sm) {
					$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
					$form = new MovieShelves\Studios\StudiosForm($dbAdapter);
					return $form;
				},
				// Series
				'SeriesDAO' => function ($sm) {
					$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
					$dao = new MovieShelves\Series\SeriesDAO($dbAdapter);
					
					return $dao;
				},
				'SeriesForm' => function ($sm) {
					$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
					$form = new MovieShelves\Series\SeriesForm($dbAdapter);
					return $form;
				},
				// Movies
				'MoviesDAO' => function ($sm) {
					$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
					$dao = new MovieShelves\Movies\MoviesDAO($dbAdapter);
					
					return $dao;
				},
				'MoviesForm' => function ($sm) {
					$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
					$form = new MovieShelves\Movies\MoviesForm($dbAdapter);
					return $form;
				}
			),
		),
	);
?>

Up to this point, I've been going to each section of my app by manually typing in the URL. That's kind of aggravating, ya know?  Guess I should add a menu.  I decided to use Zend's Navigation component to see how it works and test its future usability for us. But this site will eventually two menus, so it took awhile to figure out how I could have two different menus and call them as needed.  It took ridiculously long, actually, as the Zend docs didn't seem to cover it at all. Since I haven't done the authentication stuff yet, though, I'll just show both menus on the page.

First, we build out our menu array in the module.config.php.  You could probably have this in a separate file and pull it with an include, but for simplicity I went with this method.  I built out two menus, one called adminNavigation and one called publicNavigation.  Note, some of the links here are place holders because those pages haven't been built yet and Zend will not let you build links to non-existent routes!  This was added right below the end of the service_manager array set.

...
'navigation' => array(
	'adminNavigation' => array(
		array(
			'label' => 'Home',
			'route' => 'home',
		),
		array(
			'label' => 'Movies',
			'route' => 'movies',
			'pages' => array(
				array(
					'label' => 'Importer',
					'route' => 'formats',
				),
			),
		),
		array(
			'label' => 'Pick Lists',
			'uri' => '#',
			'pages' => array(
				array(
					'label' => 'Formats',
					'route' => 'formats',
					'pages' => array(
						array(
							'label' => 'Add',
							'route' => 'formats',
							'action' => 'add',
						),
						array(
							'label' => 'Edit',
							'route' => 'formats',
							'action' => 'edit',
						),
					),
				),
				array(
					'label' => 'Genres',
					'route' => 'genres',
					'pages' => array(
						array(
							'label' => 'Add',
							'route' => 'genres',
							'action' => 'add',
						),
						array(
							'label' => 'Edit',
							'route' => 'genres',
							'action' => 'edit',
						),
					),
				),
				array(
					'label' => 'Series',
					'route' => 'series',
					'pages' => array(
						array(
							'label' => 'Add',
							'route' => 'series',
							'action' => 'add',
						),
						array(
							'label' => 'Edit',
							'route' => 'series',
							'action' => 'edit',
						),
					),
				),
				array(
					'label' => 'Studios',
					'route' => 'studios',
					'pages' => array(
						array(
							'label' => 'Add',
							'route' => 'studios',
							'action' => 'add',
						),
						array(
							'label' => 'Edit',
							'route' => 'studios',
							'action' => 'edit',
						),
					),
				),
			),
		),
		array(
			'label' => 'Reports',
			'route' => 'movies',
			'pages' => array(
				array(
					'label' => 'File Checks',
					'route' => 'formats',
					
				),
				array(
					'label' => 'Missing Data',
					'route' => 'genres',
					
				),
			),
		),
	),
	'publicNavigation' => array(
		array(
			'label' => 'Home',
			'route' => 'home',
		),
		array(
			'label' => 'Browse',
			'uri' => '#',
			'pages' => array(
				array(
					'label' => 'By Series',
					'route' => 'formats',
				),
				array(
					'label' => 'By Genre',
					'route' => 'genres',
				),
				array(
					'label' => 'By Studio',
					'route' => 'series',
				),
				array(
					'label' => 'By Year',
					'route' => 'series',
				),
				array(
					'label' => 'View All',
					'route' => 'movies',
					
				),
			),
		),
		array(
			'label' => 'Search',
			'route' => 'movies',
		),
	),
),
...

Then I created two custom navigation functions that are so ridiculously simple, it is ridiculous.  Their only point is to override the default navigation setting.  I suspect there is another way to do this, but thus far I haven't found it and while this is silly, it works.  I put both of these in the Application/src/Application under a Navigation folder.

<?php
	namespace Application\Navigation;
 
	use Zend\Navigation\Service\DefaultNavigationFactory;
	
	class AdminNavigation extends DefaultNavigationFactory {
		protected function getName() {
			return 'adminNavigation';
		}
	}
?>
<?php
	namespace Application\Navigation;
 
	use Zend\Navigation\Service\DefaultNavigationFactory;
	
	class PublicNavigation extends DefaultNavigationFactory {
		protected function getName() {
			return 'publicNavigation';
		}
	}
?>

Then back in module.config.php, I added these new navigation elements to our service factory call.

...
'service_manager' => array(  
	'factories' => array(
		// Added Navigations
		'AdminNavigation' => 'Application\Navigation\AdminNavigation',
		'PublicNavigation' => 'Application\Navigation\PublicNavigation',
		// Formats
		'FormatsDAO' => function ($sm) {
			$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
			$dao = new MovieShelves\Formats\FormatsDAO($dbAdapter);
			
			return $dao;
		},
...

And finally, I call them in the layout (the HR is just for visual division in the screenie).

...
<div id="middleSection" class="clearfix">
	<div id="leftSideBar">
		<?php echo $this->navigation('PublicNavigation')->menu(); ?>
		<hr />
		<?php echo $this->navigation('AdminNavigation')->menu()->setMaxDepth(1); ?>
	</div>
...

Well, it works anyway.

It is a bit limiting, though.  I can't add a dynamic list of years to browse by through this mechanism, for example, though it could be done with using a fuller custom navigation component.  I'd also rather have a search box, but that's easy to change by simply not putting it in the menu and adding it to the layout (or an included submenu view) as desired.  I'm sure I'll do that when I tackle the public end. For now, though, the Zend navigation works well for quick, basic menu building without requiring you to do a ton of URL view helper calls as it sets it all up automatically. Like a lot of things with Zend, it's a use or don't sort of thing. 🙂

With that done, I quickly made a place holder view for to act as the public home page, then decided it was time to tackle adding an authentication system, which I'll cover in part 2.