Building My First Application with Zend, Part 2


So now that I have a view working, time to dump the default layout/design and try and get my own working. Yeah!  But first, taking out the translation functionality, as I don't need/want it.  That just requires removing the translator alias and configuration options from the module.config.php and deleting the language folder. Most likely once we are comfortable with our way of doing Zend frameworks we'll make our own skeleton that, among other things, doesn't include that 🙂

Next is to build the site template, i.e. layout.phtml.  Now for an app with a single module, it seems standard to just edit the one that is in the Application module.  However, for this I'm going to override that for Movie Shelves since, if I were going to build my whole site with Zend, I would need to be able to have separate layouts for each area of the site.  So I added a layout folder to my view and a new layout.phtml file there.  Then I modified my view_manager in MovieShelves/config/module.config.php to add the override.

'view_manager' => array(
	'template_map' => array(
		'layout/layout'	=> __DIR__ . '/../view/layout/layout.phtml',
	),
	'template_path_stack' => array(
		__DIR__ . '/../view',
	),
),

Then I modified my layout.phtml.  I didn't fully mimic the look of the current version as I wanted something a little simpler for this example (and I didn't want to kill an entire work day just redesigning the current version to use a purer CSS-based layout).  I put the css, js, and image files in the appropriate directories under the public folder.  For the layout.phtml itself, I ended up with this:

<?php $env = getenv('APP_ENV') ?: 'production'; ?>
<?php echo $this->doctype(); ?>

<html lang="en">
	<head>
	<?php echo $this->headTitle('An Eclectic World: Movie Shelves ')->setSeparator(': ')->setDefaultAttachOrder('APPEND'); ?>
	
	<?php echo $this->headMeta()
		->setCharset('UTF-8')
		->appendName('description', 'An online browsable directory of all of the movies in my collection')
		->appendName('author', 'Summer S. Wilson')
		->appendHttpEquiv('X-UA-Compatible', 'IE=edge')
		->appendHttpEquiv('content-language', 'en-US');
	?>
	
	<?php $this->headLink()
			->appendStylesheet($this->basePath() . '/css/movieshelves.css')
			->appendStylesheet('//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/themes/smoothness/jquery-ui.css');
	?>
	
	<?php $this->headScript()
			->appendFile('//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js')
			->appendFile('//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js')
			->appendFile($this->basePath() . '/js/core.js',   'text/javascript');
	?>
	
	<?php
		$this->headLink()
			->appendStylesheet($this->basePath() . '/assets/libraries/dataTables-1.9.4/css/jquery.dataTables_themeroller.css');
			
		$this->headScript()
			->appendFile($this->basePath() . '/assets/libraries/dataTables-1.9.4/jquery.dataTables.min.js',   'text/javascript')
			->appendFile($this->basePath() . '/assets/libraries/dataTables-1.9.4/jquery.dataTables.plugIns.js',   'text/javascript');	
	?>
	
	<?php 
		echo $this->headLink();
		echo $this->headScript();
	?>

	<?php
		if ($env == "production") {
			$this->inlineScript()->captureStart();
			echo <<<JS
				var _gaq = _gaq || [];
				_gaq.push(['_setAccount', 'UA-32109855-1']);
				_gaq.push(['_setDomainName', 'eclectic-world.com']);
				_gaq.push(['_trackPageview']);
				
				(function() {
				var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
				ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
				var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
				})();
JS;
			$this->inlineScript()->captureEnd();
		}
	?>
	
	</head>
	<body>
		<div id="page">
			<div id="siteHeader">
				<div id="siteNameBox">
					<a href="<?php echo $this->url('home') ?>" title=""><img src="<?php echo $this->basePath('img/header_movieshelves.gif') ?>" width="461" height="100" alt="Take me to the Movie Shelves front page" /></a>
				</div>
				<div id="siteMenu">
					Can I get a menu?
				</div>
			</div>
			<div id="middleSection">
				<div id="leftSideBar">
					left menu
				</div>
				<div id="pageContent">
					<?php echo $this->pageTitle; ?>
					<?php echo $this->content; ?>
				</div>
			</div>
		</div>
		<?php echo $this->inlineScript(); ?>
	</body>
</html>

Which, when updated, turned our formats table from the last part into this:

For the most part, that code isn't much different from the skeleton one other than changing the actual design.  I did add two little things, however.  First at the top I added <?php $env = getenv('APP_ENV') ?: 'production'; ?>, so I can then use an if statement around my Google Analytics code so as to avoid having it included in pages that are not the live site (and thus inflate the numbers). 

Secondly, instead of having my links and scripts echo immediately, I just set them, so I could then have a conditional statement around the dataTables CSS and JS.  The if ($this->needsDataTables ?: false) only includes the bits for dataTables if the specific view needs them.

Then in my view/movie-shelves/formats/index.phtml file I added one new line to the top, and a few lines to the bottom. I also tweaked the styling a little.

