Building My First Application with Zend, Part 5


After a bit of final tweaking from what I did in part 4, such as adding those owned counts to the other lists of attributes I'd already made (formats, genres, et al), it was time to tackle the last of the CRUD functions, and the biggest.  Namely the manager for the movies themselves!  The movies table has 17 fields (I added one, series_position, after I posted the ERD in part 1).  Four of the table's fields are foreign key fields referencing the afforementioned attribute tables, which will give me my first exposure to doing select inputs.  I also need to be able to upload a thumbnail and a larger cover image, with my own self imposed dimensional limits, which will mean delving into file uploading. I'm also going to change some stuff up in terms of file names and organization to match my understanding of them and to utilize alternate ways of doing some things.

First the file structure.  I really really really really hate the whole one file in a folder thing, so for movies, I decided to try changing it up a bit and just put everything in the Movies folder.  I also went with some different file names based on my other plans fo experimenting.

While controller and form are similar to what I did in the other model (as seen with the Genres model), I renamed the two files from the Model folder.  Movies -> MovieEntity and MoviesTable -> MoviesDAO.  I feel these names better reflect their purpose and function, and are clearer.  I did debate doing MovieBean instead but Entity seems to be more common in the PHP realm versus bean, so I went with that.  The DAO includes what some references refer to as a Mapper.

Changing the folder structure did require changing the name spaces in each file, which you'll see when I get to those.  In my module.config.php, the invokables for the Movies controller is done the same as the rest, it just drops the Controller\ bit.

'controllers' => array(
	'invokables' => array(
		'MovieShelves\Controller\Formats' => 'MovieShelves\Formats\Controller\FormatsController',
		'MovieShelves\Controller\Genres' => 'MovieShelves\Genres\Controller\GenresController',
		'MovieShelves\Controller\Studios' => 'MovieShelves\Studios\Controller\StudiosController',
		'MovieShelves\Controller\Series' => 'MovieShelves\Series\Controller\SeriesController',
		'MovieShelves\Controller\Movies' => 'MovieShelves\Movies\MoviesController', // notice it's slightly shorter 🙂
	),
),

As usual, the first thing I'm going to do with this model is try to get the listing page working., so I'll start with the entity.  Another thing I'm trying here is one of the alternate way of doing the DB ties, using hydrators, and the like. As part of this, my entity for Movie is a bit different (and longer) versus the ones I showed earlier in this article series. 

