Writing a CMS/Community with Smarty and the Zend Framework: Part 5

In this part we will take a look at what happens after a user logs on. We will also revisit quite a lot of code that has been changed.

First of all, part 4 has been updated with new information in a reply from me to a request. You might want to take a look at that.

Let’s begin by looking at the login function in UserController again:

function login($params = false){
	if($params['do'] == 'login'){
		$usr_info = $this->obj->fetchRowToArr(false, false, false, $params);
		if($usr_info != false){
			$this->globalsess->usr_row = $usr_info;
			$this->globalsess->usr_info = $usr_info->toArray();
			$result = $this->extraLoginCheck();
			if($result !== true)
				return $result;
			else{
				$this->setAclLevel($usr_info);
				$this->rdrParams["f"] = "desktop";
				$this->refresh($this->rdrParams);
			}
		}else
			$this->smarty->assign('val_err', 'yes');
	}
	
	return $this->getLoginForm();
}

So, upon successful login we set some member variable called $this->rdrParams[”f”] to “desktop” and then we call a new function called refresh. As you might suspect, the point of the refresh function is to reload the page, but why? Well, since we call all logic with the help of Smarty insert functions and return the results we have no way of affecting the rendering of other objects. In case of a successful login for instance, the menus above the main content area are already rendered, they can not magically change as a result of what happens in the login function. For instance, adding a Logout item to one of the menus is impossible if we do not refresh the whole page.

If we revisit the init function of the UserController:

function init(){
	parent::init();
	$this->name			= 'user';
	$this->obj 			= Common::loadModel($this->name);
	$this->rdrParams	= array("index" => "d", "c" => $this->name);
	if(!empty($this->globalsess->usr_row))
		$this->globalsess->usr_row->setTable($this->obj);
	parent::finishInit();
}

There you have the rest of the rdrParams variable, apparently we are storing a default path to use in case we need to redraw the whole page from within the UserController.

function refresh($params = false){
	$location = "Location: {$this->base_url}";
	if(is_array($params)){
		foreach($params as $key => $value)
			$location .= "/$key/$value";
	}
	header($location);
	exit;
}

So with the information above we see that the URL we use to redraw the page with, will end with index/d/c/user/f/desktop:

function desktop($params = false){
	$this->chkSessRdr($params);
	return $this->fetch('desktop.tpl');
}

function chkSessRdr($params = false){
	if(!$this->chkSess())
		$this->refresh($params);
}

function chkSess($function = '', $params = ''){
	if(empty($this->globalsess->usr_info)){
		if(!empty($function))
			return $this->$function( $params );
		return false;
	}
	return true;
}

So when we call the desktop function after the login we will also call chkSessRdr() which will use chkSess() with the default values. In this case globalsess->usr_info will not be empty because it was set during the login. ChkSess() will therefore return true and no redirection will take place, we will simply return the contents of desktop.tpl:

<table>
	<tr>
		<td>
			{insert name="item" controller="Menu" id="usermenu"}
		</td>
		<td>
			{insert name="item" controller="Article" id="sitenews"}
		</td>
	</tr>
</table>

The menu above will now change based on settings set in the future admin module. At the moment we make do with phpMyAdmin. When the user is logged in we will now display Logout and Desktop links. If the user is logged out we display Register and Login links. From the MenuController:

function filterItems(&$menu_items, $have_session){
	$rarr = array();
	foreach($menu_items as &$item){
		if( ($have_session && $item['sess_show']) || (!$have_session && $item['nosess_show']) )
			$rarr[] = $item;
	}
	return $rarr;
}

function render($params){
	$menu = $this->obj->fetchRowToArr($params['id'], false, 'menu_slug');

	$have_session = empty($this->globalsess->usr_info) ? false : true;
	$menu_items = $menu->findDependentRowset('menuitem', 'db_menu')->toArray();
	$menu_items = $this->filterItems($menu_items, $have_session);
	Common::sort2DAsc($menu_items, 'position');
	
	$this->smarty->assign_by_ref('menu', $menu->toArray());
	$this->smarty->assign_by_ref('menuitems', $menu_items);
	return $this->fetch();
}

The desktop will have a usermenu and display some article with the slug “sitenews”. In my case the usermenu will only have two alternatives so far; edit profile, and view profile. Let’s take a look at what happens if our user chooses to edit her profile:

function update(){
	$this->smarty->assign_by_ref('prepop', $this->globalsess->usr_info);
	return $this->smarty->fetch('user_update_form.tpl');
}

We simply prepopulate a form with the current values and display it:

{config_load file="$config_file"}
{assign var="input_class" value=""}
{assign var="label_class" value=""}
{insert name="val" ctrl="user" field="password" 	val="atleast-6"}
{insert name="val" ctrl="user" field="password2" 	val="pass_again-password"}
{insert name="val" ctrl="user" field="avatar" 		max_width="100" max_height="100" folder="upload"}
<form action="{$baseUrl}/index/d/c/user/f/saveUpdate" method="post" enctype="multipart/form-data">
<table cellpadding="0" cellspacing="0" border="0">
<tr>
	<td class="{$label_class}">
		{#description#}:
	</td>
	<td colspan="2" class="{$input_class}">
		<textarea name="description">{$prepop.description}</textarea>
	</td>
</tr>
<tr>
	<td class="{$label_class}">
		{#avatar#}:
	</td>
	<td class="{$input_class}">
		<input type="file" name="avatar" class="{$input_class}"/>
	</td>
	<td>
		{$val_error.avatar}
	</td>
</tr>
.
.
.

Currently the form has four fields, the about me description, small avatar upload and password one and two.

New stuff:
{insert name=”val” ctrl=”user” field=”avatar” max_width=”100″ max_height=”100″ folder=”upload”}

Let’s revisit insertVal() in UserController:

function insertVal($params){
	if(!empty($params['val'])){
		$validation = $this->validationToArr($params['val']);
		$this->session->fields[$params['field']]['validation'] = $validation;
	}
	
	if(!empty($params['folder'])){
		
		if(empty($params['max_width']) || empty($params['max_height'])){
			echo "Error: An image needs 'max_width' and 'max_height' in pixels.";
			exit;
		}else if(empty($params['folder'])){
			echo "Error: An image needs a 'folder' to upload to.";
			exit;
		}
			
		$this->session->images[$params['field']]['max_width'] 	= $params['max_width'];
		$this->session->images[$params['field']]['max_height'] 	= $params['max_height'];
		$this->session->images[$params['field']]['folder'] 		= $params['folder'];
	}
}

So the new information gets saved in a session variable called “images”. Note the pluralism here, I’m already preparing for the future gallery extension with multiple image uploads. Onwards to saveUpdate() and Co:

function saveUpdate($params = false){
	$post = $this->_request->getParams();
	$result = $this->validatePost($post, $this->session->fields, "insert");
	if($result === true){
		$images = $this->upload($this->session->images);
		if(!empty($images['avatar'])){
			if(!empty($this->globalsess->usr_row->avatar))
				unlink($this->globalsess->usr_row->avatar);
			$this->globalsess->usr_row->avatar = $images['avatar'];
		}
		$this->globalsess->usr_row->setFromArray( $this->intersectPost($post) );
		$this->globalsess->usr_info = $this->globalsess->usr_row->toArray();
		$this->globalsess->usr_row->save();
		return $this->view();
	}else{
		return $this->validationError('update', $post, $result);
	}
}

function intersectPost(&$post){
	return array_intersect_key($post, $this->globalsess->usr_info);
}

function viewProfile(){
	$this->smarty->assign_by_ref('user', $this->globalsess->usr_info);
	return $this->fetch('user_profile.tpl');
}

To get the above logic to work We’ve had to add the following In UserController init() and In the index.php boostrap file:

if(!empty($this->globalsess->usr_row))
	$this->globalsess->usr_row->setTable($this->obj);

Zend_Loader::loadClass('Zend_Db_Table_Row');
Zend_Loader::loadClass('Zend_Db_Table_Row_Abstract');
Zend_Loader::loadClass('Zend_Db_Table_Rowset');
Zend_Loader::loadClass('Zend_Db_Table_Rowset_Abstract');

The above lets us load the user information into a row object with the help of session data, no database fetching in order to re-activate the object is required. This functionality is covered thoroughly in the ZF manual. The reason we keep the user information in a row object in addition to an array is convenience, the row object has a few nifty functions in addition to the save function that could come in handy. Having the information duplicated in an array will let us have easy access to it in other modules where we probably just need to read values. Actually activating the user row object there would be overkill. But damit, saveUpdate() could really benefit from an alias for $this->globalsess->usr_row…

Let’s upload that avatar:

function upload($image_fields){
	$insert_arr = array();
	foreach($image_fields as $key => $value){
		if($_FILES[$key]['name'] != ''){
			$insert_arr[$key] = $value['folder']."/".uniqid().".jpg";
			$move_result = move_uploaded_file($_FILES[$key]['tmp_name'], $insert_arr[$key]);
			if($move_result){
				$result = Common::reScaleImage($insert_arr[$key], $value['max_width'], $value['max_height']);
				if(!$result){
					unlink($insert_arr[$key]);
					$insert_arr[$key] = '';
				}else
					chmod($insert_arr[$key], 0777);
			}else
				$insert_arr[$key] = '';
		}
	}
	return $insert_arr;
}

We try and move each image in the array and if it works we rescale them according to what was assigned above the update form in the template, by some designer or project manager probably. 100×100 if I remember correctly:

static function reScaleImage($filename, $conx, $cony){
	if(empty($conx) || empty($cony))
		return;
	list($new_width, $new_height) = self::constrainImage($filename, $conx, $cony);
	return self::resizeDiscardAspect($filename, $new_width, $new_height);
}

Resize and discard aspect, that sounds a little bit crude? Sure but we keep it through constrainImage():

static function constrainImage($filename, $conx, $cony){
	list($orig_width, $orig_height, $type) = getimagesize($filename);
	
	$new_width	=	0;
	$new_height	=	0;
	
	// for instance 0.66 = 125 / 188
	$con_ratio 		= $conx / $cony;
	
	// for instance  1.33 =  300 / 225
	$orig_ratio		= $orig_width / $orig_height;
	
	//if the new picture is laying and the original is standing or laying
	//"less", the original height has to lead
	if($con_ratio > $orig_ratio){
		$new_height = $cony;
		$new_width 	= round(( $cony * $orig_width) / $orig_height);
	}else if($con_ratio < $orig_ratio){
		$new_height = round(( $conx * $orig_height) / $orig_width);
		$new_width 	= $conx;
	}
	
	return array($new_width, $new_height);
}

And finally the resize function which is just a copy of the GD2 section in the PHP manual:

static function resizeDiscardAspect($fileName, $new_width, $new_height){
	//we retrieve the info from the current image
	list($orig_width, $orig_height, $type) = getimagesize($fileName);
	//we create a new image template
	$image_p = imagecreatetruecolor($new_width, $new_height);
	//we create a variable that will hold the new image
	$image = null;
	//only the three first of all the possible formats are supported, the original image is loaded if it is one of them
	switch($type){
		case 1: //GIF
			$image = imagecreatefromgif($fileName);
			break;
		case 2: //JPEG
			$image = imagecreatefromjpeg($fileName);
			break;
		case 3: //PNG
			$image = imagecreatefrompng($fileName);
			break;
		default:
			return false;
			break;
	}
	//we copy the resized image from the original into the new one and save the result as a jpeg   
	imagecopyresampled($image_p, $image, 0, 0, 0, 0, $new_width, $new_height, $orig_width, $orig_height);
	imagejpeg($image_p, $fileName, 100);
	return true;
}

SaveUpdate() shares some logic with save() nowadays:

function validationError($handler, &$post, &$result){
	$this->smarty->assign('prepop', $post);
	$this->smarty->assign('val_error', $result);
	return $this->$handler();
}

That’s the end of this update. In the next part we take a look at the gallery component, there is some really exiting stuff there, multiple uploads with a Shockwave generated with the Flex SDK for instance. Some neat jQuery stuff too.

To part 6 >>

These icons link to social bookmarking sites where readers can share and discover new web pages.
  • Digg
  • del.icio.us
  • blogmarks
  • Reddit
  • Simpy
  • StumbleUpon
  • Technorati
  • DZone
  • Ma.gnolia

Related Posts

Tags: , , ,

5 Responses to “Writing a CMS/Community with Smarty and the Zend Framework: Part 5”

  1. developercast.com » ProDevTips.com: Writing a CMS/Community with Smarty and the Zend Framework: Part 5 Says:

    […] ProDevTips.com website has posted part five of their look at creating a cms/community website by combining Smarty and the Zend Framework. In […]

  2. Daniel Says:

    Just a suggestion - it would greatly improve the look and readability of the site/guides if you could use syntax highlighting on the php code you show.
    Maybe you could use this: http://www.php.net/manual/en/function.highlight-file.php

    Thank you.

  3. Henrik Says:

    Daniel: Take a look at my wordpress code separation and highlighting article, maybe you have some input for me?

  4. Neil Says:

    with that image resize function. It doesn’t preserve transparent gifs. it replaces the transparency with black. :( if you find a way to preserve transparency mail me please.

  5. Henrik Says:

    Neil: If I ever needed to keep the transparency of GIFs and there was no way that it could be done with GD2, then my next option that I would check into would be Imagemagick. Have you checked that one already?

Leave a Reply