<?php
	$title = 'Formats';
	$this->headTitle($title);
	$this->layout()->needsDataTables = true; // ADDED THIS LINE
?>

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

<div class="smallListingTableWrapper">
	<table class="listingTable">
	<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>
</div>
<?php
// ADDED THIS CHUNK AT THE END
$this->inlineScript()->captureStart();
echo <<<JS
	$(document).ready(function(){
		$(".listingTable").dataTable({
			"bStateSave": false,
			"bJQueryUI": true,
			"bFilter": false,
			"bInfo": false,
			"bPaginate": false,
			"aoColumnDefs": [
				{ "sClass": "alignCenter", "bSortable": false, "aTargets": [ 2 ] }
			]
		});
	});
JS;
$this->inlineScript()->captureEnd();
?>

With that working, time to make the forms to add/edit/delete formats.  Now, given that this is a table with a single column beyond the identifying key, this is pretty quick and easy. First, I configured the form component which adds in displaying, validating, and processing form submissions.

<?php
	namespace MovieShelves\Formats\Form;
	
	use Zend\Form\Form;
	use Zend\Form\Element; // REQUIRED FOR THE BUTTON (NOT USED IN TUTORIAL)
	
	class FormatsForm extends Form {
		public function __construct($name = null) {
			parent::__construct('formats');
			
			$this->setAttributes(array(
				'class'  => 'standardForm'
				));
			
			$this->add(array(
				'name' => 'formatid',
				'type' => 'Hidden',
			));
			$this->add(array(
				'name' => 'formatlabel',
				'type' => 'Text',
				'attributes' => array(
					'id' => 'formatlabel',
					'required' => 'required',
				),
				'options' => array(
					'label' => 'Format Label',
					'class' => "required",
					'attributes' => array(
						'required' => 'required'
					),
					'label_attributes' => array(
						'class' => 'required',
					),
					'label_options' => array(
						'class' => 'required',
					),
				),
			));
			
			// ALL THIS TO MAKE A BUTTON!
			$button = new Element\Button('saveButton');
			$button->setLabel("Save");
			$button->setAttributes(
					array(
						'id' => 'saveButton',
					)
				);
			$this->add($button);
		}
	}
?>

I must admit, this is a place where the Zend docs rather suck – figuring out all the form options.  Most of them just give you a "here is something" and that you can set options and attributes (which are different…) but we won't tell you what all options and attributes are available to you.  Also, it doesn't make it clear the difference between a "regular" form thing and an "element", or if there is one.  And all of the examples seem to go from a very different perspective versus using the Form as we've done here by following the tutorial.

It took quite a bit of Google searching to figure out how to add the classes to the form and labels, how to make it stop putting a damn span on the label and to just do <label>content</label><input … /> versus wrapping the input in the label, which you'd generally only do for radios and check boxes.  Thank you Stack Overflow! 

It also took me a ridiculous amount of time to figure out how to get it to do a BUTTON instead of an INPUT tag for my submit button.  Not only did it require doing the Element button creation above, but I also had to change something in my add.phtml, which I'll get to in a minute.

But first, I also had to modify my Formats.php model to add some use statements at the top and a few more functions down below.

<?php
	namespace MovieShelves\Formats\Model;
	
	// ADDED THESE THREE USE STATEMENTS
	use Zend\InputFilter\InputFilter;
	use Zend\InputFilter\InputFilterAwareInterface;
	use Zend\InputFilter\InputFilterInterface;
	
	class Formats {
		public $formatid;
		public $formatlabel;
		protected $inputFilter; // ADDED THIS!
		
		public function exchangeArray($data) {
			$this->formatid = (!empty($data['formatid'])) ? $data['formatid'] : null;
			$this->formatlabel = (!empty($data['formatlabel'])) ? $data['formatlabel'] : null;
		}
		
		// ADDED THESE TWO FUNCTIONS
		public function setInputFilter(InputFilterInterface $inputFilter) {
			throw new \Exception("Not used");
		}
		
		public function getInputFilter() {
			if (!$this->inputFilter) {
				$inputFilter = new InputFilter();
				
				$inputFilter->add(array(
					'name'     => 'formatid',
					'required' => true,
					'filters'  => array(
						array('name' => 'Int'),
					),
				));
				
				$inputFilter->add(array(
					'name'     => 'formatlabel',
					'required' => true,
					'filters'  => array(
						array('name' => 'StripTags'),
						array('name' => 'StringTrim'),
					),
					'validators' => array(
						array(
							'name'    => 'StringLength',
							'options' => array(
							'encoding' => 'UTF-8',
							'min'      => 1,
							'max'      => 100,
							),
						),
					),
				));
				
				$this->inputFilter = $inputFilter;
			}
			
			return $this->inputFilter;
		}
	}
?>

In the FormatsController, I again added two more use statements and put some content in the addAction method place holder that was already there.