<?php
	namespace MovieShelves\Movies;
	
	class MovieEntity {
		protected $movieid;
		protected $title;
		protected $runtime_minutes;
		protected $dateacquired;
		protected $num_discs;
		protected $num_episodes;
		protected $thinpaked;
		protected $mpaa_rating;
		protected $issubtitled;
		protected $formats_formatid;
		protected $genres_genreid;
		protected $series_seriesid;
		protected $series_position;
		protected $studios_studioid;
		protected $upcode;
		protected $amazon_asin;
		protected $plotsummary;
		
		public function __construct() {
			// for now, has nothing in it
		}
		
		public function exchangeArray($data) {
			$this->movieid = (isset($data['movieid'])) ? $data['movieid'] : null;
			$this->title = (isset($data['title'])) ? $data['title'] : null;
			$this->runtime_minutes = (isset($data['runtime_minutes'])) ? $data['runtime_minutes'] : null;
			$this->dateacquired = (isset($data['dateacquired'])) ? $data['dateacquired'] : null;
			$this->num_discs = (isset($data['num_discs'])) ? $data['num_discs'] : null;
			$this->num_episodes = (isset($data['num_episodes'])) ? $data['num_episodes'] : null;
			$this->thinpaked = (isset($data['thinpaked'])) ? $data['thinpaked'] : null;
			$this->mpaa_rating = (isset($data['mpaa_rating'])) ? $data['mpaa_rating'] : null;
			$this->issubtitled = (isset($data['issubtitled'])) ? $data['issubtitled'] : null;
			$this->formats_formatid = (isset($data['formats_formatid'])) ? $data['formats_formatid'] : null;
			$this->genres_genreid = (isset($data['genres_genreid'])) ? $data['genres_genreid'] : null;
			$this->series_seriesid = (isset($data['series_seriesid'])) ? $data['series_seriesid'] : null;
			$this->series_position = (isset($data['series_position'])) ? $data['series_position'] : null;
			$this->studios_studioid = (isset($data['studios_studioid'])) ? $data['studios_studioid'] : null;
			$this->upcode = (isset($data['upcode'])) ? $data['upcode'] : null;
			$this->amazon_asin = (isset($data['amazon_asin'])) ? $data['amazon_asin'] : null;
			$this->plotsummary = (isset($data['plotsummary'])) ? $data['plotsummary'] : null;

		}
		
		public function getArrayCopy() {
			return get_object_vars($this);
		}
		
		// GETTERS AND SETTERS
		public function getMovieID() {
			return $this->movieid;	
		}
		public function setMovieID($movieid) {
			$this->movieid = $movieid;	
		}
		
		public function getTitle() {
			return $this->title;	
		}
		public function setTitle($title) {
			$this->title = $title;	
		}
		public function getRunTime_Minutes() {
			return $this->runtime_minutes;	
		}
		public function setRunTime_Minutes($runtime_minutes) {
			$this->runtime_minutes = $runtime_minutes;	
		}
		public function getDateAcquired() {
			return $this->dateacquired;	
		}
		public function setDateAcquired($dateacquired) {
			$this->dateacquired = $dateacquired;	
		}
		public function getNum_Discs() {
			return $this->num_discs;	
		}
		public function setNum_Discs($num_discs) {
			$this->num_discs = $num_discs;	
		}
		public function getNum_Episodes() {
			return $this->num_episodes;	
		}
		public function setNum_Episodes($num_episodes) {
			$this->num_episodes = $num_episodes;	
		}
		public function getThinPaked() {
			return $this->thinpaked;	
		}
		public function setThinPaked($thinpaked) {
			$this->thinpaked = $thinpaked;	
		}
		public function getMPAA_Rating() {
			return $this->mpaa_rating;	
		}
		public function setMPAA_Rating($mpaa_rating) {
			$this->mpaa_rating = $mpaa_rating;	
		}
		public function getIsSubtitled() {
			return $this->issubtitled;	
		}
		public function setIsSubtitled($issubtitled) {
			$this->issubtitled = $issubtitled;	
		}
		public function getFormats_FormatID() {
			return $this->formats_formatid;	
		}
		public function setFormats_FormatID($formats_formatid) {
			$this->formats_formatid = $formats_formatid;	
		}
		public function getGenres_GenreID() {
			return $this->genres_genreid;	
		}
		public function setGenres_GenreID($genres_genreid) {
			$this->genres_genreid = $genres_genreid;	
		}
		public function getSeries_SeriesID() {
			return $this->series_seriesid;	
		}
		public function setSeries_SeriesID($series_seriesid) {
			$this->series_seriesid = $series_seriesid;	
		}
		public function getSeries_Position() {
			return $this->series_position;	
		}
		public function setSeries_Position($series_position) {
			$this->series_position = $series_position;	
		}
		public function getStudios_StudioID() {
			return $this->studios_studioid;	
		}
		public function setStudios_Studioid($studios_studioid) {
			$this->studios_studioid = $studios_studioid;	
		}
		public function getUPCode() {
			return $this->upcode;	
		}
		public function setUPCode($upcode) {
			$this->upcode = $upcode;	
		}
		public function getAmazon_ASIN() {
			return $this->amazon_asin;	
		}
		public function setAmazon_ASIN($amazon_asin) {
			$this->amazon_asin = $amazon_asin;	
		}
		public function getPlotSummary() {
			return $this->plotsummary;	
		}
		public function setPlotSummary($plotsummary) {
			$this->plotsummary = $plotsummary;
		}
	}
?>

Way more code for sure, though this sort of entity is much more similar to what we were doing with Mach-II 1.6 apps. I did look at using "magic" getters/setters, which we used in 1.8 Mach-II apps and would avoid having to individually create them, but the overall consensus I got from Google searching was that they add unnecessary overhead and that by having them individually made, you are writing more explicit code.  It also will be useful later if I need to manipulate any of the data coming in or going out of the entity.

Now that I have my entity, on the DAO. 

