Advancing My First Zend App, Part 3: Flipping the Layout


Before I continued on some more, I realized I needed to "fix" my MovieShelves module to make it's routes more uniquely named, otherwise it will conflict with other modules. ๐Ÿ˜›  I also updated my app to bring the DataTables code to 1.10 standards and to do a few visual tweaks.  Then I built the movie shelves front page to include the five newest items and the five random highlights. Doing this wasn't really complicated, but I did learn one nifty trick for getting a set number of random records from MySQL.  So for my getRandom function, I did this:

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

	$select->quantifier('DISTINCT STRAIGHT_JOIN')
		->columns(array(
				'movieid', 
				'title', 
				'dateacquired', 
				'formats_formatid', 
				'series_seriesid'
			)
		)
		->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'
		)
		->order(new \Zend\Db\Sql\Expression("RAND()"))
		->limit(5)
	;
	
	$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, as a caution, per notes on this method performance may degrade once you're dealing with 10,000 plus records, but for this app that isn't an issue so yay.  I also modified my entity for retrieving the thumbnails. I was torn on how to do this at first, but I finally decided that adding a getMovieCover() function to the entity was still in keeping with it's purpose as it is getting data about the object, just without being loaded from anywhere else.

public function getMovieCover() {
	$directoryRoot = str_replace($_SERVER['SCRIPT_NAME'], '', $_SERVER['SCRIPT_FILENAME']);
	
	$moviecover = array();
	
	if ($this->movieid > 0) {
		$thumbNailFile = "/dropinn/covers/movieshelves/thumbnails/movie_" . $this->movieid . ".png";
		$coverFile = "/dropinn/covers/movieshelves/fullcovers/movie_" . $this->movieid . ".png";
		
		// do thumbnail
		if (file_exists($directoryRoot . $thumbNailFile)) {
			$thumbSize = getimagesize($directoryRoot . $thumbNailFile);
			$moviecover["thumbnail_url"] = $thumbNailFile;
			$moviecover["thumbnail_dimensions"] = $thumbSize[3];
		}
		else {
			$moviecover["thumbnail_url"] = '/img/noCoverAvailable.png';
			$moviecover["thumbnail_dimensions"] = 'width="150" height="210"';
		}
		
		// do full - we skip the no cover since we just don't show for that one
		if (file_exists($directoryRoot . $coverFile)) {
			$coverSize = getimagesize($directoryRoot . $coverFile);
			$moviecover["fullcover_url"] = $coverFile;
			$moviecover["fullcover_dimensions"] = $thumbSize[3];
		}
	}
	$this->moviecover = $moviecover;	
	
	return $this->moviecover;	
}

Now you'll notice I did pretty much hard code the directory paths a bit.  I could have set this in our config and then injected the values into the entity, theoretically, but I really didn't see enough of a benefit to it bother right now. With the function in the entity, it's easy for me to call up the thumbnail while looping the getNewest and getRandom results on the public index.

<p>I love watching anime, b-disaster movies, and the like whenever I want, so I have quite a few DVDs.  This database helps me remember what I have (especially when out shopping) and acts as an inventory for insurance purposes. Of course, it also lets me share my collection with others, like you!</p>
<h2 class="subTitle">Newest Acquisitions</h2>
<div class="pure-g">
	<?php foreach ($qNewest as $movie): ?>
	<div class="itemBox pure-u-1-5 pure-u-sm-1">
		<?php 
			if (!empty($movie->getMovieCover())): 
				$movieCover = $movie->getMovieCover();
				echo '<p><a href="' . $this->url('movies', array('action'=>'publicMovieDetails', 'movieid' => $movie->getMovieID()))
					. '"><img src="' . $movieCover['thumbnail_url'] . '" ' .  $movieCover['thumbnail_dimensions'] 
					. ' alt="' . $this->escapeHtml($movie->getTitle()) . ' Cover" class="smallCover pure-img" /></a></p>';
			endif; 
		?>
		<p class="title"><a href="<?php $this->url('movies', array('action'=>'publicMovieDetails', 'movieid' => $movie->getMovieID())) ?>"><?php echo $this->escapeHtml($movie->getTitle()); ?></a></p>
		<p class="dateLine"><?php echo $this->dateFormat(strtotime($movie->getDateAcquired()), IntlDateFormatter::LONG, IntlDateFormatter::NONE, "en_US"); ?></p>
		<p><?php echo $movie->getGenre() . " (" . $movie->getFormatLabel() . ")"; ?></p>
	</div>
	<?php endforeach; ?>