<?php
	namespace MovieShelves\Formats\Controller;
	
	use Zend\Mvc\Controller\AbstractActionController;
 	use Zend\View\Model\ViewModel;
	use MovieShelves\Formats\Model\Formats;  //ADDED THIS
 	use MovieShelves\Formats\Form\FormatsForm; // AND THIS
	
	class FormatsController extends AbstractActionController {
		protected $formatsTable;
		
		public function indexAction() {
			return new ViewModel(array(
				'formats' => $this->getFormatsTable()->fetchAll(),
			));
		}
		
		// FILLED IN THIS FUNCTION!
		public function addAction() {
			$form = new FormatsForm();
			
			$request = $this->getRequest();
			if ($request->isPost()) {
				$formats = new Formats();
				$form->setInputFilter($formats->getInputFilter());
				$form->setData($request->getPost());
				
				if ($form->isValid()) {
					$formats->exchangeArray($form->getData());
					$this->getFormatsTable()->saveFormats($formats);
					
					// Redirect to list of formatss
					return $this->redirect()->toRoute('formats');
				}
			}
			return array('form' => $form);
		}
		
		public function editAction() {
		}
		
		public function deleteAction() {
		}
		
		public function getFormatsTable() {
			if(!$this->formatsTable) {
				$sm = $this->getServiceLocator();
				$this->formatsTable = $sm->get('MovieShelves\Formats\Model\FormatsTable');
			}
			return $this->formatsTable;
		}
	}
?>

And then finally, I had to make the add.phtml. 

<?php
	$title = 'Add New Format';
	$this->headTitle($title);
	$this->layout()->pageTitle = $title;
	
	$form->setAttribute('action', $this->url('formats', array('action' => 'add')));
	$form->prepare();
	
	echo $this->form()->openTag($form) . "\n";
	echo $this->formHidden($form->get('formatid')) . "\n";
	echo $this->formRow($form->get('formatlabel')) . "\n\n";
	echo '<div class="buttonWrapper">' . $this->formButton($form->get('saveButton')) . "</div>\n";
	echo $this->form()->closeTag() . "\n";
?>

As I mentioned above, I had to change one bit from the tutorial for my button to work.  Namely, I had to change formSubmit -> formButton, otherwise it automatically switched it to an input no matter what you do.  And with that, my add form works quite nicely 🙂

Since I built the form with HTML 5 and utilized the "required" attribute, it also has some basic built in validation. 

For this form, of course, it is just that the field is required. To confirm it works fine in non-HTML5 browsers, just remove the 'required' => 'required', attribute from the formatlabel add array in FormatsForm.php. 

The edit option works in a very similar fashion.,  First, in Formats.php, I add a single additional function:

public function getArrayCopy() {
	return get_object_vars($this);
}

Then in FormatsController, we fill in the editAction function like so:

public function editAction() {
	$formatid = (int) $this->params()->fromRoute('formatid', 0);
	if (!$formatid) {
		return $this->redirect()->toRoute('formats', array(
			'action' => 'add'
		));
	}
	
	// Get the Formats with the specified id.  An exception is thrown
	// if it cannot be found, in which case go to the index page.
	try {
		$formats = $this->getFormatsTable()->getFormats($formatid);
	}
	catch (\Exception $ex) {
		return $this->redirect()->toRoute('formats', array(
			'action' => 'index'
		));
	}
	
	$form  = new FormatsForm();
	$form->bind($formats);
	
	$request = $this->getRequest();
	if ($request->isPost()) {
		$form->setInputFilter($formats->getInputFilter());
		$form->setData($request->getPost());
		
		if ($form->isValid()) {
			$this->getFormatsTable()->saveFormats($formats);
			
			// Redirect to list of formatss
			return $this->redirect()->toRoute('formats');
		}
	}
	
	return array(
		'formatid' => $formatid,
		'form' => $form,
	);
}

This code is very similar to what is done with the add action, except that it checks for the existence of the indicated ID.  If no ID is passed, it presumes it's an add and redirects there, otherwise if the requested ID has no match it throws an error.  If it's all good, then it saves.

The edit form is a copy/paste of the add form, with one change.  The line that says $form->setAttribute('action', $this->url('formats', array('action' => 'add'))); is replaced with:

$form = $this->form;
$form->setAttribute('action', $this->url(
	'formats',
	array(
		'action' => 'edit',
		'formatid'     => $this->formatid,
	)
));

While doing this I discovered that I'd misconfigured my routes.  Where I had id I needed to use formatid because that's what I'm using throughout the formats section.  Interestingly enough, Zend helpfully will not include any unallowed attribute on the URL while building links, so at first I was confused as to why my formatid was not getting passed 🙂  I also found that I did the same mistake in FormatsTable, so I fixed those instances of id -> formatid, and then my edit form worked fine as well.

In my next post, I'll work on the delete function, how to put in success/error messages in a nice format, and getting some navigation in this fledgling app!