<?php
	namespace MovieShelves\Movies;
	
	use MovieShelves\Movies\MovieEntity;
	use Zend\Db\Adapter\Adapter;
	use Zend\Db\Sql\Sql;
	use Zend\Db\Sql\Select;
	use Zend\Db\ResultSet\HydratingResultSet;
	use Zend\Stdlib\Hydrator\ClassMethods;
	
	class MoviesDAO {
		protected $tableName = 'movieshelves_movies';
		protected $dbAdapter;
		protected $sql;
		
		public function __construct(Adapter $dbAdapter) {
			$this->dbAdapter = $dbAdapter;
			$this->sql = new Sql($dbAdapter);
			$this->sql->setTable($this->tableName);
		}
		
		public function fetchAll() {
			$select = new Select('movieshelves_movies');

			$select->quantifier('DISTINCT STRAIGHT_JOIN')
				->columns(array(
						'movieid', 
						'title', 
						'runtime_minutes', 
						'dateacquired', 
						'num_discs', 
						'num_episodes', 
						'thinpaked', 
						'mpaa_rating', 
						'issubtitled', 
						'formats_formatid', 
						'genres_genreid', 
						'series_seriesid', 
						'series_position', 
						'studios_studioid', 
						'upcode', 
						'amazon_asin',
						'plotsummary', 
					)
				)
				->join(
					'movieshelves_formats',
					'movieshelves_movies.formats_formatid = movieshelves_formats.formatid',
					array('formatlabel'),
					'INNER'
				)
				->join(
					'movieshelves_genres',
					'movieshelves_movies.genres_genreid = movieshelves_genres.genreid',
					array('genre'),
					'INNER'
				)
				->join(
					'movieshelves_series',
					'movieshelves_movies.series_seriesid = movieshelves_series.seriesid',
					array('seriesname'),
					'LEFT'
				)
				->join(
					'movieshelves_studios',
					'movieshelves_movies.studios_studioid = movieshelves_studios.studioid',
					array('studio'),
					'LEFT'
				)
			;
			
			$statement = $this->sql->prepareStatementForSqlObject($select);
			$results = $statement->execute();
			
			$entityPrototype = new MovieEntity();
			$hydrator = new ClassMethods(false);
			$resultset = new HydratingResultSet($hydrator, $entityPrototype);
			$resultset->initialize($results);
			
			return $resultset;	
		}
	}
?>

Quite a bit different here, so let's go chunk by chunk.

<?php
	namespace MovieShelves\Movies;
	
	use MovieShelves\Movies\MovieEntity;  // New
	use Zend\Db\Adapter\Adapter; // New
	use Zend\Db\Sql\Sql; // New
	use Zend\Db\Sql\Select;
	use Zend\Db\ResultSet\HydratingResultSet; // New
	use Zend\Stdlib\Hydrator\ClassMethods; // New
?>

First, notice the namespace reflects the dropping of the Model folder, so now it's just MovieShelves\Movies. My DAO also has way more use statements than you'd find in one of my Tables files, which just had two.

use Zend\Db\TableGateway\TableGateway;
use Zend\Db\Sql\Select;

Now the TableGateway use statement is gone and replaced by four marked as new above. Notice the Entity is also included with a use here, rather than having it be pushed in from the Module.php file. 

Inside my actual class, the protected variable $tableGateway is replaced with three protected variables, one for the tableName and then ones for the dbAdapter and SQL objects.  The construct function is also very different, expecting an adapter instead of a TableGateway and populating my protected variables appropriately.

	protected $tableName = 'movieshelves_movies';
	protected $dbAdapter;
	protected $sql;
	
	public function __construct(Adapter $dbAdapter) {
		$this->dbAdapter = $dbAdapter;
		$this->sql = new Sql($dbAdapter);
		$this->sql->setTable($this->tableName);
	}

Now, as part of those changes, the way this DAO is added in Module.PHP is different as well.  So before we look at the fetchAll function in detail, let's hop over there to note the changes.  I've heavily snipped this sample so we can just focus on the differences, with Genres stuff left as a contrast.

<?php
	...
	use MovieShelves\Genres\Model\Genres;
	use MovieShelves\Genres\Model\GenresTable;
	...
	use MovieShelves\Movies\MoviesDAO;
	
	class Module implements AutoloaderProviderInterface, ConfigProviderInterface {
		...
		public function getServiceConfig() {
			return array(
				'factories' => array(
					...
					// Genres
					'MovieShelves\Genres\Model\GenresTable' =>  function($sm) {
						$tableGateway = $sm->get('GenresTableGateway');
						$table = new GenresTable($tableGateway);
						return $table;
					},
					'GenresTableGateway' => function ($sm) {
						$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
						$resultSetPrototype = new ResultSet();
						$resultSetPrototype->setArrayObjectPrototype(new Genres());
						return new TableGateway('movieshelves_genres', $dbAdapter, null, $resultSetPrototype);
					},
					...
					// Movies
					'MoviesDAO' => function ($sm) {
						$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
						$mapper = new MoviesDAO($dbAdapter);
						
						return $mapper;
					}
				),
			);
		}
	}