</div>

<h2 class="subTitle">Random Highlights</h2>
<div class="pure-g">
	<?php foreach ($qRandom as $movie): ?>
	<div class="itemBox pure-u-1-5 pure-u-sm-1">
		<?php 
			if (!empty($movie->getMovieCover())): 
				$movieCover = $movie->getMovieCover();
				echo '<p><a href="' . $this->url('movies', array('action'=>'publicMovieDetails', 'movieid' => $movie->getMovieID()))
					. '"><img src="' . $movieCover['thumbnail_url'] . '" ' .  $movieCover['thumbnail_dimensions'] 
					. ' alt="' . $this->escapeHtml($movie->getTitle()) . ' Cover" class="smallCover pure-img" /></a></p>';
			endif; 
		?>
		<p class="title"><a href="<?php $this->url('movies', array('action'=>'publicMovieDetails', 'movieid' => $movie->getMovieID())) ?>"><?php echo $this->escapeHtml($movie->getTitle()); ?></a></p>
		<p><?php echo $movie->getGenre() . " (" . $movie->getFormatLabel() . ")"; ?></p>
	</div>
	<?php endforeach; ?>
</div>

And here's what our movies front page now looks like:

Notice I also added a menu bar at the top for navigating the main sections of the site.  ๐Ÿ™‚  I made that top menu into a separate little phtml file under Application/view/menubars called public-top-menu.phtml.  To have it automatically added to the template, I added it to my template map:

'view_manager' =< array(
		'display_not_found_reason' =< true,
		'display_exceptions'	   =< true,
		'doctype'				  =< 'HTML5',
		'not_found_template'	   =< 'error/404',
		'exception_template'	   =< 'error/index',
		'template_map' =< array(
			'application/index/index' =< __DIR__ . '/../view/application/index/index.phtml',
			'error/404'			   =< __DIR__ . '/../view/error/404.phtml',
			'error/index'			 =< __DIR__ . '/../view/error/index.phtml',
			'menubars/public-top-menu'	=< __DIR__ . '/../view/menubars/public-top-menu.phtml'
		),
		'template_path_stack' =< array(
			__DIR__ . '/../view',
		),
	),

So I could then just use the render helper in the layout by adding this one line where I wanted the top menu to appear:

<?php echo $this->render('menubars/public-top-menu') ?>

I also broke out the side menus from the Zend Navigation thing to their own menubars as well, to make it easier for the next bit. 

With that function added and the other clean up done, I was ready to look at another function that is common with our "real-world" applications (i.e. ones for work), namely flipping the design of the site based on which section you're in.  Often times this just involves changing the base template so that it includes a different set of CSS files and maybe minor layout changes.  We usually use physically separate template files for each so we have cleaner code and easier management. To mimic this with my application here, I decided to completely change the admin interface to use a different (and singularly cohesive) design.

I started by splitting my CSS file into two files – a Core file that sets up the standards used across the site, and then section specific ones that deal with the area specific tweaks.  For example, in my core.css, I set specifics for fonts, margins, etc.

* {
	-webkit-box-sizing: border-box;
	-moz-box-sizing: border-box;
	box-sizing: border-box;
}
/* Tag restyling */
body {
	font: 16px/1.125em 'Open Sans', sans-serif;
	margin: 0;
	padding: 0;
}
html, button, input, select, textarea, p, table, ul, dl, ol, .pure-g [class *= "pure-u"] { font: 1em 'Open Sans', sans-serif; }
h1, h2, h3, h4 {
	margin: 0.625em 0;
	padding: 0;
	font-weight: bold;
	text-align: center;
}
h1 { font-size: 2.5em /* 2.500em/16px */; }
h2 { font-size: 2.19em /* 35px/16px */; }
h3 { font-size: 1.875em /* 30px/16px */; }
h4 { font-size: 1.625em /* 25px/16px */; }
img { border: 0; }
p {
	padding: 0 1.25em;
	margin-bottom: 0.9375em;
	margin-top: 0em;
}
table {
	padding: 0em;
	border-spacing: 0em;
	margin: 0.3125em auto 0.625em;
}
th, tr, td {
	vertical-align: top;
	padding: 0.125em;
}
th { white-space: nowrap; }
/* Layout Divs */
#layout {
	clear: both;
	background: #FFF;
	padding: 0 !important;
	width: 98%;
	margin: 0.625em auto;
}
** SNIP **