?>

I went from two use statements to one, and instead of having to do two factory addditions, I just do the single one for the DAO that calls the DB adapter and passes it to a new instead of the DAO which it then returns. The longer code in the Entity and DAO resulted in less code needed in the module, which I liked. 

Now back to MoviesDAO and the fetchAll() function.  The first part, which builds my select, really isn't much different from the other models, except it features chained joins and I pass in some quantiers to tell it to pull only distint records and to utilize STRAIGHT_JOIN to ensure my tables are joined in the order given.  Since format and genre are required on all movies, I inner joined those then left join my two optional fields.

	public function fetchAll() {
		$select = new Select('movieshelves_movies');

		$select->quantifier('DISTINCT STRAIGHT_JOIN')
				->columns(array(
					'movieid', 
					'title', 
					'runtime_minutes', 
					'dateacquired', 
					'num_discs', 
					'num_episodes', 
					'thinpaked', 
					'mpaa_rating', 
					'issubtitled', 
					'formats_formatid', 
					'genres_genreid', 
					'series_seriesid', 
					'series_position', 
					'studios_studioid', 
					'upcode', 
					'amazon_asin',
					'plotsummary', 
				)
			)
			->join(
				'movieshelves_formats',
				'movieshelves_movies.formats_formatid = movieshelves_formats.formatid',
				array('formatlabel'),
				'INNER'
			)
			->join(
				'movieshelves_genres',
				'movieshelves_movies.genres_genreid = movieshelves_genres.genreid',
				array('genre'),
				'INNER'
			)
			->join(
				'movieshelves_series',
				'movieshelves_movies.series_seriesid = movieshelves_series.seriesid',
				array('seriesname'),
				'LEFT'
			)
			->join(
				'movieshelves_studios',
				'movieshelves_movies.studios_studioid = movieshelves_studios.studioid',
				array('studio'),
				'LEFT'
			)
		;
		...
?>

Where things really change up is when it comes to executing the select. In previous models, the select was followed with just two lines to send the select to the tableGateway and return the results.  It could even be compacted to one line, if you wanted.

$resultSet = $this->tableGateway->selectWith($select);
	
return $resultSet;

Now however, I first send my select to the SQL functions to the prepareStatementForSqlObject, which will generate the statement interface for us, then tell it to execute the statement it made.  After that, the results are passed to the hydrator, which takes my results and converts them into individual instances of my movie entity.  The ClassMethods is one type of hydrator, that looks for getter/setter functions in the entity to perform the hydration.  You can also use ObjectProperty, which would eliminate the need for getter/setters and would use the public functions, much like TableGateway does. Once it's all hydrater, I return the results as usual.

		...
		$statement = $this->sql->prepareStatementForSqlObject($select);
		$results = $statement->execute();
		
		$entityPrototype = new MovieEntity();
		$hydrator = new ClassMethods(false);
		$resultset = new HydratingResultSet($hydrator, $entityPrototype);
		$resultset->initialize($results);
		
		return $resultset;	
	}

Now I will be perfectly honest and say that this point I have no idea which way is "better" – tableGateway or doing this type of hydrating. I couldn't really find any explanation for the pros/cons of any of the DB methods, so I mostly did this as a way of trying it and seeing how it works.

Anyway, you may have noticed that my joins above not only let us get the movie details, but also the text versions of the foreign key items, i.e. the label, category, etc.  So that these additional details are available, the entity had to be modified to add the appropropriate protected variables, exchange array items, and getter/setters for the new details.  The format is exactly the same so I won't repaste it here. That gave me close to what I wanted…but for some things, I didn't want the raw data displayed and there was no joining table. I mean no one (sane) makes a connector table that just has yes/no. 

I could change the query to IF those fields and convert them, but I decided to use one of the benefits listed of getters/setters, manipulating the input/output. For example, I changed IsSubtitled to convert the 0/1 to No/Yes.

	public function getIsSubtitled() {
		if ($this->issubtitled == '1')
			$this->issubtitled = 'Yes';
		else
			$this->issubtitled = 'No';
			
		return $this->issubtitled;	
	}

I did a similar thing for the num_episodes to return N/A instead of 0 for regular films.  Is this a best practice?  I have no idea…it's another thing I couldn't find an answer about one way or another.  Put it works, so yay. 

Finally, I have my index.phtml.  The difference here is a bit more subtle. Can you find it? 🙂

<?php
	$title = 'Movies';
	$this->headTitle($title);
	$this->layout()->page = $title;
	$this->layout()->needsDataTables = true;
?>

<div class="fullTableWrapper">
	<table class="sortedTable">
	<thead>
		<tr>
			<th>ID</th>
			<th>Title</th>
			<th>Acquired</th>
			<th>MPAA</th>
			<th>Format</th>
			<th>Genre</th>
			<th>Series</th>
			<th>Studio</th>
			<th> </th>
		</tr>
	</thead>
	<tbody>
		<?php foreach ($movies as $movie) : ?>
		<tr>
			<td><?php echo $this->escapeHtml($movie->getMovieID());?></td>
			<td><?php echo $this->escapeHtml($movie->getTitle());?></td>
			<td><?php echo $this->escapeHtml($movie->getDateAcquired());?></td>
			<td><?php echo $this->escapeHtml($movie->getMPAA_Rating());?></td>
			<td><?php echo $this->escapeHtml($movie->getFormatLabel());?></td>
			<td><?php echo $this->escapeHtml($movie->getGenre());?></td>
			<td>
				<?php 
					if (!empty($movie->getSeriesName())) 
						echo $this->escapeHtml($movie->getSeriesName());
					
					if (!empty($movie->getSeries_Position())) 
						echo "(" . $this->escapeHtml($movie->getSeries_Position()) . ")";
				?>
			</td>
			<td><?php echo $this->escapeHtml($movie->getStudio());?></td>
			<td>
				<a href="<?php echo $this->url('movies', array('action'=>'edit', 'movieid' => $movie->getMovieID()));?>">Edit</a> ~ 
				<span class="pseudoLink deleteLink" MovieID="<?php echo $this->escapeHtml($movie->getMovieID());?>" Title="<?php echo $this->escapeHtml($movie->getTitle());?>">Delete</span>
			</td>
		</tr>
		<?php endforeach; ?>
	</tbody>
	</table>
	<p class="alignRight tableFooter"><a href="<?php echo $this->url('movies', array('action'=>'add'));?>">Add new movie</a></p>
</div>

<div id="confirmDelete" title="Delete Movie" class="helpDialogs">
	<p>Are you certain you wish to delete <em id="moviesNameHere"></em> from your collection?</p>
</div>

<?php $this->inlineScript()->captureStart(); ?>
	$(document).ready(function(){
		$(".sortedTable").dataTable({
			"bStateSave": false,
			"bAutoWidth": false,
			"bJQueryUI": true,
			"aaSorting": [[ 1, "asc" ]],
			"iDisplayLength": 10,
			"sPaginationType": "full_numbers",
			"aoColumnDefs": [
				{ "sWidth": "100px", "aTargets": [ 3 ] },
				{ "sClass": "alignCenter", "aTargets": [ 2, 4 ] },
				{ "sWidth": "130px", "sClass": "alignCenter", "bSortable": false, "aTargets": [ 8 ] }
			],
			"fnInitComplete": function() {
				/* auto change settings if it has fewer than 25 rows */
				var oListSettings = this.fnSettings();
				var wrapper = this.parent();
				
				if (oListSettings.fnRecordsTotal() < 25) {
					$('.dataTables_paginate', wrapper).hide();
					$('.dataTables_length', wrapper).hide();
				}
			}
		});
		$(".sortedTable").on("click", '.deleteLink', function(){
			var MovieID = $(this).attr("MovieID");
			var Title = $(this).attr("Title");
			var linkToGoTo = '<?php echo $this->url('movies');?>delete/' + MovieID;
			
			$("#moviesNameHere").html(Title);
			
			$("#confirmDelete").dialog({
				resizable: false,
				width: 450,
				height: 275,
				modal: true,
				buttons: {
					 "Yes, Delete It": function() {
						window.location.href = linkToGoTo;
					 },
					 "No, Go Back": function() {
						$( this ).dialog( "close");
					 }
				}
			});
		});

	});
<?php $this->inlineScript()->captureEnd(); ?>

In my foreach loop, I have to call each data point a little differently.  Instead of $movie->movieid, I now must use the getter, so it is now $movie->getMovieID(). Otherwise it's all the same as the rest of my indexes.  I don't have all the fields showing because the table gets useless once you throw in the plot summary and the other data points are less important for browsing.

The end result?

Not bad. 🙂  As I expected, though, this will require multiple posts, so in the next part, I'll go over the form stuff.