While in movieshelves.css, I only deal with color changes – i.e. backgrounds, fonts, links, and borders, so the entire file is quite small.

body { background: #300; }
h1 { color: #330066; }
h2 { color: #333366; }
h3 { color: #336699; }
h4 { color: #6666CC; }
a { color: #00F; }
a:hover { color: #127290; }
#pageHeader {
	border: 0.0625em solid #900;
	background: #000 url('/img/header_movieshelves_background.jpg');
}
#pageHeader #loginLink a { color: #FFF; }
#pageHeader #topMenu ul li a {
	color: #FFF;
	background-color: #300;
	border-top: 1px solid #900;
	border-right: 1px solid #900;
	border-left: 1px solid #900;
}
#pageHeader #topMenu li a:hover, .pure-menu li a:focus { background-color: #009; }
#pageBody {
	border: 0.0625em solid #900;
	background: #FFC;
}
#sideMenu { color: #330000; }
#pageContent { border-left: 0.0625em solid #900; }
.fullCover { border: 0.063em solid #630; }
.pseudoLink { color: #00F; }
.sortedTable {
	border-right: 0.0625em solid #d3d3d3;
	border-left: 0.0625em solid #d3d3d3;
}
.sortedTable th { color: #333; }
.sortedTable tbody tr td:first-child { border-left: 0.0625em solid #d4d1bf; }
.sortedTable tbody tr td:last-child { border-right: 0.0625em solid #d4d1bf; }
.dataTables_wrapper .dataTables_paginate .fg-button { border: 1px solid #D4D1BF !important; }

With that taken care of, I was ready to make an admin-layout.phtml template that pulls in the new core.css with it's own admin.css. Since the plan for this admin area is that will just pull in all the admin functions across our (eventually) multiple modules, for the most part it just needs the nice layout, styling, and menus.  All functionality will come from the individual modules. So I made the layout template and it's CSS, then I added it to the module.config.php template_map, the same as I did the top menu above.  Then it was just a minor change in my Authentication module's authorizationCheck function to add the lines indicated below.

public function authorizationCheck(MvcEvent $event) {
	// get some variables for easier reference later
	$requestedRoute = $event->getRouteMatch()->getMatchedRouteName();
	$requestedAction = $event->getRouteMatch()->getParam('action');
	
	$authenticationService = $event->getApplication()
			->getServiceManager()
			->get('AuthenticationService');
		
	// we only care about authentication on public events.
	if (strpos($requestedRoute, 'public') === false && strpos($requestedAction, 'public') === false) {
		// If they are trying to log, just let them do so, no need to do an endless loop now :-)
		if ($requestedRoute === 'login') {
			return;
		}
		// check authentication, if not, redirect to log in
		elseif ($authenticationService->isAuthenticated() === false) {
			$response = $event->getResponse();
			$response->getHeaders()
				->clearHeaders()
				->addHeaderLine(
					'Location', '/login'
				);
			$response->setStatusCode(302)
				->sendHeaders();
				
			exit;	
		}
		// if we're authenticated, freshen up our session so we don't get timed out while active
		else {
			$authenticationService->renewSession();
			
			// while we're at it, flip the layout -- ADDED THESE FOUR LINES!
			if (strpos($requestedRoute, 'public') === false && strpos($requestedAction, 'public') === false) {
				$controller = $event->getTarget();	
				
				$controller->layout('layout/admin-layout');
			}

		}
	}
	// otherwise just renew the session
	elseif ($authenticationService->isAuthenticated()) {
		$authenticationService->renewSession();
	}
	
	return true;
}

And that's it. Now when users log in, the layout automatically changes to the admin side.  ๐Ÿ™‚  Now, I could have also done this in the individual controller actions by changing it on the ViewModel before returning it.  Doing that though would require splitting up my "fetchAll" functions into public and admin stuff even though they return the same things.  I'll post a screen shot next article once I finish laying it out.