Hector Wrap-up

Just to circle back around and tidy things up nicely, I wanted to add a final post to this tutorial, containing a very brief summary of what was covered in each installment, as well as a link to each (from a single place). In conjunction with publishing this post, I have also added, to the bottom of each post in the series, a sequential list of the posts contained in this tutorial.

As a final note, I’d like to offer sincere thanks to my favorite blogger, epicdude312, for providing feedback on every single one of the posts in the Hector tutorial. I encourage you to check out what he’s up to on his blog.

So, without further ado, here’s where we’ve been:

In the very first post, we went over the software required for building our application, as well as the steps required to get Kohana configured in advance of actually beginning to construct our application.

In the second post, we built our initial database schema to store user account data.

In the third post, we defined a couple of template classes to serve as the basis for all of the pages in our application.

In the fourth post, we managed to get a sign in page rendered.

In the fifth post, we added code to process a new user creation request.

In the sixth post, we handled processing the account creation confirmation URL, when the user clicks it.

In the seventh post, we added the code required to handle signing in an existing user.

In the eighth post, we handled password reset request generation.

In the ninth post, we handled password reset processing.

In the tenth post, we introduced the scope of Hector’s functionality and fleshed out the database schema with application-specific tables.

In the eleventh post, we implemented the ability to create a new collection.

In the twelfth post, we added collection edit functionality.

In the thirteenth post, we implemented the ability to add items to a collection.

In the fourteenth post, we added item edit functionality.

In the fifteenth post, we added item delete (and undo).

In the sixteenth post, we added collection delete (and undo) as well as physical deletion of collections (and their items), when flagged for delete.

And that does it. Again, I’d love to hear about how you’re using Kohana to build things!

00

Clean Up The Mess, Boys!

OK, so last time we added the ability to delete individual items, but what about an entire collection? Let’s get that in place now!

Just as we did in our last installment, we need to revisit a couple of existing files to supplement them.

1) First up is the Edit Collection view. Open it (“/opt/lampp/htdocs/hector/application/views/Edit_Collection.php”) and add the highlighted code:

				<fieldset>
<?php
	echo '<legend>'.$page_description.'</legend>';
	echo Form::open('Collection/update/'.$collection_arr['collection_id'], array('class' => 'custom'));
?>	
					<div class="row">
						<div class="large-4 columns end">
<?php
	echo Form::label('selCollectionType', 'Type:');
	echo Form::select('selCollectionType', $collection_type_sel_arr, $post ? $post['selCollectionType'] : $collection_arr['type_id']);
?>	
						</div>
					</div>
					<div class="row">
						<div class="large-8 columns end">
<?php	
	echo Form::label('txtCollectionName', 'Name:');
	echo Form::input('txtCollectionName', $post ? $post['txtCollectionName'] : $collection_arr['name'], array('id' => 'id_txtCollectionName', 'size' => '35', 'maxlength' => '75', 'required' => true));
	if (array_key_exists('txtCollectionName', $errors))
	{
		echo '<small class="error">'.$errors['txtCollectionName'].'</small>';
	}
?>
						</div>
					</div>
					<br />
					<div class="row">
						<div class="small-6 medium-3 large-2 columns">
<?php
	echo Form::button('btnUpdate', 'Update', array('class' => 'small button radius submit'));
?>
						</div>
						<div class="small-6 medium-9 large-10 columns">
<?php
	echo Form::button('btnDelete', 'Delete', array('class' => 'small button radius secondary submit'));
?>
						</div>						
					</div>
<?php	
	echo Form::close();
	echo HTML::anchor('Collection/view', '<< Back to Collections');
?>
				</fieldset>

As you can see, this adds a “Delete” button to the form. The update action in the Collection controller is still handling form submission, for both the “Update” and the “Delete” buttons.

2) So let’s supplement the Collection controller (“/opt/lampp/htdocs/hector/application/classes/Controller/Collection.php”), to handle deletes, right now:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Collection extends Controller_Template_Sitepage {
.
.
.

	public function action_update()
	{
			  
		/*
		/	Purpose: Action to handle "update collection" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/

		// Grab selected collection ID from URL
		$collection_id = $this->request->param('collection');

		// Grab $_POST array from submitted form
		$post = $this->request->post();
		
		if (isset($post['btnUpdate']))
		{	
				  
			// Update button was pressed
				  
			$collection_updated_arr = Model::factory('Collection')->update_collection($post, $collection_id);
			if ($collection_updated_arr['Success'])
			{
					
				// Send message indicating successful collection update
				Session::instance()->set('message', "You've updated collection \"".$collection_updated_arr['Clean_Post_Data']['txtCollectionName']."\"!");				
				
				// Redirect to edit action 
				$this->redirect('Collection/edit/'.$collection_id);	
			} // if ($collection_updated_arr['Success'])
			else
			{
					  
				// Validation failed, display form with custom error text				
				$accordions_open = array(
					'add_item' => FALSE
				);
				$posts = array(
					'add_item' => NULL
					,'edit_collection' => $post
				);
				$this->template->content = ViewBuilder::factory('Page')->edit_collection($accordions_open, $collection_id, $this->template, $posts, $collection_updated_arr['Errors']);
			}
		} // if (isset($post['btnUpdate']))
		elseif (isset($post['btnDelete']))
		{
      		  
			// Delete button was pressed
			
			$collection_deleted_arr = Model::factory('Collection')->delete_collection($collection_id, $to_delete = 1);
			if ($collection_deleted_arr['Success']) 
			{
      			  
				// Send message indicating successful collection delete, giving option to Undo
				Session::instance()->set('warning', "You've deleted collection \"" 
					.$post['txtCollectionName']."\" (and all of its items)! " 
					.HTML::anchor('Collection/undo_delete/'.$collection_id, 'Click here to Undo.')
				);      			  
			} // if ($collection_deleted_arr['Success'])
			// else NOOP - This did not delete anything because of a bad URL
			
			// Redirect to view action
			$this->redirect('Collection/view');					  
		} // elseif (isset($post['btnDelete']))		
		else
		{
      		  
			// Bad URL (i.e. someone went to /Collection/update manually)

			// Redirect to view action
			$this->redirect('Collection/view');
		}
		
	}	

.
.
.
} // End Collection

3) Following our well-established pattern for Controller actions, this calls the Collection model – specifically the delete_collection() function. Add that to the “Business logic functions” section of the model now (“/opt/lampp/htdocs/hector/application/classes/Model/Collection.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Collection extends Model_Database 
.
.
.

	// Business logic functions

	public function delete_collection($collection_id, $to_delete)
	{

		/*
		/	Purpose: Delete/undelete specific collection for current user
		/
		/	Parms:
		/		'collection_id' >> Collection ID to be deleted/undeleted
		/		'to_delete' >> Flag indicating whether to delete or undelete collection
		/
		/	Returns:
		/		Array containing:
		/			'Success' >> Boolean indicating success/failure of collection delete/undelete
		*/

		$return_arr = array(
			'Success' => FALSE		  
		);
		
		// Read one collection's attributes and items
		$collection_data = array(
			'user_id' => Session::instance()->get('user_id')
			,'collection_id' => $collection_id
			,'to_delete' => $to_delete
			,'last_update_dttm' => date('Y-m-d H:i:s')
		);
		$coll_items_arr = $this->read_one_colls_items($collection_data);
		if ($coll_items_arr['Success'])
		{

			// Found collection AND its items (if any) AND they belong to current user
			
			// Set parm for use by Undo call prior to update to collection
			$collection_data['prev_update_dttm'] = $coll_items_arr['Collection']['last_update_dttm'];
			
			$coll_and_items_deleted_arr = $this->update_to_delete($collection_data);
			if ($coll_and_items_deleted_arr['Rows_Affected'] == 1)
			{
					
				// Delete or undelete succeeded!

				// All is well!
				$return_arr['Success'] = TRUE;
			}	  
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Collection

This, in turn, calls the previously-defined read_one_colls_items() function to find the specified collection (and its items). Once these are in hand, it calls the update_to_delete() SQL function.

4) Let’s add the new update_to_delete() function to the “SQL functions” section of the (Collection) model now:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Collection extends Model_Database {
.
.
.

	// SQL functions

	public function update_to_delete($data)
	{
   
		/*
		/	Purpose: Flags (or unflags) specified collection, for deletion
		/
		/	Parms:
		/		Array containing:
		/			'collection_id' >> ID of collection to delete/undelete
		/			'to_delete' >> Flag indicating whether to delete or undelete collection
		/			'last_update_dttm' >> Collection's last update DATETIME
		/
		/	Returns:
		/		Array containing:
		/			'Rows_Affected' >> Number of collection rows affected by UPDATE
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/
 
		$return_arr = array();
   
		// Need to handle delete/undelete of collection and its associated items as an
		//		atomic operation
		$db = Database::instance('default');
		$db->begin();
 
		try 
		{
 			
			// "Delete"/Undelete collection row
			$return_arr['Rows_Affected'] = 
				DB::update($this->table_name)
					->set(array(
						'to_delete' => $data['to_delete']
						,'last_update_dttm' => $data['last_update_dttm'] 
					))
					->where('collection_id', '=', $data['collection_id'])
					->execute()
			;
			
			// "Delete"/Undelete ITEM row(s) (if any) belonging to this collection
			$query = 
				DB::update('item')
					->set(array(
						'to_delete' => $data['to_delete']
						,'last_update_dttm' => $data['last_update_dttm']
					))
					->where('collection_id', '=', $data['collection_id'])
			;
		
			if ($data['to_delete'] == 1)
			{
				  
				// This is setting a delete, so last_update_dttm needs to be set to match what was 
				//		used for the collection to allow for potential Undo
				$query
					->and_where('to_delete', '=', 0)
				;
			}
			else
			{
				  
				// This is unsetting a delete, so the last_update_dttm matching collection row needs
				//		to be in the WHERE clause to ONLY unset those rows deleted with collection (as
				//		opposed to those that might have been flagged for earlier deletion)
				$query
					->and_where('last_update_dttm', '=', $data['prev_update_dttm'])
					->and_where('to_delete', '=', 1)
				;
			}
			
			// Attempt ITEM "Delete"/Undelete 
			$query
				->execute();
			
			// Updates were successful, commit the changes
			$db->commit();
		}
		catch (Database_Exception $e)
		{
      		  
			// Update failed, roll back changes
			$db->rollback();
			
			// Generate system email with appropriate data to track down/recreate error,
			//		redirect user to sign in page, and display error message
			$error_data = array(
				'problem_descr' => 'Collection->update_to_delete()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Collection

Here we see something new – a database transaction to handle the soft-delete (or undelete, as the case may be) of both the collection and its items in a single, atomic action. If either of the SQL actions fails, the entire transaction rolls back. This is only possible because of the fact that we defined these tables (collection and item) as InnoDB.

Well, now you’ve done it – you can delete a collection and its items.

5) Try it out and see if you believe me – edit a collection and then press the “Delete” button:

Hector_Delete_Collection_16_1

Hector_Collection_Deleted_16_2

Of course, the only problem is that you might have made a mistake and not really wanted to delete that. Since this, like the item deletion before it, is a soft-delete, we provide the user the ability to undo the delete.

Leave this page up, but DON’T click that “Undo” link yet, unless you want to check out the resultant error page! 🙂

The link’s URL routes program flow to the undo_delete action in the Collection controller, passing the collection id with it:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Collection extends Controller_Template_Sitepage {
.
.
.

	public function action_update()
	{
			  
		/*
		/	Purpose: Action to handle "update collection" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/

		// Grab selected collection ID from URL
		$collection_id = $this->request->param('collection');

		// Grab $_POST array from submitted form
		$post = $this->request->post();
		
		if (isset($post['btnUpdate']))
		{	
				  
			// Update button was pressed
				  
			$collection_updated_arr = Model::factory('Collection')->update_collection($post, $collection_id);
			if ($collection_updated_arr['Success'])
			{
					
				// Send message indicating successful collection update
				Session::instance()->set('message', "You've updated collection \"".$collection_updated_arr['Clean_Post_Data']['txtCollectionName']."\"!");				
				
				// Redirect to edit action 
				$this->redirect('Collection/edit/'.$collection_id);	
			} // if ($collection_updated_arr['Success'])
			else
			{
					  
				// Validation failed, display form with custom error text				
				$accordions_open = array(
					'add_item' => FALSE
				);
				$posts = array(
					'add_item' => NULL
					,'edit_collection' => $post
				);
				$this->template->content = ViewBuilder::factory('Page')->edit_collection($accordions_open, $collection_id, $this->template, $posts, $collection_updated_arr['Errors']);
			}
		} // if (isset($post['btnUpdate']))
		elseif (isset($post['btnDelete']))
		{
      		  
			// Delete button was pressed
			
			$collection_deleted_arr = Model::factory('Collection')->delete_collection($collection_id, $to_delete = 1);
			if ($collection_deleted_arr['Success']) 
			{
      			  
				// Send message indicating successful collection delete, giving option to Undo
				Session::instance()->set('warning', "You've deleted collection \"" 
					.$post['txtCollectionName']."\" (and all of its items)! " 
					.HTML::anchor('Collection/undo_delete/'.$collection_id, 'Click here to Undo.')
				);      			  
			} // if ($collection_deleted_arr['Success'])
			// else NOOP - This did not delete anything because of a bad URL
			
			// Redirect to view action
			$this->redirect('Collection/view');					  
		} // elseif (isset($post['btnDelete']))		
		else
		{
      		  
			// Bad URL (i.e. someone went to /Collection/update manually)

			// Redirect to view action
			$this->redirect('Collection/view');
		}
		
	}	

.
.
.
} // End Collection

6) Let’s add the code so it will do what it says it will. Open up the Collection controller (“/opt/lampp/htdocs/hector/application/classes/Controller/Collection.php”) and add the undo_delete action to it:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Collection extends Controller_Template_Sitepage {
.
.
.

	public function action_undo_delete()
	{
		
		/*
		/	Purpose: Action to handle "undo delete collection" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/

		// Undo link was clicked

		// Grab selected collection ID from URL
		$collection_id = $this->request->param('collection');

		// "Undelete" collection (and its items)			
		$collection_undeleted_arr = Model::factory('Collection')->delete_collection($collection_id, $to_delete = 0);
		if ($collection_undeleted_arr['Success']) 
		{
			
			// Send message indicating successful collection UNdelete
			Session::instance()->set('message', "You've undone collection delete.");				  
		}
		// else NOOP - This did not undelete anything because of a bad URL
				
		// Redirect to view action
		$this->redirect('Collection/view');
		
	}

.
.
.
} // End Collection

Just as we did with the undo item delete, the undo collection delete also relies on reusable, semi-generic functions we’ve previously defined to handle flipping the “to_delete” database column value back to 1 so that the previously deleted collection and its items are once again visible to the user.

7) As a cleanup step, let’s tighten up our routing so that only the Item controller actions we’ve defined will match our “collection_and_seq_in_url” routing. Open up “bootstrap.php” and add this single line:

.
.
.

/**
 * Set the routes. Each route must have a minimum of a name, a URI and a set of
 * defaults for the URI.
 */
Route::set('validate', '<controller>/<action>/<email_addr>/<authcode>'
		  ,array(
		  	'controller' => 'Validate'
		  	,'email_addr' => '[^/,;?\n]++'
		  )
	);

Route::set('collection_and_seq_in_url', '<controller>/<action>/<collection>/<seq>'
		  ,array(
		  	'controller' => 'Item'
		  	, 'action' => '(edit|undo_delete|update)'
		  )
	);  	 

Route::set('collection_in_url', '<controller>/<action>/<collection>'
		  ,array(
		  	'controller' => '(Collection|Item)'
		  )
	);  	 

Route::set('default', '(<controller>(/<action>(/<junk>)))', array('junk' => '.*'))
	->defaults(array(
		'controller' => 'Main',
		'action'     => 'index',
	));

8) We’re all set now – revisit the page you ended up on in step #5 (above) and click the “Click here to Undo.” link1. The collection should magically re-appear:

Hector_Collection_Delete_Undone_16_3

9) Click on the collection and you’ll see the items are back too:

Hector_Collection_Items_Undeleted_16_4

10) For your next stunt, pick a collection with multiple items. If you want to use the same one you just used in step #5 (above), that is fine. Delete an item from that collection and do NOT undo that delete:

Hector_Item_Deleted_16_5

11) Confirm that the item is no longer visible (as it’s been soft-deleted):

Hector_Deleted_Item_Not_Visible_16_6

12) Next, delete the collection it belongs to. It should disappear from your list of existing collections:

Hector_Delete_Collection_16_7

Hector_Collection_Deleted_16_8

13) Click on the undo collection delete link and it should reappear in your list of collections:

Hector_Collection_Delete_Undone_16_9

14) Finally, drill back down into the collection:

Hector_Collection_Items_After_Delete_Undone_16_10

Did you notice that only the items that were soft-deleted as part of the collection delete have been restored? The item you individually deleted remains deleted. Kinda cool, eh?!

If you want to quit now, that is your prerogative. But, we’re still missing one thing. If we go ahead and use the application as is, the delete functionality will appear, from the front-end, to be working fine. However, as the deletes we’ve been performing thus far are only soft-deletes, we are leaving our database tables littered with rows pending deletion.

Two obvious approaches to performing physical deletion are:

A) Have a Cron job scheduled to run at some interval (e.g., nightly) to physically delete the previous day’s soft-deleted items
B) Have a user’s previously soft-deleted items physically deleted the next time they sign into the system

We’ll go with the latter, in our case, but it would be a good exercise for you to figure out on your own how to implement the first approach!

Because of the way we’ve structured the collection and item tables, if we want to delete ONLY the current user’s flagged collections and items, we need to get a list of the collections and then use each collection_id value in SQL DELETE statements against both tables. Rather than writing a brand-new function to do this, we can re-purpose an existing one, making only minor changes.

15) Open up the Collection model (“/opt/lampp/htdocs/hector/application/classes/Model/Collection.php”) and add/revise the highlighted lines, below, in the read_one_user() function:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Collection extends Model_Database {
.
.
.

	public function read_one_user($data)
	{			  
			  
		/*
		/	Purpose: Read all Collections for one (specified) user
		/
		/	Parms:
		/		Array containing:
		/			'user_id' >> User ID to return Collections for
		/			'to_delete' >> Flag indicating whether Collection is flagged for deletion
		/
		/	Returns:
		/		Array containing:
		/			'Rows' >> Array containing specified user's Collection rows
		/		AND
		/			'Rows_Affected' >> Number of Collection rows in 'Rows'
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/

		$return_arr = array();
		
		try 
		{		
			$return_arr['Rows'] = 
				DB::select(
					'collection_id'
					,array($this->table_name . '.descr', 'name')
					,array('collection_type.descr', 'type')
				)
					->from($this->table_name)
					->join('collection_type')
						->on($this->table_name . '.coll_type_id', '=', 'collection_type.coll_type_id')
					->where('user_id', '=', $data['user_id'])
					->and_where('to_delete', '=', $data['to_delete'])
					->execute()
			;	
			$return_arr['Rows_Affected'] = count($return_arr['Rows']);
		}
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error,
			//		redirect user to sign in page, and display error message
			$error_data = array(
				'problem_descr' => 'Collection->read_one_user()'
					. '<br />' . Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}	
		return ($return_arr);
	}

.
.
.

16) Because this function was already defined, we can assume it is being used somewhere in our application. A quick search yields only a single place it is currently employed – the view_collections() function in the ViewBuilder_Page class (“/opt/lampp/htdocs/hector/application/classes/ViewBuilder/Page.php”). Add the single line highlighted below, and we should be all set:

<?php defined('SYSPATH') or die('No direct script access.');

class ViewBuilder_Page {
.
.
.

	public static function view_collections($accordions_open, $template, $post, $errors)
	{

		/*
		/	Purpose: Build View Collections page
		/
		/	Parms:
		/		'accordions_open' >> Array, indexed by view, indicating whether that view's accordion should default to open
		/		'template' >> Array containing page header attributes
		/		'post' >> Submitted form data or NULL
		/		'errors' >> Form validation errors or NULL
		/
		/	Returns:
		/		Page to render
		*/			
			
		// Read all collection types
		$collection_type_sel_arr = Model::factory('CollectionType')->read_all_collection_types();

		// Read user's collections
		$collection_data = array(
			'user_id' => Session::instance()->get('user_id')
			,'to_delete' => 0
		);
		$collection_arr = Model::factory('Collection')->read_one_user($collection_data);			
		
		// Render Collections page
		$template->page_description = 'Collections';
		$template->title .= $template->page_description;
		$template->navbar = View::factory('Navbar');
			  
		$collection_view = ViewBuilder::factory('CollectionView');
		
		// Add Collection view
		$content = $collection_view->add_collection($accordions_open['add_collection'], $collection_type_sel_arr, $post, $errors);
		
		// Collections view
		$content .= $collection_view->collections($accordions_open['collections'], $collection_arr);
		
		return $content;
		
	}	

.
.
.
} // End ViewBuilder_Page

17) Now let’s return to the AppUser model (“/opt/lampp/htdocs/hector/application/classes/Model/AppUser.php”) and add code to physically delete a user’s pending deletes (collections and items) each time they successfully sign in2:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_AppUser extends Model_Database {
.
.
.

	public function sign_user_in($post)
	{

		/*
		/	Purpose: Attempt to sign in user
		/
		/	Parms:
		/		'post' >> Submitted form data from Sign In view
		/
		/	Returns:
		/		Array containing:
		/			'Clean_Post_Data' >> Array containing cleaned form data
		/			'Success' >> Boolean indicating success/failure of user sign in
		/		[AND]
		/			'Errors' >> Array containing form validation errors
		*/
			  
		$return_arr = array(
			'Success' => FALSE		  
		);
			  
		// Validate Forgot Password form entry
		$validation_results_arr = Validation_AppUser::sign_in($post);
		if ($validation_results_arr['Success'])
		{
				  			
			// Validation clean, sign in user
			$app_user_data = array(
				'username_or_email_addr' => $validation_results_arr['Clean_Post_Data']['txtUsernameOrEmailAddr']  
			);
			
			// Read user's data row
			$app_user_rowset = $this->read_user_data_flexible($app_user_data);
			if ($app_user_rowset['Rows_Affected'] == 1)
			{
				
				// Isolate row of data
				$app_user_arr = $app_user_rowset['Rows'][0];

				// Set session vars
				$session = Session::instance();			
				$session->set('username', $app_user_arr['username']);			
				$session->set('user_id', $app_user_arr['user_id']);	
				$session->set('todays_date', date('Y-m-d'));
				$session->set('sel_date', date('Y-m-d'));
			
				// Update last login AND invalidate password reset request (if exists)
				$app_user_data['user_id'] = $session->get('user_id');
				$last_signin_updated = $this->update_user_last_signin_dttm($app_user_data);
				     	
				// Physically delete user's collections and items pending delete
				$collection_model = Model::factory('Collection');
				$delete_data = array(
					'user_id' => $session->get('user_id')
					,'to_delete' => 1
				);
				$users_collections_to_delete_arr = $collection_model->read_one_user($delete_data);				

				// Iterate through user's collections that are flagged for deletion and physically delete them
				foreach ($users_collections_to_delete_arr['Rows'] as $collection_to_delete)
				{					
					$delete_data['collection_id'] = $collection_to_delete['collection_id'];
					$collection_and_item_delete = $collection_model->delete_collection_and_items($delete_data);
				}
				
				// All is well!
				$return_arr['Success'] = TRUE;
			}
		}
		else
		{
			$return_arr['Errors'] = $validation_results_arr['Errors'];
		}
		
		return $return_arr;
		
	}	
	
.
.
.
} // End Model_AppUser	

18) Finally, let’s add the new delete_collection_and_items() function to the “SQL functions” section of the Collection model (“/opt/lampp/htdocs/hector/application/classes/Model/Collection.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Collection extends Model_Database {
.
.
.

	// SQL functions

	public function delete_collection_and_items($data)
	{
		
		/*
		/	Purpose: Delete all collections, flagged for deletion, for one (specified) user
		/
		/	Parms:
		/		Array containing:
		/			'user_id' >> User ID to delete flagged collections for
		/			'collection_id' >> ID of collection to delete
		/
		/	Returns:
		/		Array containing:
		/			'Rows_Affected' >> Number of collection rows deleted
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/
		
		$return_arr = array();
   
		// Need to handle physical delete of collection and its associated items as an
		//		atomic operation
		$db = Database::instance('default');
		$db->begin();
 
		try 
		{
			// Delete collection row
			$return_arr['Rows_Affected'] = 
				DB::delete($this->table_name)
					->where('user_id', '=', $data['user_id'])
					->and_where('collection_id', '=', $data['collection_id'])
					->and_where('to_delete', '=', 1)
					->execute()
			;
			
			// Delete item row
			$dummy = 
				DB::delete('item')
					->where('collection_id', '=', $data['collection_id'])
					->and_where('to_delete', '=', 1)
					->execute()
			;		
			
 			// Deletes were successful, commit the changes
 			$db->commit();
			
		}
		catch (Database_Exception $e)
		{
		
			// Delete failed, roll back changes
			$db->rollback();

			// Generate system email with appropriate data to track down/recreate error
			$error_data = array(
				'problem_descr' => 'Collection->delete_collection_and_items()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;		
		
	}

.
.
.
} // End Model_Collection

Once again, we see a SQL transaction being used to ensure the deletes from both the collection and item tables are handled in a single, atomic action.

Though it will be transparent to the end-user, if you want to see the behind-the-scenes action your latest handiwork performs, proceed with the next four steps.

19) Add a collection and add items to it:

Hector_Dummy_Collection_Added_16_11

Hector_Items_Added_To_Dummy_Collection_16_12

20) Delete the collection and sign out of Hector:

Hector_Dummy_Collection_Deleted_16_13

Hector_Signed_Out_16_14

21) Query the database, noticing the presence of collection and item rows, flagged for deletion (to_delete = 1), in their respective DB tables:

Hector_DB_Collection_Flagged_For_Delete_16_15

Hector_DB_Items_Flagged_For_Delete_16_16

22) Now sign back in and re-query the database – those previously flagged rows are completely gone:

Hector_Dummy_Collection_Really_Deleted_16_17

Hector_DB_Collection_Really_Deleted_16_18

Hector_DB_Items_Really_Deleted_16_19

Well well, it seems you’ve made it to the very end of the tutorial; congratulations on your perseverance! I sincerely appreciate the time you’ve taken to follow along and, most importantly, I hope you’ve learned something that you can apply to your next project.

Certainly if you notice a bug or take wholesale issue with my approach to anything PLEASE PLEASE reach out and let me know. I am always trying to learn something new myself and improve my skills every day so I am thankful for any feedback you’re willing to provide. And do leave a comment or send me an email with a link to what you’re working on, as I’d love to check it out!

Have fun coding – that’s what it’s about, after all!

Notes:

  1. Alternatively, if you no longer have the page still up, delete another collection and then hit the undo link that is displayed in the message block.
  2. If they haven’t undone erroneous deletes during the same session they’re out of luck.
11

That Item Is Not Needed, Let’s Delete It

OK, let’s step back and recap what we’ve built to this point. First, we constructed a relatively generic user module suitable for just about any web application. Next, we built Hector, utilizing the user module and adding specific functionality to create and maintain collection definitions. And, finally, we added the ability to populate collections with items (which can also be edited/updated/revised). But what if you sell an item in one of your collections (or even sell an entire collection) and no longer want to track it in the system? Or, more likely for someone like me, who tends to type faster than he should, what if you make a mistake and want to delete something you’ve created erroneously?

Adding the ability to delete items and collections seems like a fair solution to that sort of predicament, so let’s do that!

1) The first order of business is adding a means for the user to indicate they want to delete an item. Though there are certainly several ways to add this to the UI, I think a simple one is to add a “Delete” button adjacent to the “Update” button in the Edit Item fieldset, so let’s open the Edit Item view (“/opt/lampp/htdocs/hector/application/views/Edit_Item.php”) and do just that:

				<br />
				<div class="row">
					<div class="small-5 medium-2 columns">
<?php
	echo '&nbsp;&nbsp;&nbsp;&nbsp;<strong>Collection:</strong> ';
?>
					</div>
					<div class="small-7 medium-10 columns">
<?php
	echo $coll_and_item_arr['Collection']['name']
		.' ('.$coll_and_item_arr['Collection']['type'].')'
	;
?>
					</div>
				</div>
				<fieldset>
<?php
	echo '<legend>'.$page_description.'</legend>';
	echo Form::open('Item/update/'.$collection_id.'/'.$seq, array('class' => 'custom'));
?>	
					<div class="row">
						<div class="large-2 columns end">
<?php
	echo Form::label('txtItemSeq', 'Sequence:');
	echo Form::input('txtItemSeq', $post ? $post['txtItemSeq']: $coll_and_item_arr['Item']['seq'], array('id' => 'id_txtItemSeq', 'size' => '35', 'maxlength' => '3', 'required' => true));
	if (array_key_exists('txtItemSeq', $errors))
	{
		echo '<small class="error">'.$errors['txtItemSeq'].'</small>';
	}
?>	
						</div>
					</div>
					<div class="row">
						<div class="large-8 columns end">
<?php	
	echo Form::label('txtItemName', 'Name:');
	echo Form::input('txtItemName', $post ? $post['txtItemName']: $coll_and_item_arr['Item']['descr'], array('id' => 'id_txtItemName', 'size' => '35', 'maxlength' => '75', 'required' => true));
	if (array_key_exists('txtItemName', $errors))
	{
		echo '<small class="error">'.$errors['txtItemName'].'</small>';
	}
?>	
						</div>
					</div>
					<div class="row">
						<div class="large-2 columns end">
<?php	
	echo Form::label('selItemStatus', 'Status:');
	echo Form::select('selItemStatus', $item_status_sel_arr, $post ? $post['selItemStatus'] : $coll_and_item_arr['Item']['status']);
?>
						</div>
					</div>
					<br />
					<div class="row">
						<div class="small-6 medium-3 large-2 columns">
<?php
	echo Form::button('btnUpdate', 'Update', array('class' => 'small button radius submit'));
?>
						</div>
						<div class="small-6 medium-9 large-10 columns">
<?php
	echo Form::button('btnDelete', 'Delete', array('class' => 'small button radius secondary submit'));
?>
						</div>						
					</div>
<?php	
	echo Form::close();
	echo HTML::anchor('Collection/edit/'.$collection_id, '<< Back to Items');
?>
				</fieldset>

You’ll notice that we haven’t changed the form action, which still routes this form to the update action in the Item controller. What this means is that we’ll need to amend the update action so it is able to discern which button submitted the form.

2) Open the Item controller (“/opt/lampp/htdocs/hector/application/classes/Controller/Item.php”), revisit the update action, and add in the code to handle the “Delete” button:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Item extends Controller_Template_Sitepage {
.
.
.

	public function action_update()
	{
			  
		/*
		/	Purpose: Action to handle "update item" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/

		// Grab selected collection ID, seq (item ID) from URL
		$collection_id = $this->request->param('collection');
		$seq = $this->request->param('seq');
				  
		// Grab $_POST array from submitted form
		$post = $this->request->post();
		
		if (isset($post['btnUpdate']))
		{
			
			// Update button was pressed
			
			$item_updated_arr = Model::factory('Item')->update_item($post, $collection_id, $seq);
			if ($item_updated_arr['Success'])
			{
					
				// Send message indicating successful item update
				Session::instance()->set('message', "You've updated item \"".$item_updated_arr['Clean_Post_Data']['txtItemName']."\"!");
				
				// Redirect to Collection/edit action 
				$this->redirect('Collection/edit/'.$collection_id);
			}
			else
			{
					  
				// Validation failed
     		
				// Determine if this item exists AND belongs to current user
				$coll_and_item_arr = Model::factory('Item')->edit_item($collection_id, $seq);
				if ($coll_and_item_arr['Success'])
				{

					// Display form with custom error text
					$posts = array(
						'edit_item' => $post
					);
					$this->template->content = ViewBuilder::factory('Page')->edit_item($collection_id, $seq, $this->template, $coll_and_item_arr, $posts, $item_updated_arr['Errors']);				  
				}
				else
				{
     		
					// Redirect to Collection/view action 
					$this->redirect('Collection/view');	
				}      		
			}
		} // if (isset($post['btnUpdate']))
		elseif (isset($post['btnDelete']))
		{
			
			// Delete button was pressed
			$item_deleted_arr = Model::factory('Item')->delete_item($collection_id, $seq, $to_delete = 1);
			if ($item_deleted_arr['Success'])
			{
 				
				// Send message indicating successful item delete, giving option to Undo
				Session::instance()->set('warning', "You've deleted item \"" 
					.$post['txtItemName']."\"! " 
					.HTML::anchor('Item/undo_delete/'.$collection_id.'/'.$seq, 'Click here to Undo.') 
				);     		  
			}
			
			// Redirect to Collection/edit action
			$this->redirect('Collection/edit/'.$collection_id);
		} // elseif (isset($post['btnDelete']))		
		else
		{
      	
			// Bad URL (i.e. someone went to /Item/update manually

			// Redirect to Collection/edit action
			$this->redirect('Collection/edit/'.$collection_id);
		}
		
	}	
	
.
.
.
} // End Item

Though the delete logic looks a lot like the update code, with respect to handing off responsibility to the model and then routing the user accordingly (dependent on the success or failure of the call to the model’s function), there is a new wrinkle here. Unlike creates and updates, which can be effectively undone by way of a subsequent update or (soon 🙂 ) a delete, deletes themselves are usually permanent.

One common approach to ensuring a user REALLY wants to delete something is to offer a confirmation dialog or warning (e.g., “Are you sure you want to delete this?”). I have been convinced that it is better to actually perform a delete and THEN give the user the immediate option to undo the delete (props to Nathan Barry). You could get really fancy with this and have a “Trash” folder or some such (like WordPress does with comments and elsewhere). For simplicity’s sake, though, we’ll just supply an undo link in the message block we generate following a successful delete. That way, if the user accidentally deletes an item they didn’t intend to, they can undo it right then-and-there as their next action.

The mechanism for accomplishing this is via a soft-delete, where we use a 1 in the to_delete database column to indicate that the user has chosen to delete an item. As you may have noticed, if you’ve been scrutinizing the SQL functions as we’ve been defining them, we control visibility of the items that are shown (when viewing the collection they belong to) by making “to_delete = 0” a part of the WHERE clause in the SELECT statement. The point at which we physically delete rows where “to_delete = 1” is a system-design choice we’ll consider later.

Now that we’ve explained the rationale behind our approach, let’s proceed with implementing the actual delete functionality.

3) Open up the Item model (“/opt/lampp/htdocs/hector/application/classes/Model/Item.php”) and add the delete_item() function to the “Business logic functions” section:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Item extends Model_Database {
.
.
.

	// Business logic functions

	public function delete_item($collection_id, $seq, $to_delete)
	{
			  
		/*
		/	Purpose: Delete/undelete item after first confirming collection belongs to current user
		/
		/	Parms:
		/		'collection_id' >> Collection ID from/to which item is to be deleted/undeleted
		/		'seq' >> Item's sequence in collection
		/		'to_delete' >> Flag indicating whether to delete or undelete item
		/
		/	Returns:
		/		Array containing:
		/			'Success' >> Boolean indicating success/failure of item delete/undelete
		*/			  

		$return_arr = array(
			'Success' => FALSE		  
		);

		// Select collection that item is attempting to "delete"/"undelete" from/to (to 
		//		ensure it is a collection owned by the current user)
		$collection_data = array(
			'user_id' => Session::instance()->get('user_id')
			,'collection_id' => $collection_id
		);
		$collection_row_exists = Model::factory('Collection')->read_exists($collection_data);
		if ($collection_row_exists['Rows_Affected'] == 1)
		{
			
			// Perform item "delete"/"undelete"
			$item_data = array(
				'user_id' => Session::instance()->get('user_id')
				,'collection_id' => $collection_id
				,'seq' => $seq
				,'to_delete' => $to_delete
				,'last_update_dttm' => date('Y-m-d H:i:s')
			);
			$item_deleted_arr = $this->update_to_delete($item_data);
			if ($item_deleted_arr['Rows_Affected'] == 1)
			{	

				// All is well!
				$return_arr['Success'] = TRUE;
			}
			// else NOOP - Attempt to delete non-existent item in current user's collection			
		}
		// else NOOP - Bad URL
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Item

We see a call to our old friend, the Collection model’s read_exists() function, as well as something we haven’t yet seen, the Item model’s update_to_delete() function. Note that among the parameters passed, in the $item_data array, to the update_to_delete() function, is $to_delete, which will allow us to re-use this function later on.

4) Go ahead and add the update_to_delete() function to the “SQL functions” section of the Item model now:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Item extends Model_Database {
.
.
.

	// SQL functions

	public function update_to_delete($data)
	{

		/*
		/	Purpose: Flags (or unflags) specified item, in specific collection, for deletion
		/
		/	Parms:
		/		Array containing:
		/			'collection_id' >> ID of collection to delete/undelete from
		/			'seq' >> Item to delete/undelete
		/			'to_delete' >> Flag indicating whether to delete or undelete item
		/			'last_update_dttm' >> Item's last update DATETIME
		/
		/	Returns:
		/		Array containing:
		/			'Rows_Affected' >> Number of item rows affected by UPDATE
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/

		$return_arr = array();
		
		try
		{
			$return_arr['Rows_Affected'] = 
				DB::update($this->table_name)
					->set(array(
						'to_delete' => $data['to_delete']
						,'last_update_dttm' => $data['last_update_dttm'] 
					))
					->where('collection_id', '=', $data['collection_id'])
					->and_where('seq', '=', $data['seq'])
					->execute()
			;
		}
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error
			$error_data = array(
				'problem_descr' => 'Item->update_to_delete()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Item

You’ll notice that this is somewhat generic, in that it can flag items for deletion OR unset the to_delete flag. This will get used to do the latter when we implement the undo item delete functionality.

That’s all that is required to soft-delete an item, which will prevent it from being displayed in a collection’s list of items. Can you point back to where we wrote the code that retrieves only non-deleted items1?

5) What are you waiting for?! Go ahead and try deleting an item now – edit a collection, edit an item, and press the “Delete” button:

Hector_Delete_Item_15_1

Hector_Item_Deleted_15_2

As we saw up in step #2 (above), and what you just saw when you tested out the delete functionality a moment ago, on a successful delete request the user will receive a message block message with a link to undo the delete. That link’s URL routes program flow to the undo_delete action in the Item controller, passing the collection id and item sequence number with it.

Leave this page up, but DON’T click that “Undo” link yet, unless you like seeing error pages! 🙂

6) We’ll add the undo_delete action to the Item controller (“/opt/lampp/htdocs/hector/application/classes/Controller/Item.php”) now:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Item extends Controller_Template_Sitepage {
.
.
.

	public function action_undo_delete()
	{
		
		/*
		/	Purpose: Action to handle "undo delete item" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/
		
		// Undo link was clicked

		// Grab selected collection ID, seq (item ID) from URL
		$collection_id = $this->request->param('collection');
		$seq = $this->request->param('seq');
		
		// "Undelete" item
		$item_undeleted_arr = Model::factory('Item')->delete_item($collection_id, $seq, $to_delete = 0);
		if ($item_undeleted_arr['Success'])
		{
       	
			// Send message indicating successful item UNdelete
			Session::instance()->set('message', "You've undone item delete.");				  
		}
		// else NOOP - This did not undelete anything because of a bad URL
		
		// Redirect to Collection/edit action 
		$this->redirect('Collection/edit/' . $collection_id);
		
	}	

.
.
.
} // End Item

As I mentioned, in step #3 (above), this calls the very same delete_item() function in the Item model that we defined in that very same step. This means the other functions the undo item delete functionality relies upon have already been defined. And best of all, that means we’re done implementing item undo delete!

7) Try it out – revisit the page you ended up on in step #5 (above) and click the “Click here to Undo.” link2. Your list of items in the current collection should once again include the item you initially deleted:

Hector_Item_Delete_Undone_15_3

Now that we have item delete (and undelete) in place, only collection delete remains to be implemented. Why don’t we save that for next time?

Notes:

  1. It was the read_one_collection() function in the Item model, defined in step #7 of the edit collection post.
  2. If you no longer have the page still up, delete another item and then hit the undo link that is displayed in the message block.
00

What?! I Don’t Have That!

Welcome back! This time around we’ll add code to handle editing an item in a collection.

First of all, let’s take a gander at the Items view (“/opt/lampp/htdocs/hector/application/views/Items.php”) that we defined in an earlier post. It displays all of the (non-deleted) items in a collection. The highlighted row shows the controller and action that will be called when a particular item is clicked on, for editing:

<dd class="accordion-navigation">
	<a href="#items">View Items</a>
<?php	
	echo '<div id="items" class="content '.($accordion_group_open == TRUE ? ' active' : '').'">';
?>
		<fieldset>
<?php
	echo '<legend>'.$page_description.'</legend>';
	if (count($items_arr))
	{
		foreach ($items_arr as $item)
		{	
?>
			<div class="row">
				<div class="small-6 columns">
<?php
			echo HTML::anchor('Item/edit/'.$item['collection_id'].'/'.$item['seq'], $item['descr']);			
?>
				</div>
				<div class="small-6 columns">
<?php   					
			echo $item_status_sel_arr[$item['status']];
?>					  
				</div>
			</div>   					
<?php
		}
	}
	else
	{
		echo 'No items found.  Click on "Add Item" above to get started adding to your collection!<br />';	 
	}
?>
		</fieldset>
	</div>
</dd>

Since, by now, we’re old pros at diagnosing what to add next, I’m certain you are shouting at your screen right now that our first order of business is adding the edit action to the Item controller.

Actually, since this is the first time we’ve seen a URL of the form “../[controller]/[action]/[collection_id]/[seq]”, the first thing we need to do is add a route to our bootstrap file.

1) Crack open the bootstrap file (“/opt/lampp/htdocs/hector/application/bootstrap.php”) and add the new routing; remember, sequence is important here as the first matching route will handle the URL (and we want the “collection_and_seq_in_url” rule to be the first to take a shot at it!):

.
.
.

/**
 * Set the routes. Each route must have a minimum of a name, a URI and a set of
 * defaults for the URI.
 */
Route::set('validate', '<controller>/<action>/<email_addr>/<authcode>'
		  ,array(
		  	'controller' => 'Validate'
		  	,'email_addr' => '[^/,;?\n]++'
		  )
	);

Route::set('collection_and_seq_in_url', '<controller>/<action>/<collection>/<seq>'
		  ,array(
		  	'controller' => 'Item'
		  )
	);  	 

Route::set('collection_in_url', '<controller>/<action>/<collection>'
		  ,array(
		  	'controller' => '(Collection|Item)'
		  )
	);  	 

Route::set('default', '(<controller>(/<action>(/<junk>)))', array('junk' => '.*'))
	->defaults(array(
		'controller' => 'Main',
		'action'     => 'index',
	));

2) Now that our application will actually route to our Item controller, go ahead and add the edit action to it (“/opt/lampp/htdocs/hector/application/classes/Controller/Item.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Item extends Controller_Template_Sitepage {
.
.
.

	public function action_edit()
	{

		/*
		/	Purpose: Action to handle "edit item" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/

		// Grab selected collection ID, seq (item ID) from URL
		$collection_id = $this->request->param('collection');
		$seq = $this->request->param('seq');

		// Determine if this item exists AND belongs to current user
		$coll_and_item_arr = Model::factory('Item')->edit_item($collection_id, $seq);
		if ($coll_and_item_arr['Success'])
		{
			
			// Show selected item
			$posts = array(
				'edit_item' => NULL
			);
 			$this->template->content = ViewBuilder::factory('Page')->edit_item($collection_id, $seq, $this->template, $coll_and_item_arr, $posts, $errors = array());				  
		}
		else
		{
     		
			// Redirect to Collection/view action 
			$this->redirect('Collection/view');	
		}
		
	}

.
.
.
} // End Item

The first thing this function does is pull the collection id and seq from the URL and pass them along to the Item model’s new edit_item() function. Everything else in here lines up nicely with the edit action we defined in our Collection controller so there is no need to bog down our march forward with any further explanation.

3) Let’s continue, by adding the edit_item() function to the “Business logic functions” section of the Item model (“/opt/lampp/htdocs/hector/application/classes/Model/Item.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Item extends Model_Database {
.
.
.

	// Business logic functions

	public function edit_item($collection_id, $seq)
	{
			  
		/*
		/	Purpose: Edit specific item after first confirming collection belongs to current user
		/
		/	Parms:
		/		'collection_id' >> Collection ID of item to be edited
		/		'seq' >> Item's sequence in collection
		/
		/	Returns:
		/		Array containing:
		/			'Success' >> Boolean indicating success/failure of collection's (and any 
		/								associated items) existence
		/		[AND]
		/			'Collection' >> Array containing collection's attributes
		/			'Item' >> Array containing item in collection
		*/

		$return_arr = array(
			'Success' => FALSE		  
		);
		
		$coll_and_item_data = array(
			'user_id' => Session::instance()->get('user_id')
			,'collection_id' => $collection_id
			,'seq' => $seq
		);
		$coll_and_item_arr = Model::factory('Collection')->read_one_item($coll_and_item_data);
		if ($coll_and_item_arr['Success'])
		{
			$return_arr = $coll_and_item_arr;	  
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Item

This function reads the one selected item and the collection it belongs to.

4) We’ll accomplish this by adding the read_one_item() function to the “Business logic functions” section of the Collection model (“/opt/lampp/htdocs/hector/application/classes/Model/Collection.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Controller extends Model_Database {
.
.
.

	// Business logic functions

	public function read_one_item($data)
	{
			  
		/*
		/	Purpose: Read one specific collection, including one specific item contained in 
		/					it
		/
		/	Parms:
		/		Array containing:
		/			'user_id' >> User ID to return collection for
		/			'collection_id' >> Collection ID of collection to return
		/			'seq' >> Item ID of item to return
		/
		/	Returns:
		/		Array containing:
		/			'Success' >> Boolean indicating success/failure of collection read
		/		[AND]
		/			'Collection' >> Array containing collection's attributes
		/			'Item' >> Array containing item in collection
		*/

		$return_arr = array(
			'Success' => FALSE		  
		);

		// Read one collection's attributes
		$collection_rowset = $this->read_one($data);
		if ($collection_rowset['Rows_Affected'] == 1)
		{

			// Collection exists AND belongs to current user
				
			// Preserve collection's attributes
			$return_arr['Collection'] = $collection_rowset['Rows'][0];

			// Read collection's ITEM
			$item_rowset = Model::factory('Item')->read_one($data);
			
			// NO DB Exception, if we made it this far
					
			if ($item_rowset['Rows_Affected'] == 1)
			{
				$return_arr['Item'] = $item_rowset['Rows'][0];
				
				// All is well!
				$return_arr['Success'] = TRUE;
			}
			// else NOOP - Nothing to do here as no ITEM row returned
		}
		// else NOOP - Nothing to do here as no COLLECTION row returned
		
		return $return_arr;		
		
	}

.
.
.
} // End Model_Controller

This first utilizes the Collection model’s read_one() function, which we’ve previously defined, to read the specified collection’s attributes.

5) It then calls the read_one() function in the Item model (“/opt/lampp/htdocs/hector/application/classes/Model/Item.php”). Add that now, inside the “SQL functions” section:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Item extends Model_Database {
.
.
.

	// SQL functions

	public function read_one($data)
	{
			  
		/*
		/	Purpose: Read one specific item for specified collection
		/
		/	Parms:
		/		Array containing:
		/			'collection_id' >> Collection ID of collection to return
		/			'seq' >> Seq of item, within collection, to return
		/
		/	Returns:
		/		Array containing:
		/			'Rows' >> Array containing specific item contained in specified collection
		/		AND
		/			'Rows_Affected' >> Number of item rows in 'Rows'
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/

		$return_arr = array();
		
		try
		{
			$return_arr['Rows'] = 
				DB::select()
					->from($this->table_name)
					->where('collection_id', '=', $data['collection_id'])
					->and_where('seq', '=', $data['seq'])
					->execute()
			;
			$return_arr['Rows_Affected'] = count($return_arr['Rows']);
		}
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error
			$error_data = array(
				'problem_descr' => 'Item->read_one()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Item

Returning to the Item controller, now that we have our data, it is time to generate the page to display it.

6) First things first, we call our trusty pal, the ViewBuilder_Page class (“/opt/lampp/htdocs/hector/application/classes/ViewBuilder/Page.php”). We do need to supplement it with the new edit_item() function, however:

<?php defined('SYSPATH') or die('No direct script access.');

class ViewBuilder_Page {
.
.
.

	public function edit_item($collection_id, $seq, $template, $coll_and_item_arr, $posts, $errors)
	{

		/*
		/	Purpose: Build Edit Item page
		/
		/	Parms:
		/		'collection_id' >> Collection ID of item to edit
		/		'seq' >> Item's sequence in collection
		/		'template' >> Array containing page header attributes
		/		'coll_and_item_arr' >> Array selected collection and item data
		/		'posts' >> Array, indexed by view, containing either submitted form data or NULL
		/		'errors' >> Form validation errors or NULL
		/
		/	Returns:
		/		Page to render
		*/			
			  
		// Read all item statuses
		$item_status_sel_arr = Model::factory('ItemStatus')->read_all_item_statuses();  
		
		// Render Edit Item page
		$template->page_description = 'Edit Item';
		$template->title .= $template->page_description;
		$template->navbar = View::factory('Navbar');

		$item_view = ViewBuilder::factory('ItemView');
		
		// Edit Item view
		$content = $item_view->edit_item($collection_id, $seq, $item_status_sel_arr, $coll_and_item_arr, $posts['edit_item'], $errors);

		return $content;
		
	}

.
.
.
} // End ViewBuilder_Page

7) We’ve already defined the item status retrieval code (back in our this prior post) so we’ll proceed on to the ViewBuilder_ItemView class (“/opt/lampp/htdocs/hector/application/classes/ViewBuilder/ItemView.php”), this time to add the edit_item() function:

<?php defined('SYSPATH') or die('No direct script access.');

class ViewBuilder_ItemView {
.
.
.

	public function edit_item($collection_id, $seq, $item_status_sel_arr, $coll_and_item_arr, $post_or_null, $errors)
	{
	
		/*
		/	Purpose: Build Edit Item view
		/
		/	Parms:
		/		'collection_id' >> Collection ID of item to edit
		/		'seq' >> Item's sequence in collection
		/		'item_status_sel_arr' >> Array containing selectable item statuses
		/		'coll_and_item_arr' >> Array containing user's selected collection and specific item
		/		'post_or_null' >> Submitted form data or NULL
		/		'errors' >> Form validation errors or NULL
		/
		/	Returns:
		/		View to render
		*/			
			  
		$content =
		
			// Render message block
			View::factory('Message_Block')
      	
			// Render Edit Item view
			.View::factory('Edit_Item')
				->set('collection_id', $collection_id)
				->set('seq', $seq)
				->set('item_status_sel_arr', $item_status_sel_arr)
				->set('coll_and_item_arr', $coll_and_item_arr)
				->set('page_description', 'Edit Item')
				->set('post', $post_or_null)
				->set('errors', $errors)
		;      
		
		return $content;
		
	}

.
.
.
} // End ViewBuilder_ItemView

8) Finally, we’ll create the brand-new Edit Item view (create it as “Edit_Item.php” and stash it in the “/opt/lampp/htdocs/hector/application/views” directory):

Edit_Item_Views_14_1

Add the following code and markup:

				<br />
				<div class="row">
					<div class="small-5 medium-2 columns">
<?php
	echo '&nbsp;&nbsp;&nbsp;&nbsp;<strong>Collection:</strong> ';
?>
					</div>
					<div class="small-7 medium-10 columns">
<?php
	echo $coll_and_item_arr['Collection']['name']
		.' ('.$coll_and_item_arr['Collection']['type'].')'
	;
?>
					</div>
				</div>
				<fieldset>
<?php
	echo '<legend>'.$page_description.'</legend>';
	echo Form::open('Item/update/'.$collection_id.'/'.$seq, array('class' => 'custom'));
?>	
					<div class="row">
						<div class="large-2 columns end">
<?php
	echo Form::label('txtItemSeq', 'Sequence:');
	echo Form::input('txtItemSeq', $post ? $post['txtItemSeq']: $coll_and_item_arr['Item']['seq'], array('id' => 'id_txtItemSeq', 'size' => '35', 'maxlength' => '3', 'required' => true));
	if (array_key_exists('txtItemSeq', $errors))
	{
		echo '<small class="error">'.$errors['txtItemSeq'].'</small>';
	}
?>	
						</div>
					</div>
					<div class="row">
						<div class="large-8 columns end">
<?php	
	echo Form::label('txtItemName', 'Name:');
	echo Form::input('txtItemName', $post ? $post['txtItemName']: $coll_and_item_arr['Item']['descr'], array('id' => 'id_txtItemName', 'size' => '35', 'maxlength' => '75', 'required' => true));
	if (array_key_exists('txtItemName', $errors))
	{
		echo '<small class="error">'.$errors['txtItemName'].'</small>';
	}
?>	
						</div>
					</div>
					<div class="row">
						<div class="large-2 columns end">
<?php	
	echo Form::label('selItemStatus', 'Status:');
	echo Form::select('selItemStatus', $item_status_sel_arr, $post ? $post['selItemStatus'] : $coll_and_item_arr['Item']['status']);
?>
						</div>
					</div>
					<br />
					<div class="row">
						<div class="small-6 medium-3 large-2 columns">
<?php
	echo Form::button('btnUpdate', 'Update', array('class' => 'small button radius submit'));
?>
						</div>
					</div>
<?php	
	echo Form::close();
	echo HTML::anchor('Collection/edit/'.$collection_id, '<< Back to Items');
?>
				</fieldset>

9) Alrighty then, you should now be able to click on an item and see the Edit Item page rendered, complete with the form fields pre-populated with the selected item’s values!:

Hector_Edit_Item_14_2

Of course, that alone isn’t terribly useful; let’s continue on and add the code to handle updates to the item’s data.

10) In step #8 (above) we see that the form’s action routes the submitted form to the Item controller’s (“/opt/lampp/htdocs/hector/application/classes/Controller/Item.php”) update action, so let’s get that defined here-and-now:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Item extends Controller_Template_Sitepage {
.
.
.

	public function action_update()
	{
			  
		/*
		/	Purpose: Action to handle "update item" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/

		// Grab selected collection ID, seq (item ID) from URL
		$collection_id = $this->request->param('collection');
		$seq = $this->request->param('seq');
				  
		// Grab $_POST array from submitted form
		$post = $this->request->post();
		
		if (isset($post['btnUpdate']))
		{
			
			// Update button was pressed
			
			$item_updated_arr = Model::factory('Item')->update_item($post, $collection_id, $seq);
			if ($item_updated_arr['Success'])
			{
					
				// Send message indicating successful item update
				Session::instance()->set('message', "You've updated item \"".$item_updated_arr['Clean_Post_Data']['txtItemName']."\"!");
				
				// Redirect to Collection/edit action 
				$this->redirect('Collection/edit/'.$collection_id);
			}
			else
			{
					  
				// Validation failed
     		
				// Determine if this item exists AND belongs to current user
				$coll_and_item_arr = Model::factory('Item')->edit_item($collection_id, $seq);
				if ($coll_and_item_arr['Success'])
				{

					// Display form with custom error text
					$posts = array(
						'edit_item' => $post
					);
					$this->template->content = ViewBuilder::factory('Page')->edit_item($collection_id, $seq, $this->template, $coll_and_item_arr, $posts, $item_updated_arr['Errors']);				  
				}
				else
				{
     		
					// Redirect to Collection/view action 
					$this->redirect('Collection/view');	
				}      		
			}
		} // if (isset($post['btnUpdate']))
		else
		{
      	
			// Bad URL (i.e. someone went to /Item/update manually

			// Redirect to Collection/edit action
			$this->redirect('Collection/edit/'.$collection_id);
		}
		
	}

.
.
.
} // End Item

The code for this action looks very similar to the actions we’ve defined before. Yet again we rely on the model (Item, in this instance) to do the heavy-lifting and we direct the process flow according to its return value.

11) The entrance point, in to the model, is the update_item() function. Add it to the “Business logic functions” section of the Item model (“/opt/lampp/htdocs/hector/application/classes/Model/Item.php”) now:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Item extends Model_Database {
.
.
.

	// Business logic functions

	public function update_item($post, $collection_id, $seq)
	{

		/*
		/	Purpose: Update an existing item after first confirming collection belongs to current user
		/
		/	Parms:
		/		'post' >> Submitted form data from Edit Item view
		/		'collection_id' >> Collection ID to which item belongs
		/		'seq' >> Item's current sequence in collection
		/
		/	Returns:
		/		Array containing:
		/			'Clean_Post_Data' >> Array containing cleaned form data
		/			'Success' >> Boolean indicating success/failure of item update
		/		[AND]
		/			'Errors' >> Array containing form validation errors
		*/			

		$return_arr = array(
			'Success' => FALSE		  
		);
		
		// Validate Add Item form entry
		$validation_results_arr = Validation_Item::form_fields($post);
		if ($validation_results_arr['Success'])
		{
			  
			// Validation clean
			
			// Select collection that item is attempting to be added to (to ensure it is a 
			//		collection owned by the current user)
			$collection_data = array(
				'user_id' => Session::instance()->get('user_id')
				,'collection_id' => $collection_id
			);
			$collection_row_exists = Model::factory('Collection')->read_exists($collection_data);
			if ($collection_row_exists['Rows_Affected'] == 1)
			{
				
				// Update existing item							
				$form_item_data = array(
					'user_id' => Session::instance()->get('user_id')
					,'collection_id' => $collection_id
					,'prev_seq' => $seq
					,'seq' => $validation_results_arr['Clean_Post_Data']['txtItemSeq']
					,'descr' => $validation_results_arr['Clean_Post_Data']['txtItemName']
					,'status' => $validation_results_arr['Clean_Post_Data']['selItemStatus']
				);
			
				if ($form_item_data['seq'] == $form_item_data['prev_seq'])
				{
					
					// Seq has NOT changed, perform item update
					$item_updated_arr = $this->update($form_item_data);
					$return_arr['Clean_Post_Data'] = $validation_results_arr['Clean_Post_Data'];

					// All is well!
					$return_arr['Success'] = TRUE; 
				}
				else
				{
				  
					//	Seq HAS changed
				
					// Perform item select
					$item_rowset = $this->read_exists($form_item_data);
					if ($item_rowset['Rows_Affected'] == 0)
					{
					  
						// No existing item with specified seq value
				  
						// Perform item update
						$item_updated_arr = $this->update($form_item_data);
						$return_arr['Clean_Post_Data'] = $validation_results_arr['Clean_Post_Data'];

						// All is well!
						$return_arr['Success'] = TRUE;
					}
					else
					{
				
						// There IS an existing item with specified seq value, need to re-seq				

						$item_arr = $this->read_one_collection($form_item_data);	
						
						$item_seq_arr = array();
						foreach ($item_arr['Rows'] as $item)
						{
							$item_seq_arr[$item['seq']] = $item;				
						}
					
						// Find max consecutive existing seq!
						// Start looking at index immediately above target
						$max_consec_seq = $form_item_data['seq'] + 1;					
						while (array_key_exists($max_consec_seq, $item_seq_arr))
						{
							$max_consec_seq++;	// NOTE: This will end up 1 more than it should be - adjust in for loop below
						}
					
						// Starting at max existing index, go back down to seq user is attempting to insert,
						//		incrementing by 1 each existing index					
						$item_data = array(
							'collection_id' => $form_item_data['collection_id']	  
						);
						for ($i = $max_consec_seq - 1; $i >= $form_item_data['seq']; $i--)
						{
							$item_data['old_seq'] = $i;
							$item_updated_arr = $this->update_incr_seq($item_data);
						} // for ($i = $max_consec_seq - 1; $i >= $data['seq']; $i--)
				
						// Perform item update
						if (($form_item_data['seq'] < $form_item_data['prev_seq']) AND ($max_consec_seq >= $form_item_data['prev_seq']))
						{
					
							// Need to adjust source seq value to account for increment
							$form_item_data['prev_seq']++;
						}
						$final_item_updated_arr = $this->update($form_item_data);
						$return_arr['Clean_Post_Data'] = $validation_results_arr['Clean_Post_Data'];
						
						// All is well!
						$return_arr['Success'] = TRUE;			
					}
				}	
			}
			// else NOOP - Collection doesn't exist or doesn't belong to current user
		}
		else
		{
			$return_arr['Errors'] = $validation_results_arr['Errors'];
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Item

This code, particularly how re-sequencing of items is addressed, should remind you quite a bit of what we wrote to handle creation of new items (the create_item() function, also in the Item model)1.

We’ve already defined all but one of the functions used here, so let’s move on to coding the one lone new function now.

12) That is the update() function; add it to the “SQL functions” section of the (Item) model:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Item extends Model_Database {
.
.
.

	// SQL functions

	public function update($data)
	{
			  
		/*
		/	Purpose: Updates specified item
		/
		/	Parms:
		/		Array containing:
		/			'collection_id' >> ID of collection to update
		/			'seq' >> Item's updated seq
		/			'descr' >> Item's updated description
		/			'status' >> Item's updated status
		/			'prev_seq' >> (Old) seq of item, within collection, to update
		/
		/	Returns:
		/		Array containing:
		/			'Rows_Affected' >> Number of item rows affected by UPDATE
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/

		$return_arr = array();
		
		try
		{
			$return_arr['Rows_Affected'] = 
				DB::update($this->table_name)
					->set(array(
						'seq' => $data['seq']
						,'descr' => $data['descr']
						,'status' => $data['status']
					))
					->where('collection_id', '=', $data['collection_id'])
					->and_where('seq', '=', $data['prev_seq'])
					->execute()
			;
		}
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error
			$error_data = array(
				'problem_descr' => 'Item->update()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Item

And that is actually all we need to add to handle updating items, as the additional functions we see called, by the Item model’s update_item() function, have all been previously defined (we continue to reap the benefits of re-usability 🙂 ).

13) Go ahead and modify an item, pressing the “Update” button to apply your change(s):

Hector_Update_First_Item_14_3

Hector_First_Item_Updated_14_4.png

14) If you didn’t change ALL THREE of the field values in step #12 (above), try changing them all now and confirm they’ve been updated, as expected:

Hector_Update_Second_Item_14_5.png

Hector_Second_Item_Updated_14_6.png

Now that you’ve added functionality such that item changes (large or small) stick, can you think of anything else you might want to implement to make Hector more useful? I have a few ideas – check back next time and we’ll tackle one of them.

Notes:

  1. In fact, for the ambitious, I’d encourage you to consider pulling this code out of both of these functions, placing it in its own function, and modifying create_item() and update_item() to call this new, “DRY”-er function.
00

I Have These And Want Some Of Those!

As teased at the end of the last post, we’re now ready to continue on to adding an item to a collection. The pieces of code we’ll write in this installment will closely mirror those we added a few posts back to get “Add Collection” functionality up-and-running.

We have already written the code to render the Add Item page and form, we just need to pick up from the point where the form is submitted. Here’s the form again, with the action highlighted:

		<dd class="accordion-navigation">
			<a href="#add-item">Add Item</a>
<?php	
	echo '<div id="add-item" class="content '.($accordion_group_open == TRUE ? ' active' : '').'">';
?>
				<fieldset>
<?php
	echo '<legend>'.$page_description.'</legend>';
	echo Form::open('Item/add/'.$collection_id, array('class' => 'custom'));
?>	
					<div class="row">
						<div class="large-2 columns end">
<?php
	echo Form::label('txtItemSeq', 'Sequence:');
	echo Form::input('txtItemSeq', $post ? $post['txtItemSeq']: '', array('id' => 'id_txtItemSeq', 'size' => '35', 'maxlength' => '3', 'required' => true));
	if (array_key_exists('txtItemSeq', $errors))
	{
		echo '<small class="error">'.$errors['txtItemSeq'].'</small>';
	}
?>	
						</div>
					</div>
					<div class="row">
						<div class="large-8 columns end">
<?php	
	echo Form::label('txtItemName', 'Name:');
	echo Form::input('txtItemName', $post ? $post['txtItemName']: '', array('id' => 'id_txtItemName', 'size' => '35', 'maxlength' => '75', 'required' => true));
	if (array_key_exists('txtItemName', $errors))
	{
		echo '<small class="error">'.$errors['txtItemName'].'</small>';
	}
?>	
						</div>
					</div>
					<div class="row">
						<div class="large-3 columns end">
<?php	
	echo Form::label('selItemStatus', 'Status:');
	echo Form::select('selItemStatus', $item_status_sel_arr, $post ? $post['selItemStatus'] : 0);
?>
						</div>
					</div>
					<br />
<?php
	echo Form::button('btnAdd', 'Add', array('class' => 'small button radius submit'));
	echo Form::close();
?>
				</fieldset>
			</div>
		</dd>

1) At this point, it shouldn’t come as much of a surprise to you that the action we need to define is the add action in the Item controller. Since this is our first encounter with the Item controller, we need to create it (save it as “Item.php” in the “/opt/lampp/htdocs/hector/application/classes/Controller” directory):

Item_Controller_13_1

While we’re at it, we’ll add the before() function and both the default and add actions, in one fell swoop:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Item extends Controller_Template_Sitepage {
	
	public function before()
	{
		
		/*
		/	Purpose: Ensure user is signed in before actions in controller are called
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/

		parent::before();  	  
		if ( ! (Session::instance()->get('user_id')))
		{
					
			// User is NOT signed in, redirect to Sign In page
			$this->redirect('User');			
		}
		
	}

	public function action_index()
	{

		/*
		/	Purpose: Default action - redirects to collection controller
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/

		// Redirect to Collection's default action, by default
		$this->redirect('Collection');	
		
	}

	public function action_add()
	{		
		
		/*
		/	Purpose: Action to handle "add item" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/

		// Grab selected collection ID from URL
		$collection_id = $this->request->param('collection');

		// Grab $_POST array from submitted form
		$post = $this->request->post();
		
		if (isset($post['btnAdd']))
		{
		
			// Add button was pressed
			
			$item_created_arr = Model::factory('Item')->create_item($post, $collection_id);
			if ($item_created_arr['Success'])
			{
				
				// Send message indicating successful item creation
				Session::instance()->set('message', "You've added new item \"".$item_created_arr['Clean_Post_Data']['txtItemName']."\"!");
			
				// Redirect to Collection/edit action 
				$this->redirect('Collection/edit/'.$collection_id);			
			}
			else
			{
				
				// Validation failed, display form with custom error text
				$accordions_open = array(
					'add_item' => TRUE
				);
				$posts = array(
					'add_item' => $post
					,'edit_collection' => NULL
				);
				$this->template->content = ViewBuilder::factory('Page')->edit_collection($accordions_open, $collection_id, $this->template, $posts, $item_created_arr['Errors']);
			}		
		} // if (isset($post['btnAdd']))
		else	// This "else" is required so that failed validation (above) won't auto redirect!
		{
		
			// Redirect to Collection/edit action 
			$this->redirect('Collection/edit/'.$collection_id);			
		}	
		
	}
	
} // End Item

Nothing terribly shocking here – the before() function ensures that only signed in users can access any actions in this controller and the default action redirects any users coming to this class directly (such as via a malformed URL) to the Collection controller’s default action. The add action closely resembles the code we wrote when we defined the “Add Collection” functionality.

2) Diving right into the Item model (“/opt/lampp/htdocs/hector/application/classes/Model/Item.php”), let’s add the create_item() function, responsible for initiating the create item process, to the “Business logic functions” section:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Item extends Model_Database {
.
.
.

	// Business logic functions

	public function create_item($post, $collection_id)
	{
			  
		/*
		/	Purpose: Create new item after first confirming collection belongs to current user
		/
		/	Parms:
		/		'post' >> Submitted form data from New Item view
		/		'collection_id' >> Collection ID to which item is to be added
		/
		/	Returns:
		/		Array containing:
		/			'Clean_Post_Data' >> Array containing cleaned form data
		/			'Success' >> Boolean indicating success/failure of item creation
		/		[AND]
		/			'Errors' >> Array containing form validation errors
		*/

		$return_arr = array(
			'Success' => FALSE		  
		);
		
		// Validate Add Item form entry
		$validation_results_arr = Validation_Item::form_fields($post);
		if ($validation_results_arr['Success'])
		{
				  			
			// Validation clean							
			  
			// Select collection that item is attempting to be added to (to ensure it is a 
			//		collection owned by the current user)
			$collection_data = array(
				'user_id' => Session::instance()->get('user_id')
				,'collection_id' => $collection_id
			);
			$collection_row_exists = Model::factory('Collection')->read_exists($collection_data);
			if ($collection_row_exists['Rows_Affected'] == 1)
			{
			
				$form_item_data = array(
					'user_id' => Session::instance()->get('user_id')
					,'collection_id' => $collection_id
					,'seq' => $validation_results_arr['Clean_Post_Data']['txtItemSeq']
					,'descr' => $validation_results_arr['Clean_Post_Data']['txtItemName']
					,'status' => $validation_results_arr['Clean_Post_Data']['selItemStatus']
				);	
				
				// Perform item select
				$item_rowset = $this->read_exists($form_item_data);
				if ($item_rowset['Rows_Affected'] == 0)
				{
					
					// No existing item with specified seq value
				  
					// Perform item creation
					$item_created_arr = $this->create($form_item_data);
					$return_arr['Clean_Post_Data'] = $validation_results_arr['Clean_Post_Data'];

					// All is well!
					$return_arr['Success'] = TRUE;
				}		 
				else
				{
					
					// There IS an existing item with specified seq value, need to re-seq
								
					$item_arr = $this->read_one_collection($form_item_data);
						
					$item_seq_arr = array();
					foreach ($item_arr['Rows'] as $item)
					{
						$item_seq_arr[$item['seq']] = $item;				
					}
				
					// Find max consecutive existing seq
					// Start looking at index immediately above target
					$max_consec_seq = $form_item_data['seq'] + 1;
					while (array_key_exists($max_consec_seq, $item_seq_arr))
					{
						$max_consec_seq++;	// NOTE: This will end up 1 more than it should be - adjust in for loop below
					}
				
					// Starting at max existing index, go back down to seq user is attempting to insert,
					//		incrementing by 1 each existing index
					$item_data = array(
						'collection_id' => $form_item_data['collection_id']	  
					);		
					for ($i = $max_consec_seq - 1; $i >= $form_item_data['seq']; $i--)
					{
						$item_data['old_seq'] = $i;
						$item_updated_arr = $this->update_incr_seq($item_data);
					}
					  
					// Perform item creation
					$item_created_arr = $this->create($form_item_data);
					$return_arr['Clean_Post_Data'] = $validation_results_arr['Clean_Post_Data'];
					
					// All is well!
					$return_arr['Success'] = TRUE;					  
				}
			}
			// else NOOP - Collection doesn't exist or doesn't belong to current user
		}
		else
		{
			$return_arr['Errors'] = $validation_results_arr['Errors'];
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Item

Beyond the familiar call to a validation class, and some additional function calls that follow our expected process flow (akin to what we did for collection creation), there is some distinctly new code in here, as well. This code is to handle the re-sequencing of items if the user is attempting to add an item with a sequence number matching that of an item already present in this collection.

As always, let’s walk through each of the functions as we add the code to our project.

3) Start by creating the Validation_Item class now (name it “Item.php” and save it in the “/opt/lampp/htdocs/hector/application/classes/Validation” directory):

Item_Validation_13_2

Add the form_fields() static function to it, as well:

<?php defined('SYSPATH') or die('No direct script access.');

class Validation_Item extends Validation {

	public static function form_fields($post)
	{
			
		/*
		/	Purpose: Validate item form submitted data
		/
		/	Parms:
		/		Array containing:
		/			'txtItemSeq' >> New item's sequence number
		/			'txtItemName' >> New item's name
		/
		/	Returns:
		/		Array containing:
		/			'Clean_Post_Data' >> Array containing cleaned form data
		/			'Success' >> Boolean indicating success/failure of validation
		/		[AND]
		/			'Errors' >> Array containing form validation errors
		*/
		
		$return_arr = array(
			'Success' => FALSE		  
		);
			  
		$clean_post_data = Validation::factory($post)
					
			// Item sequence number must be non-blank
			->rule('txtItemSeq', 'not_empty')
			
			// Item sequence number must be numeric
			->rule('txtItemSeq', 'numeric')

			// Item sequence number must be a valid number between 1 and 999
			->rule('txtItemSeq', 'range', array(':value', 1, 999))		

			// Item name must be non-blank
			->rule('txtItemName', 'not_empty')
				
			// Item name must not be more than 75 chars long
			->rule('txtItemName', 'max_length', array(':value', 75))
			
		;
		
		if ($clean_post_data->check())
		{
			$return_arr['Clean_Post_Data'] = $clean_post_data;
			
			$return_arr['Success'] = TRUE;
		}
		else
		{	
			$return_arr['Errors'] = $clean_post_data->errors('form_errors');
		}
		
		return $return_arr;
		
	}
	
} // End Validation_Item

In addition to Kohana Validation rules we have seen previously, this class demonstrates the use of a couple of nice rules specifically for numeric values.

4) As in the past, when we’ve added a validation class, we need to add some corresponding error messages to our form errors file (“/opt/lampp/htdocs/hector/application/messages/form_errors.php”):

<?php defined('SYSPATH') or die('No direct script access.');

return array
(
.
.
.

	// Item creation error messages           
	,'txtItemName' => array (
		'max_length' => 'Item name must be 75 chars or less.'
		,'not_empty' => "Item name can't be blank!"
	)
	,'txtItemSeq' => array (
		'not_empty' => "Item sequence can't be blank!"
		,'numeric' => 'Item sequence must be between 1 and 999!'
		,'range' => 'Item sequence must be between 1 and 999!'
	)	
);

We’ve already defined the Collection->read_exists() function and the read_one_collection() function (in our last post), so we’ll continue on by adding three new functions to the Item model (“/opt/lampp/htdocs/hector/application/classes/Model/Item.php”). We’ll address them each one-at-a-time.

5) Starting with the read_exists() function. Add it in the “SQL functions” section:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Item extends Model_Database {
.
.
.
	
	// SQL functions

	public function read_exists($data)
	{
		
		/*
		/	Purpose: Confirm/refute existence of specific item within a specific collection
		/
		/	Parms:
		/		Array containing:
		/			'collection_id' >> Collection ID that item belongs to
		/			'seq' >> Seq of item to find
		/
		/	Returns:
		/		Array containing:
		/			'Rows_Affected' >> Number of item rows (0 or 1) matching specified criteria
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/

		$return_arr = array();

		try
		{
			$return_arr['Rows_Affected'] = 
				DB::select(array(DB::expr('COUNT(seq)'), 'total_count'))
					->from($this->table_name)
					->where('collection_id', '=', $data['collection_id'])
					->and_where('seq', '=', $data['seq'])
					->execute()
					->get('total_count', 0)
			;
		}  
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error
			$error_data = array(
				'problem_descr' => 'Item->read_exists()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Item

I think the comments for this function pretty clearly spell out what it does.

6) Next up is the create() function. It, too, goes in the “SQL functions” section:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Item extends Model_Database {
.
.
.

	// SQL functions

	public function create($data)
	{

		/*
		/	Purpose: Create new item row in database
		/
		/	Parms:
		/		Array containing:
		/			'collection_id' >> Collection ID that new item belongs to
		/			'seq' >> New item's sequence in collection
		/			'descr' >> New item's description
		/			'status' >> New item's status
		/		
		/	Returns:
		/		Array containing:
		/			'Row_Created_ID' >> New item's ID
		/		AND
		/			'Rows_Affected' >> Number of rows (1) inserted into table
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/
  
		$return_arr = array();
		
		try
		{
			list($return_arr['Row_Created_ID'], $return_arr['Rows_Affected']) = 
				DB::insert($this->table_name)
					->columns($this->table_cols)
					->values(array(
						$data['collection_id']
						,$data['seq']
						,$data['descr']
						,$data['status']
				
						// Default to_delete to FALSE
						,FALSE
				
						// Default last_update_dttm to current datetime
						,date('Y-m-d H:i:s') 
					))
					->execute()
			;		
		}
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error
			$error_data = array(
				'problem_descr' => 'Item->create()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Item

Nothing conceptually new here, either.

7) After that is the update_incr_seq() function. Add this in the “SQL functions” section as well:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Item extends Model_Database {
.
.
.

	// SQL functions

	public function update_incr_seq($data)
	{

		/*
		/	Purpose: Increments seq (by 1) of specified item in specific collection
		/
		/	Parms:
		/		Array containing:
		/			'collection_id' >> ID of collection to update
		/			'old_seq' >> (Old) seq of item, within collection, to update
		/
		/	Returns:
		/		Array containing:
		/			'Rows_Affected' >> Number of item rows affected by UPDATE
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/

		$return_arr = array();
		
		try
		{
			$return_arr['Rows_Affected'] = 
				DB::update($this->table_name)
					->set(array(
						'seq' => DB::expr('seq + 1')
					))
					->where('collection_id', '=', $data['collection_id'])
					->and_where('seq', '=', $data['old_seq'])
					->execute()
			;
		}
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error
			$error_data = array(
				'problem_descr' => 'Item->update_incr_seq()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Item

As the comments suggest, this function simply bumps up the specific item’s seq column value by 1.

8) You should now be able to add items to your collections and see them sorted by sequence number:

Hector_Add_First_Item_13_3

Hector_First_Item_Added_13_4

9) Add another item, this time with the same sequence number as an existing item. Notice that doing so causes the existing one to be bumped down the list (and this continues, if that move displaces another consecutive item, until all sequence numbers are unique for a collection):

Hector_Add_Second_Item_13_5

Hector_Second_Item_Added_13_6

Stay tuned for editing an item – that’s next time!

Notes:

00

Oops, That’s Not What It’s Called!

In this installment of the Hector tutorial, we’ll be walking through the code required to edit a collection.

In order to tie things together a bit, recall the Collections view (“/opt/lampp/htdocs/hector/application/views/Collections.php”) from the last post. It is responsible for displaying the list of Collections you’ve added to the system. Take a look at the highlighted row – this is the controller/action combination that will get routed to when we click on an existing collection:

		<dd class="accordion-navigation">
			<a href="#collections">View Collections</a>
<?php	
	echo '<div id="collections" class="content ' . ($accordion_group_open == TRUE ? ' active' : '') . '">';
?>
				<fieldset>
<?php
	echo '<legend>'.$page_description.'</legend>';
	if (count($collection_arr))
	{
		foreach ($collection_arr as $collection)
		{	
?>
					<div class="row">
						<div class="small-6 columns">
<?php
			echo HTML::anchor('Collection/edit/'.$collection['collection_id'], $collection['name']);			
?>
						</div>
						<div class="small-6 columns">
<?php   					
			echo '('.$collection['type'].')';
?>					  
						</div>
					</div>   					
<?php
		}
	}
	else
	{
		echo 'No collections found.  Click on "Add Collection" above to get started!<br />';	 
	}
?>
				</fieldset>
			</div>
		</dd>

1) Let’s add the edit action to the Collection controller (“/opt/lampp/htdocs/hector/application/classes/Controller/Collection.php”) now:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Collection extends Controller_Template_Sitepage {
.
.
.

	public function action_edit()
	{

		/*
		/	Purpose: Action to handle "edit collection" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/

		// Grab selected collection ID from URL
		$collection_id = $this->request->param('collection');
	
		// Determine if this collection exists AND belongs to current user
		$collection_exists_arr = Model::factory('Collection')->edit_collection($collection_id);
		if ($collection_exists_arr['Success'])
		{
		
			// Show selected collection
			$accordions_open = array(
				'add_item' => FALSE
			);
			$posts = array(
				'add_item' => NULL
				,'edit_collection' => NULL
			);
			$this->template->content = ViewBuilder::factory('Page')->edit_collection($accordions_open, $collection_id, $this->template, $posts, $errors = array());
		}
		else
		{

			// Redirect to view action 
			$this->redirect('Collection/view');				  
		}
		
	}
	
.
.
.
} // End Collection

Again we call on the Collection model, this time calling the edit_collection() function to confirm the selected collection exists and is owned by the current user, before calling the ViewBuilder_Page class’s edit_collection() function to actually render the Edit Collection page.

2) Re-open the Collection model (“/opt/lampp/htdocs/hector/application/classes/Model/Collection.php”) and add the edit_collection() function in the “Business logic functions” section:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Collection extends Model_Database {
.
.
.

	// Business logic functions

	public function edit_collection($collection_id)
	{
			  
		/*
		/	Purpose: Edit specific collection for current user
		/
		/	Parms:
		/		'collection_id' >> Collection ID to edit
		/
		/	Returns:
		/		Array containing:
		/			'Success' >> Boolean indicating success/failure of collection's existence
		*/
			  
		$return_arr = array(
			'Success' => FALSE		  
		);
		
		$collection_data = array(
			'user_id' => Session::instance()->get('user_id')
			,'collection_id' => $collection_id
		);
		$coll_items_arr = $this->read_exists($collection_data);
		if ($coll_items_arr['Rows_Affected'] == 1)
		{

			// All is well!
			$return_arr['Success'] = TRUE;	  
		}
		
		return $return_arr;
		
	}
	
.
.
.
} // End Model_Collection

This requires little explanation – as I said above, this function merely confirms that the specified collection_id exists and is tied to the current user.

3) This function, in turn, calls the read_exists() SQL function, so let’s put that in now, under the “SQL functions” section (naturally):

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Collection extends Model_Database {
.
.
.

	// SQL functions

	public function read_exists($data)
	{
		
		/*
		/	Purpose: Confirm/refute existence of specific collection for one (specified) user
		/
		/	Parms:
		/		Array containing:
		/			'user_id' >> User ID to find collection for
		/			'collection_id' >> Collection ID of collection to find
		/
		/	Returns:
		/		Array containing:
		/			'Rows_Affected' >> Number of collection rows (0 or 1) matching specified criteria
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/

		$return_arr = array();
		
		try
		{
			$return_arr['Rows_Affected'] = 
				DB::select(array(DB::expr('COUNT(collection_id)'), 'total_count'))
					->from($this->table_name)
					->where('user_id', '=', $data['user_id'])
					->and_where('collection_id', '=', $data['collection_id'])
					->execute()
					->get('total_count', 0)
			;
		}
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error
			$error_data = array(
				'problem_descr' => 'Collection->read_exists()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Collection

4) Returning to step #1 (above), if we’ve established that this collection DOES exist and DOES belong to the current user, we next want to get it displayed on a page. The first step toward accomplishing that is the call to the new edit_collection() function in our old friend the ViewBuilder_Page class (“/opt/lampp/htdocs/hector/application/classes/ViewBuilder/Page.php”). Add it now:

<?php defined('SYSPATH') or die('No direct script access.');

class ViewBuilder_Page {
.
.
.

	public function edit_collection($accordions_open, $collection_id, $template, $posts, $errors)
	{
			  
		/*
		/	Purpose: Build Edit Collection page
		/
		/	Parms:
		/		'accordions_open' >> Array, indexed by view, indicating whether that view's accordion should default to open
		/		'collection_id' >> Collection ID to edit
		/		'template' >> Array containing page header attributes
		/		'posts' >> Array, indexed by view, containing either submitted form data or NULL
		/		'errors' >> Form validation errors or NULL
		/
		/	Returns:
		/		Page to render
		*/			
		
		// Read all collection types
		$collection_type_sel_arr = Model::factory('CollectionType')->read_all_collection_types();
			  			  
		// Read user's selected collection (and its items)
		$coll_and_items_data = array(
			'user_id' => Session::instance()->get('user_id')
			,'collection_id' => $collection_id
		);
		$coll_and_items_arr = Model::factory('Collection')->read_one_colls_items($coll_and_items_data);

		// Read all item statuses
		$item_status_sel_arr = Model::factory('ItemStatus')->read_all_item_statuses();
		
		// Render Items page
		$template->page_description = 'Items';
		$template->title .= $template->page_description;
		$template->navbar = View::factory('Navbar');

		$collection_view = ViewBuilder::factory('CollectionView');
		$item_view = ViewBuilder::factory('ItemView');

		// Edit Collection view
		$content = $collection_view->edit_collection($collection_id, $collection_type_sel_arr, $coll_and_items_arr, $posts['edit_collection'], $errors);
		
		// Add Item view
		$content .= $item_view->add_item($collection_id, $item_status_sel_arr, $accordions_open['add_item'], $posts['add_item'], $errors);

		// Items view
		$content .= $item_view->items($collection_id, $coll_and_items_arr, $item_status_sel_arr);
		
		return $content;
		
	}	

.
.
.
} // End ViewBuilder_Page

This first retrieves all defined collection types (which we covered a few posts back). Next, it retrieves the chosen collection (and its associated items). Once it has all of this data, it utilizes three views to generate the overall page.

5) Let’s add the read_one_colls_items() function to the “Business logic functions” section of the Collection model (“/opt/lampp/htdocs/hector/application/classes/Model/Collection.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Collection extends Model_Database {
.
.
.

	// Business logic functions

	public function read_one_colls_items($data)
	{
			  
		/*
		/	Purpose: Read one specific collection, including items contained in it
		/
		/	Parms:
		/		Array containing:
		/			'user_id' >> User ID to return collection for
		/			'collection_id' >> Collection ID of collection to return
		/
		/	Returns:
		/		Array containing:
		/			'Success' >> Boolean indicating success/failure of collection read
		/		[AND]
		/			'Collection' >> Array containing collection's attributes
		/			'Items' >> Array containing items in collection
		*/

		$return_arr = array(
			'Success' => FALSE		  
		);

		// Read one collection's attributes
		$collection_rowset = $this->read_one($data);
		if ($collection_rowset['Rows_Affected'] == 1)
		{

			// Collection exists AND belongs to current user
				
			// Preserve collection's attributes
			$return_arr['Collection'] = $collection_rowset['Rows'][0];
					  
			// Read collection's items
			$item_rowset = Model::factory('Item')->read_one_collection($data);
						  
			// NO DB Exception, if we made it this far
			$return_arr['Items'] = $item_rowset['Rows'];
			
			// All is well!
			$return_arr['Success'] = TRUE;
		}
		// else NOOP - Nothing to do here as no rows returned because EITHER this 
		//		collection doesn't exist OR it doesn't belong to current user
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Collection

This function relies on a couple of not-yet-defined SQL functions to select the collection (and items).

6) First, we need to add the new read_one() function to the Collection model. Add it under the “SQL functions” section:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Collection extends Model_Database {
.
.
.
	
	// SQL functions
	  
	public function read_one($data)
	{			  
			  
		/*
		/	Purpose: Read specific collection for one (specified) user
		/
		/	Parms:
		/		Array containing:
		/			'user_id' >> User ID to return collection for
		/			'collection_id' >> Collection ID of collection to return
		/
		/	Returns:
		/		Array containing:
		/			'Rows' >> Array of specified collection columns
		/		AND
		/			'Rows_Affected' >> Number of collection rows in 'Rows'
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/

		$return_arr = array();
		
		try
		{
			$return_arr['Rows'] = 
				DB::select(
					'collection_id'
					,array($this->table_name.'.descr', 'name')
					,array($this->table_name.'.coll_type_id', 'type_id')				  
					,array('collection_type.descr', 'type')
					,array($this->table_name.'.last_update_dttm', 'last_update_dttm')				  
				)
					->from($this->table_name)
					->join('collection_type')
						->on($this->table_name.'.coll_type_id', '=', 'collection_type.coll_type_id')
					->where('user_id', '=', $data['user_id'])
					->and_where('collection_id', '=', $data['collection_id'])
					->execute()
			;		
			$return_arr['Rows_Affected'] = count($return_arr['Rows']);
		}
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error
			$error_data = array(
				'problem_descr' => 'Collection->read_one()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}	

.
.
.
} // End Model_Collection

This simply retrieves all attributes for the specified collection.

7) Then we need to add code to the Item model. Open it (“/opt/lampp/htdocs/hector/application/classes/Model/Item.php”) and add the read_one_collection() function to the “SQL functions” section:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Item extends Model_Database {
.
.
.

	// SQL functions

	public function read_one_collection($data)
	{
			  
		/*
		/	Purpose: Read all items for specified collection
		/
		/	Parms:
		/		Array containing:
		/			'collection_id' >> Collection ID of collection to return
		/
		/	Returns:
		/		Array containing:
		/			'Rows' >> Array containing items contained in specified collection
		/		AND
		/			'Rows_Affected' >> Number of collection rows in 'Rows'
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/

		$return_arr = array();
		
		try 
		{		
			$return_arr['Rows'] = 
				DB::select()
					->from($this->table_name)
					->where('collection_id', '=', $data['collection_id'])
					->and_where('to_delete', '=', 0)
					->order_by('seq')
					->execute()
			;		
			$return_arr['Rows_Affected'] = count($return_arr['Rows']);
		}
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error
			$error_data = array(
				'problem_descr' => 'Item->read_one_collection()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}
   
.
.
.
} // End Model_Item

This selects all items, belonging to a specific collection, that are not flagged for deletion.

8) The final chunk of data we need to retrieve is the item statuses. Re-open the ItemStatus model (“/opt/lampp/htdocs/hector/application/classes/Model/ItemStatus.php”) and add the read_all_item_statuses() function to the “Business logic functions” section:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_ItemStatus extends Model_Database {
.
.
.
	
	// Business logic functions

	public function read_all_item_statuses()
	{
			  
		/*
		/	Purpose: Read all item statuses and store in array, indexed by ID
		/
		/	Parms:
		/		[NONE]
		/
		/	Returns:
		/		Array containing item statuses, indexed by item_status_cd
		*/
			
		$return_arr = array();

		// Read all item statuses
		$item_status_arr = $this->read();
		
		// Populate select array for item statuses
		foreach ($item_status_arr['Rows'] as $status_index => $status)
		{
			$return_arr[$status['item_status_cd']] = $status['descr'];
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_ItemStatus

This calls the read() function in this same class, to perform the actual database row selection.

9) Let’s add the new read() function to the “SQL functions” section of this same model, while we’re at it:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_ItemStatus extends Model_Database {
.
.
.

	// SQL functions

	public function read()
	{			  
			  
		/*
		/	Purpose: Read all item statuses
		/
		/	Parms:
		/		[NONE]
		/
		/	Returns:
		/		Array containing:
		/			'Rows' >> Array of all item statuses in system
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/

		$return_arr = array();
		
		try
		{
			$return_arr['Rows'] = 
				DB::select()
					->from($this->table_name)
					->order_by('seq')
					->execute()
			;
		}
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error,
			//		redirect user to sign in page, and display error message
			$error_data = array(
				'problem_descr' => 'ItemStatus->read()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_ItemStatus

Now that we have all of the requisite data, we can call our view builders to cobble together the resultant page.

10) First up is a supplement to the ViewBuilder_CollectionView class (“/opt/lampp/htdocs/hector/application/classes/ViewBuilder/CollectionView.php”). Add the new edit_collection() function:

<?php defined('SYSPATH') or die('No direct script access.');

class ViewBuilder_CollectionView {
.
.
.

	public function edit_collection($collection_id, $collection_type_sel_arr, $coll_and_items_arr, $post_or_null, $errors)
	{
			  
		/*
		/	Purpose: Build Edit Collection view
		/
		/	Parms:
		/		'collection_id' >> Collection ID to edit
		/		'collection_type_sel_arr' >> Array containing selectable collection types
		/		'coll_and_items_arr' >> Array containing user's selected collection (and its items)
		/		'post_or_null' >> Submitted form data or NULL
		/		'errors' >> Form validation errors or NULL
		/
		/	Returns:
		/		View to render
		*/			
      
		$content =
 
			// Render message block
			View::factory('Message_Block')
       	
			// Render Edit Collection view
			.View::factory('Edit_Collection')
				->set('collection_type_sel_arr', $collection_type_sel_arr)
				->set('collection_arr', $coll_and_items_arr['Collection'])
				->set('page_description', 'Edit Collection')
				->set('post', $post_or_null)
				->set('errors', $errors)
		; 
     	
		return $content;
		
	}

.
.
.		
} // End ViewBuilder_CollectionView

This looks very similar to our other view builder functions responsible for (eventual) form rendering.

11) To get the Edit Collection form to render, we need to add the new view (call it “Edit_Collection.php” and save it in the “/opt/lampp/htdocs/hector/application/views” directory):

Edit_Collection_Views_12_1

Add the following code and markup:

				<fieldset>
<?php
	echo '<legend>'.$page_description.'</legend>';
	echo Form::open('Collection/update/'.$collection_arr['collection_id'], array('class' => 'custom'));
?>	
					<div class="row">
						<div class="large-4 columns end">
<?php
	echo Form::label('selCollectionType', 'Type:');
	echo Form::select('selCollectionType', $collection_type_sel_arr, $post ? $post['selCollectionType'] : $collection_arr['type_id']);
?>	
						</div>
					</div>
					<div class="row">
						<div class="large-8 columns end">
<?php	
	echo Form::label('txtCollectionName', 'Name:');
	echo Form::input('txtCollectionName', $post ? $post['txtCollectionName'] : $collection_arr['name'], array('id' => 'id_txtCollectionName', 'size' => '35', 'maxlength' => '75', 'required' => true));
	if (array_key_exists('txtCollectionName', $errors))
	{
		echo '<small class="error">'.$errors['txtCollectionName'].'</small>';
	}
?>
						</div>
					</div>
					<br />
					<div class="row">
						<div class="small-6 medium-3 large-2 columns">
<?php
	echo Form::button('btnUpdate', 'Update', array('class' => 'small button radius submit'));
?>
						</div>
					</div>
<?php	
	echo Form::close();
	echo HTML::anchor('Collection/view', '<< Back to Collections');
?>
				</fieldset>

This form, as expected, looks quite a bit like the Add Collection form. The differences are not terribly surprisingly either (an “Update” button instead of “Add” and no accordion code).

12) Moving on, now we get to the item portion of the page. Before we handle coding the views, we first need to define our ItemView ViewBuilder sub-class (name it “ItemView.php” and save it in the “/opt/lampp/htdocs/hector/application/classes/ViewBuilder” directory):

ItemView_ViewBuilder_12_2

In addition to defining the class itself, we’ll add two functions to it now: add_item() and items():

<?php defined('SYSPATH') or die('No direct script access.');

class ViewBuilder_ItemView {

	public function add_item($collection_id, $item_status_sel_arr, $accordion_open, $post_or_null, $errors)
	{
			  
		/*
		/	Purpose: Build Add Item view
		/
		/	Parms:
		/		'collection_id' >> Collection ID of item to edit
		/		'item_status_sel_arr' >> Array containing selectable item statuses
		/		'accordion_open' >> Boolean indicating whether view's accordion should default to open
		/		'post_or_null' >> Submitted form data or NULL
		/		'errors' >> Form validation errors or NULL
		/
		/	Returns:
		/		View to render
		*/			

		$content =

			View::factory('Accordion_Start')
				->set('accordion_id', 'add_item')

			// Render Add Item view
			.View::factory('Add_Item')
				->set('collection_id', $collection_id)
				->set('item_status_sel_arr', $item_status_sel_arr)
				->set('accordion_group_open', $accordion_open)
				->set('page_description', 'New Item')
				->set('post', $post_or_null)
				->set('errors', $errors)
      		
			.View::factory('Accordion_End')
		;
      
		return $content;
		
	}
	
	public function items($collection_id, $coll_and_items_arr, $item_status_sel_arr)
	{

		/*
		/	Purpose: Build Items view
		/
		/	Parms:
		/		'collection_id' >> Collection ID of items
		/		'coll_and_items_arr' >> Array containing user's selected collection (and its items)
		/		'item_status_sel_arr' >> Array containing selectable item statuses
		/
		/	Returns:
		/		View to render
		*/			

		$content =
      			 
			View::factory('Accordion_Start')
				->set('accordion_id', 'items')

			// Render Items view
			.View::factory('Items')
				->set('items_arr', $coll_and_items_arr['Items'])
				->set('item_status_sel_arr', $item_status_sel_arr)
				->set('accordion_group_open', TRUE)
				->set('page_description', 'Items')
  
			.View::factory('Accordion_End')
		;
      
		return $content;
		
	}

} // End ViewBuilder_ItemView

As you can see, these two functions handle the addition of two accordion areas to the page via calls to views.

13) The first of the views is Add Item (create a file named “Add_Item.php” and save it in the “/opt/lampp/htdocs/hector/application/views” directory):

Add_Item_Views_12_3

Add the following code and markup:

		<dd class="accordion-navigation">
			<a href="#add-item">Add Item</a>
<?php	
	echo '<div id="add-item" class="content '.($accordion_group_open == TRUE ? ' active' : '').'">';
?>
				<fieldset>
<?php
	echo '<legend>'.$page_description.'</legend>';
	echo Form::open('Item/add/'.$collection_id, array('class' => 'custom'));
?>	
					<div class="row">
						<div class="large-2 columns end">
<?php
	echo Form::label('txtItemSeq', 'Sequence:');
	echo Form::input('txtItemSeq', $post ? $post['txtItemSeq']: '', array('id' => 'id_txtItemSeq', 'size' => '35', 'maxlength' => '3', 'required' => true));
	if (array_key_exists('txtItemSeq', $errors))
	{
		echo '<small class="error">'.$errors['txtItemSeq'].'</small>';
	}
?>	
						</div>
					</div>
					<div class="row">
						<div class="large-8 columns end">
<?php	
	echo Form::label('txtItemName', 'Name:');
	echo Form::input('txtItemName', $post ? $post['txtItemName']: '', array('id' => 'id_txtItemName', 'size' => '35', 'maxlength' => '75', 'required' => true));
	if (array_key_exists('txtItemName', $errors))
	{
		echo '<small class="error">'.$errors['txtItemName'].'</small>';
	}
?>	
						</div>
					</div>
					<div class="row">
						<div class="large-3 columns end">
<?php	
	echo Form::label('selItemStatus', 'Status:');
	echo Form::select('selItemStatus', $item_status_sel_arr, $post ? $post['selItemStatus'] : 0);
?>
						</div>
					</div>
					<br />
<?php
	echo Form::button('btnAdd', 'Add', array('class' => 'small button radius submit'));
	echo Form::close();
?>
				</fieldset>
			</div>
		</dd>

We’ll discuss the details of this view when we tackle adding items to a collection in our next post.

14) And the final view is Items (create the file named “Items.php” and save it in the “/opt/lampp/htdocs/hector/application/views” directory):

Items_Views_12_4

Add this code and markup:

<dd class="accordion-navigation">
	<a href="#items">View Items</a>
<?php	
	echo '<div id="items" class="content '.($accordion_group_open == TRUE ? ' active' : '').'">';
?>
		<fieldset>
<?php
	echo '<legend>'.$page_description.'</legend>';
	if (count($items_arr))
	{
		foreach ($items_arr as $item)
		{	
?>
			<div class="row">
				<div class="small-6 columns">
<?php
			echo HTML::anchor('Item/edit/'.$item['collection_id'].'/'.$item['seq'], $item['descr']);			
?>
				</div>
				<div class="small-6 columns">
<?php   					
			echo $item_status_sel_arr[$item['status']];
?>					  
				</div>
			</div>   					
<?php
		}
	}
	else
	{
		echo 'No items found.  Click on "Add Item" above to get started adding to your collection!<br />';	 
	}
?>
		</fieldset>
	</div>
</dd>

Like the Add Item view, we’ll delve into the details of this view when we work on adding items to a collection (in the next installment of the tutorial).

At this point you should be able to click on the collection you added, at the end of last post, and see the Edit Collection page come up populated with relevant data.

What?! That doesn’t work? Does it look like the page merely refreshed and you’re back on the View Collections page?:

Hector_One_Collection_12_5

Any idea what is happening?

If you guessed that we need to add an additional routing to the bootstrap file, congratulations!

We have no route, presently, to handle URLs of the form “controller/action/[collection_id]”, which is what we’re trying to access when editing a collection1. It might be prudent to address that now!

15) Open up “bootstrap.php” (located in the “/opt/lampp/htdocs/hector/application” directory) and add the highlighted route – remember, the sequence is important!:

<?php defined('SYSPATH') or die('No direct script access.');
.
.
.

/**
 * Set the routes. Each route must have a minimum of a name, a URI and a set of
 * defaults for the URI.
 */
Route::set('validate', '<controller>/<action>/<email_addr>/<authcode>'
		  ,array(
		  	'controller' => 'Validate'
		  	,'email_addr' => '[^/,;?\n]++'
		  )
	);

Route::set('collection_in_url', '<controller>/<action>/<collection>'
		  ,array(
		  	'controller' => '(Collection|Item)'
		  )
	);

Route::set('default', '(<controller>(/<action>(/<junk>)))', array('junk' => '.*'))
	->defaults(array(
		'controller' => 'Main',
		'action'     => 'index',
	));

Ahh, that’s better – you should be able to bring up the Edit Collection page now:

Hector_Edit_Collection_12_6

Let’s continue by adding the code necessary to make changes, made to the collection, actually update relevant data in the underlying database.

To do that, we need to identify the controller/action combination defined in the Edit Collection form.

Returning to the Edit Collection view introduced in step #11 (above), we see the form action is set to Collection/update/[collection_id]:

				<fieldset>
<?php
	echo '<legend>'.$page_description.'</legend>';
	echo Form::open('Collection/update/'.$collection_arr['collection_id'], array('class' => 'custom'));
?>	
					<div class="row">
						<div class="large-4 columns end">
<?php
	echo Form::label('selCollectionType', 'Type:');
	echo Form::select('selCollectionType', $collection_type_sel_arr, $post ? $post['selCollectionType'] : $collection_arr['type_id']);
?>	
						</div>
					</div>
					<div class="row">
						<div class="large-8 columns end">
<?php	
	echo Form::label('txtCollectionName', 'Name:');
	echo Form::input('txtCollectionName', $post ? $post['txtCollectionName'] : $collection_arr['name'], array('id' => 'id_txtCollectionName', 'size' => '35', 'maxlength' => '75', 'required' => true));
	if (array_key_exists('txtCollectionName', $errors))
	{
		echo '<small class="error">'.$errors['txtCollectionName'].'</small>';
	}
?>
						</div>
					</div>
					<br />
					<div class="row">
						<div class="small-6 medium-3 large-2 columns">
<?php
	echo Form::button('btnUpdate', 'Update', array('class' => 'small button radius submit'));
?>
						</div>
					</div>
<?php	
	echo Form::close();
	echo HTML::anchor('Collection/view', '<< Back to Collections');
?>
				</fieldset>

16) Let’s add the update action to the Collection controller (“/opt/lampp/htdocs/hector/application/classes/Controller/Collection.php”) now:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Collection extends Controller_Template_Sitepage {
.
.
.

	public function action_update()
	{
			  
		/*
		/	Purpose: Action to handle "update collection" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/

		// Grab selected collection ID from URL
		$collection_id = $this->request->param('collection');

		// Grab $_POST array from submitted form
		$post = $this->request->post();
		
		if (isset($post['btnUpdate']))
		{	
				  
			// Update button was pressed
				  
			$collection_updated_arr = Model::factory('Collection')->update_collection($post, $collection_id);
			if ($collection_updated_arr['Success'])
			{
					
				// Send message indicating successful collection update
				Session::instance()->set('message', "You've updated collection \"".$collection_updated_arr['Clean_Post_Data']['txtCollectionName']."\"!");				
				
				// Redirect to edit action 
				$this->redirect('Collection/edit/'.$collection_id);	
			} // if ($collection_updated_arr['Success'])
			else
			{
					  
				// Validation failed, display form with custom error text				
				$accordions_open = array(
					'add_item' => FALSE
				);
				$posts = array(
					'add_item' => NULL
					,'edit_collection' => $post
				);
				$this->template->content = ViewBuilder::factory('Page')->edit_collection($accordions_open, $collection_id, $this->template, $posts, $collection_updated_arr['Errors']);
			}
		} // if (isset($post['btnUpdate']))
		else
		{
      		  
			// Bad URL (i.e. someone went to /Collection/update manually)

			// Redirect to view action
			$this->redirect('Collection/view');
		}
		
	}	

.
.
.
} // End Collection

Similar to what we’ve seen several times before, this controller calls a model function to handle database interaction and then routes the user to either a success state (where a success message is displayed) or an error state (where the submitted form is re-generated, complete with submitted data retained and error message(s) explaining problems with the data). The wrinkle here is that we’re checking first to see if the form was submitted as a result of the “Update” button being pressed.

17) So, on we go. Add another business logic function – this time, update_collection(), to the Collection model (“/opt/lampp/htdocs/hector/application/classes/Model/Collection.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Collection extends Model_Database {
.
.
.

	// Business logic functions

	public function update_collection($post, $collection_id)
	{
			  
		/*
		/	Purpose: Update specific collection for current user
		/
		/	Parms:
		/		'post' >> Submitted form data from Edit Collection view
		/		'collection_id' >> Collection ID to be updated
		/
		/	Returns:
		/		Array containing:
		/			'Clean_Post_Data' >> Array containing cleaned form data
		/			'Success' >> Boolean indicating success/failure of collection update
		/		[AND]
		/			'Errors' >> Array containing form validation errors
		*/

		$return_arr = array(
			'Success' => FALSE		  
		);
			  
		// Validate Edit Collection form entry
		$validation_results_arr = Validation_Collection::form_fields($post);

		if ($validation_results_arr['Success'])
		{
			
			// Validation clean, update existing collection							
			$collection_data = array(
				'user_id' => Session::instance()->get('user_id')
				,'collection_id' => $collection_id
				,'descr' => $validation_results_arr['Clean_Post_Data']['txtCollectionName']
				,'coll_type_id' => $validation_results_arr['Clean_Post_Data']['selCollectionType']
			);			
			if ($this->update($collection_data))
			{
				$return_arr['Clean_Post_Data'] = $validation_results_arr['Clean_Post_Data'];
				
				// All is well!
				$return_arr['Success'] = TRUE;
			}
		}
		else
		{
			$return_arr['Errors'] = $validation_results_arr['Errors'];
		}
		
		return $return_arr;	
		
	}

.
.
.
} // End Model_Collection

This, too, follows the pattern we’ve established with the business logic functions in our model. We call a validation class’s function to ensure submitted form data is in acceptable shape, and if so, perform a SQL function and return with a success flag. If the validation fails, we return the set of errors so the user can fix their mistakes and resubmit.

We’ve already defined the Validation_Collection::form_fields() function used here (as it was needed for initial creation of the collection).

18) So now we merely need to write the update() SQL function in the Collection model (“/opt/lampp/htdocs/hector/application/classes/Model/Collection.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Collection extends Model_Database {
.
.
.

	// SQL functions

	public function update($data)
	{

		/*
		/	Purpose: Updates specified collection
		/
		/	Parms:
		/		Array containing:
		/			'user_id' >> User ID to update collection for
		/			'collection_id' >> ID of collection to update
		/			'descr' >> Collection's updated description
		/			'coll_type_id' >> Collection's updated type ID
		/
		/	Returns:
		/		Array containing:
		/			'Rows_Affected' >> Number of collection rows affected by UPDATE
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/

		$return_arr = array();
		
		try
		{
			$return_arr['Rows_Affected'] = 
				DB::update($this->table_name)
					->set(array(
						'descr' => $data['descr']
						,'coll_type_id' => $data['coll_type_id']
					))
					->where('user_id', '=', $data['user_id'])
					->and_where('collection_id', '=', $data['collection_id'])
					->execute()
			;
		}
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error
			$error_data = array(
				'problem_descr' => 'Collection->update()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}   

.
.
.
} // End Model_Collection

Nothing earth-shattering here – though it is worth noting that the UPDATE statement does include the user_id column in the WHERE clause just as final insurance to prevent an update to a collection that the current user does not own.

19) Now you should be able to edit an existing collection and receive a success message when you press the “Update” button:

Hector_Collection_Updated_12_7

20) You can also see the changes you made (to the collection type and/or name) reflected in the list of all of your collections if you go back to the View Collections page (via the “Back to Collections” link):

Hector_One_Collection_Updated_12_8

Well done – you made it!

Next time, we’ll bolster Hector with the ability to add items.

Notes:

  1. As foreshadowing, we’ll also need to be able to route similar URLs to the to-be-defined Item controller.
00

It’s Not Junk, It’s A Collection!

Now that we have Hector’s table additions built and initial models defined, let’s revisit the Collection controller (“/opt/lampp/htdocs/hector/application/classes/Controller/Collection.php”).

We’d put a placeholder into the default action so that when we signed in via our user module, we’d get a page indicating we’d been successfully signed in.

1) Let’s replace that now, with a call to the view action:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Collection extends Controller_Template_Sitepage {
.
.
.
	
	public function action_index()
	{

		/*
		/	Purpose: Default action - routes to view action
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/

		// Route to view action, by default
		$this->action_view();
		
	}

.
.
.
} // End Collection

2) Next, add the view action, itself, to the Collection controller:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Collection extends Controller_Template_Sitepage {
.
.
.
	
	public function action_view()
	{
			  		
		/*
		/	Purpose: Action to handle "view collections" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/

		// Show all of current user's collections
		$accordions_open = array(
			'add_collection' => FALSE
			,'collections' => TRUE
		);
		$this->template->content = ViewBuilder::factory('Page')->view_collections($accordions_open, $this->template, NULL, $errors = array());
		
	}

.
.
.
} // End Collection

The view action, in turn, relies on that old standby, the ViewBuilder_Page class, this time calling its new view_collections() function. This is the first time we’ve seen the $accordions_open parameter. This array is passed all the way through to the page to be rendered, resolving conditional statements in the page code itself, which determine whether each accordion on the page is to be initially displayed open or not (i.e., closed)1.

3) It’s time now to define the code for the view_collections() function in the ViewBuilder_Page class (“/opt/lampp/htdocs/hector/application/classes/ViewBuilder/Page.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class ViewBuilder_Page {
.
.
.

	public function view_collections($accordions_open, $template, $post, $errors)
	{

		/*
		/	Purpose: Build View Collections page
		/
		/	Parms:
		/		'accordions_open' >> Array, indexed by view, indicating whether that view's accordion should default to open
		/		'template' >> Array containing page header attributes
		/		'post' >> Submitted form data or NULL
		/		'errors' >> Form validation errors or NULL
		/
		/	Returns:
		/		Page to render
		*/			
			
		// Read all collection types
		$collection_type_sel_arr = Model::factory('CollectionType')->read_all_collection_types();

		// Read user's collections
		$collection_data = array(
			'user_id' => Session::instance()->get('user_id')
		);
		$collection_arr = Model::factory('Collection')->read_one_user($collection_data);			
		
		// Render Collections page
		$template->page_description = 'Collections';
		$template->title .= $template->page_description;
		$template->navbar = View::factory('Navbar');
			  
		$collection_view = ViewBuilder::factory('CollectionView');
		
		// Add Collection view
		$content = $collection_view->add_collection($accordions_open['add_collection'], $collection_type_sel_arr, $post, $errors);
		
		// Collections view
		$content .= $collection_view->collections($accordions_open['collections'], $collection_arr);
		
		return $content;
		
	}	

.
.
.
} // End ViewBuilder_Page

This is the first function we’ve seen in this class that is a little more substantial. It performs two calls to models, to retrieve sets of data from the database. It then calls two functions in the CollectionView ViewBuilder sub-class, passing each its respective accordion default state, its respective dataset, and, in the case of the add_collection function(), it passes two additional parameters that should make you suspect there will be a form rendered.

Returning to the new models we defined last post, let’s tackle the new addition to the CollectionType model first.

4) Open the CollectionType model (“/opt/lampp/htdocs/hector/application/classes/Model/CollectionType.php”) and add the read_all_collection_types() function to the “Business logic functions” section:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_CollectionType extends Model_Database {
.
.
.

	// Business logic functions

	public function read_all_collection_types()
	{
			  
		/*
		/	Purpose: Read all collection types and store in array, indexed by ID
		/
		/	Parms:
		/		[NONE]
		/
		/	Returns:
		/		Array containing collection types, indexed by coll_type_id
		*/
			
		$return_arr = array();

		// Read all collection types
		$collection_type_arr = $this->read();
		
		// Populate select array for collection types
		foreach ($collection_type_arr['Rows'] as $type_index => $type)
		{
			$return_arr[$type['coll_type_id']] = $type['descr'];
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_CollectionType

This relies on a new SQL function in this class, read(), to supply data to populate the return array (to be keyed by coll_type_id).

5) Add the read() function, as shown, to the “SQL functions” section of the model:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_CollectionType extends Model_Database {
.
.
.

	// SQL functions

	public function read()
	{			  
			  
		/*
		/	Purpose: Read all collection types
		/
		/	Parms:
		/		[NONE]
		/
		/	Returns:
		/		Array containing:
		/			'Rows' >> Array of all collection types in system
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/

		$return_arr = array();
		
		try
		{
			$return_arr['Rows'] = 
				DB::select()
					->from($this->table_name)
					->order_by('descr')
					->execute()
			;
		}
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error,
			//		redirect user to sign in page, and display error message
			$error_data = array(
				'problem_descr' => 'CollectionType->read()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_CollectionType

This, shockingly 🙂 , performs a SELECT against the table, returning all rows contained in it.

6) Returning to step #3 (above), we need to add the read_one_user() function to the Collection model (“/opt/lampp/htdocs/hector/application/classes/Model/Collection.php”); do so within the “SQL functions” section:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Collection extends Model_Database {
.
.
.

	// SQL functions

	public function read_one_user($data)
	{			  
			  
		/*
		/	Purpose: Read all collections for one (specified) user
		/
		/	Parms:
		/		Array containing:
		/			'user_id' >> User ID to return collections for
		/
		/	Returns:
		/		Array containing:
		/			'Rows' >> Array containing specified user's collection rows
		/		AND
		/			'Rows_Affected' >> Number of collection rows in 'Rows'
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/

		$return_arr = array();
		
		try 
		{		
			$return_arr['Rows'] = 
				DB::select(
					'collection_id'
					,array($this->table_name.'.descr', 'name')
					,array('collection_type.descr', 'type')
				)
					->from($this->table_name)
					->join('collection_type')
						->on($this->table_name.'.coll_type_id', '=', 'collection_type.coll_type_id')
					->where('user_id', '=', $data['user_id'])
					->and_where('to_delete', '=', 0)
					->execute()
			;	
			$return_arr['Rows_Affected'] = count($return_arr['Rows']);
		}
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error,
			//		redirect user to sign in page, and display error message
			$error_data = array(
				'problem_descr' => 'Collection->read_one_user()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Collection

This function, as the comments in the code itself suggest, reads all collections (and the description for each collection’s type) for the specified user.

Again returning to step #3 (above), we encounter, for the first time, a new view that will be present throughout all pages in the application going forward. It renders the navigation bar at the top of each page for all signed in users.

7) Create “Navbar.php” and save it in the views directory (“/opt/lampp/htdocs/hector/application/views”):

Navbar_Views_11_1

It is heavily dependent on Foundation constructs for its appearance and functionality. This is largely user module-specific, meaning it is quite application-agnostic. Only the Hector-specific bits of code are highlighted below but you do need to add the entire block of code to make things work!:

				<nav class="top-bar" data-topbar>
					<ul class="title-area">
						<li class="name">
<?php						
	echo '<h1>'.HTML::anchor('Main', 'Hector').'</h1>';
?>							
						</li>
						<li class="toggle-topbar menu-icon"><a href="#"><span>Menu</span></a></li>
					</ul>

					<section class="top-bar-section">
						<ul class="left">
							<li class="divider"></li>
							<li class="has-dropdown">
								<a href="#">Collections</a>
								<ul class="dropdown">
<?php			
	echo '<li>'.HTML::anchor('Collection/view', 'Mine').'</li>';
?>									
								</ul>
							</li>
							<li class="divider"></li>
						</ul>
						<ul class="right">
							<li class="divider hide-for-small"></li>
							<li class="has-dropdown">
<?php
	echo HTML::anchor('#', Session::instance()->get('screen_name').' <b class="caret"></b>', array('role' => 'button', 'class' => 'dropdown-toggle', 'data-toggle' => 'dropdown')) 
		.'<ul class="dropdown">'
		.'<li>'.HTML::anchor('User/sign_out', 'Sign Out').'</li>'
		.'</ul>'
	;
?>	
							</li>
						</ul>
					</section>
				</nav>

You should also note that there are several anchor tags that route to actions contained in the User controller. We’ve previously defined all of them, save one – the sign_out action:

				<nav class="top-bar" data-topbar>
					<ul class="title-area">
						<li class="name">
<?php						
	echo '<h1>'.HTML::anchor('Main', 'Hector').'</h1>';
?>							
						</li>
						<li class="toggle-topbar menu-icon"><a href="#"><span>Menu</span></a></li>
					</ul>

					<section class="top-bar-section">
						<ul class="left">
							<li class="divider"></li>
							<li class="has-dropdown">
								<a href="#">Collections</a>
								<ul class="dropdown">
<?php			
	echo '<li>'.HTML::anchor('Collection/view', 'Mine').'</li>';
?>									
								</ul>
							</li>
							<li class="divider"></li>
						</ul>
						<ul class="right">
							<li class="divider hide-for-small"></li>
							<li class="has-dropdown">
<?php
	echo HTML::anchor('#', Session::instance()->get('screen_name').' <b class="caret"></b>', array('role' => 'button', 'class' => 'dropdown-toggle', 'data-toggle' => 'dropdown')) 
		.'<ul class="dropdown">'
		.'<li>'.HTML::anchor('User/sign_out', 'Sign Out').'</li>'
		.'</ul>'
	;
?>	
							</li>
						</ul>
					</section>
				</nav>

8) Go ahead and add it to the User controller (“/opt/lampp/htdocs/hector/application/classes/Controller/User.php”) now:

<?php defined('SYSPATH') or die('No direct script access.');
 
class Controller_User extends Controller_Template_Sitepage {
.
.
.
	
	public function action_sign_out()
	{		
		
		/*
		/	Purpose: Action to handle "sign out" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/
		
		// Unset all Session vars
		$session = Session::instance();
		$session_vars = $session->as_array();
		foreach ($session_vars as $session_var_key => $session_var)
		{
			$session->delete($session_var_key); 
		}
		
		// Redirect to main page
		$this->redirect('User/sign_in');	
		
	}

.
.
.	
} // End User

All this does is clear out all Session variables and redirect the user to the main Sign In page.

9) Returning to the main task at hand (working toward adding a new collection), now that we’ve retrieved all the data to be displayed by the View Collections page, it is time to return to step #3 (above) to actually generate the page. This requires creation of the ViewBuilder_CollectionView class (name it “CollectionView.php” and add it to the “/opt/lampp/htdocs/hector/application/classes/ViewBuilder” directory):

CollectionView_ViewBuilder_11_2

Define the new class and, while we’re at it, insert the add_collection() function:

<?php defined('SYSPATH') or die('No direct script access.');

class ViewBuilder_CollectionView {

	public function add_collection($accordion_open, $collection_type_sel_arr, $post_or_null, $errors)
	{

		/*
		/	Purpose: Build Add Collection view
		/
		/	Parms:
		/		'accordion_open' >> Boolean indicating whether view's accordion should default to open
		/		'collection_type_sel_arr' >> Array containing selectable collection types
		/		'post_or_null' >> Submitted form data or NULL
		/		'errors' >> Form validation errors or NULL
		/
		/	Returns:
		/		View to render
		*/			
		      
		$content =
 
			// Include message block
			View::factory('Message_Block')
      	
			.View::factory('Accordion_Start')
				->set('accordion_id', 'add_collection')

			// Render Add Collection view
			.View::factory('Add_Collection')
				->set('collection_type_sel_arr', $collection_type_sel_arr)
				->set('accordion_group_open', $accordion_open)
				->set('page_description', 'New Collection')
				->set('post', $post_or_null)
				->set('errors', $errors)
      		
			.View::factory('Accordion_End')
		;
     
		return $content;
		
	}
	
} // End ViewBuilder_CollectionView

10) We’ve already coded the Message Block view (back in this post), but this is the first time we’ve encountered Accordion Start. It will be used heavily as we proceed on with handling collections and items. Let’s create it (name it “Accordion_Start.php” and place it in the “/opt/lampp/htdocs/hector/application/views” directory):

Accordion_Start_Views_11_3

Add this single line:

	<dl class="accordion" data-accordion>

Yes, that really is all there is to it! 🙂

11) Let’s add the corresponding Accordion End view, while we’re at it (name it “Accordion_End.php” and also place it in the “/opt/lampp/htdocs/hector/application/views” directory):

Accordion_End_Views_11_4

Add this single tag:

	</dl>

12) Next up is the view that provides the user with a form for adding a new collection. Create a new view (name it “Add_Collection.php” and again place it in the “/opt/lampp/htdocs/hector/application/views” directory):

Add_Collection_Views_11_5

Add the following code and markup:

		<dd class="accordion-navigation">
			<a href="#add-collection">Add Collection</a>
<?php	
	echo '<div id="add-collection" class="content '.($accordion_group_open == TRUE ? ' active' : '').'">';
?>
				<fieldset>
<?php
	echo '<legend>'.$page_description.'</legend>';
	echo Form::open('Collection/add', array('class' => 'custom'));
?>	
					<div class="row">
						<div class="large-3 columns end">
<?php
	echo Form::label('selCollectionType', 'Type:');
	echo Form::select('selCollectionType', $collection_type_sel_arr, $post ? $post['selCollectionType'] : 0);
?>	
						</div>
					</div>
					<div class="row">
						<div class="large-8 columns end">
<?php	
	echo Form::label('txtCollectionName', 'Name:');
	echo Form::input('txtCollectionName', $post ? $post['txtCollectionName']: '', array('id' => 'id_txtCollectionName', 'size' => '35', 'maxlength' => '75', 'required' => true));
	if (array_key_exists('txtCollectionName', $errors))
	{
		echo '<small class="error">'.$errors['txtCollectionName'].'</small>';
	}
?>
						</div>
					</div>
					<br />
<?php
	echo Form::button('btnAdd', 'Add', array('class' => 'small button radius submit'));
	echo Form::close();
?>
				</fieldset>
			</div>
		</dd>

As you can see from this code, the form is defined with input fields for collection type and collection name, and like our previous user module forms, is equipped to retain values and display errors if any are generated on form submission.

13) Jumping back to step #3 (above), we need to add the second part of the View Collections page. This will be accomplished via the addition of a collections() function to the ViewBuilder_CollectionView class (“/opt/lampp/htdocs/hector/application/classes/ViewBuilder/CollectionView.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class ViewBuilder_CollectionView {
.
.
.

	public function collections($accordion_open, $collection_arr)
	{
		
		/*
		/	Purpose: Build Collections view
		/
		/	Parms:
		/		'accordion_open' >> Boolean indicating whether view's accordion should default to open
		/		'collection_arr' >> Array containing user's collections
		/
		/	Returns:
		/		View to render
		*/			
			  		
		$content = 
		
			View::factory('Accordion_Start')
				->set('accordion_id', 'collections')

			// Render Collections view
			.View::factory('Collections')
				->set('collection_arr', $collection_arr['Rows'])
				->set('accordion_group_open', $accordion_open)
				->set('page_description', 'Collections')
  
			.View::factory('Accordion_End')
		;
		
		return $content;
		
	}

.
.
.
} // End ViewBuilder_CollectionView

This will call the views responsible for actually displaying the list of the current user’s collections, contained inside an accordion.

14) Finally, we need to create the view that handles displaying the bulk of the content on this page (once we have actually created collections, that is). Add this now (name it “Collections.php” and again place it in the “/opt/lampp/htdocs/hector/application/views” directory):

Collections_Views_11_6

Add the following code and markup:

		<dd class="accordion-navigation">
			<a href="#collections">View Collections</a>
<?php	
	echo '<div id="collections" class="content ' . ($accordion_group_open == TRUE ? ' active' : '') . '">';
?>
				<fieldset>
<?php
	echo '<legend>'.$page_description.'</legend>';
	if (count($collection_arr))
	{
		foreach ($collection_arr as $collection)
		{	
?>
					<div class="row">
						<div class="small-6 columns">
<?php
			echo HTML::anchor('Collection/edit/'.$collection['collection_id'], $collection['name']);			
?>
						</div>
						<div class="small-6 columns">
<?php   					
			echo '('.$collection['type'].')';
?>					  
						</div>
					</div>   					
<?php
		}
	}
	else
	{
		echo 'No collections found.  Click on "Add Collection" above to get started!<br />';	 
	}
?>
				</fieldset>
			</div>
		</dd>

This displays the accordion open or closed, based on the state value it is passed, and displays one collection per row (or a message indicating that no collections exist). You’ll also notice that each collection’s name is displayed as a clickable link to allow editing of that collection. We’ll add the code to handle that action later.

For now, let’s add code to process a populated, and submitted, Add Collection form. Take a look at the form action we added in step #12 (above):

		<dd class="accordion-navigation">
			<a href="#add-collection">Add Collection</a>
<?php	
	echo '<div id="add-collection" class="content '.($accordion_group_open == TRUE ? ' active' : '').'">';
?>
				<fieldset>
<?php
	echo '<legend>'.$page_description.'</legend>';
	echo Form::open('Collection/add', array('class' => 'custom'));
?>	
					<div class="row">
						<div class="large-3 columns end">
<?php
	echo Form::label('selCollectionType', 'Type:');
	echo Form::select('selCollectionType', $collection_type_sel_arr, $post ? $post['selCollectionType'] : 0);
?>	
						</div>
					</div>
					<div class="row">
						<div class="large-8 columns end">
<?php	
	echo Form::label('txtCollectionName', 'Name:');
	echo Form::input('txtCollectionName', $post ? $post['txtCollectionName']: '', array('id' => 'id_txtCollectionName', 'size' => '35', 'maxlength' => '75', 'required' => true));
	if (array_key_exists('txtCollectionName', $errors))
	{
		echo '<small class="error">'.$errors['txtCollectionName'].'</small>';
	}
?>
						</div>
					</div>
					<br />
<?php
	echo Form::button('btnAdd', 'Add', array('class' => 'small button radius submit'));
	echo Form::close();
?>
				</fieldset>
			</div>
		</dd>

15) Once submitted, the form gets routed to the add action in the Collection controller, so re-visit the Collection controller (“/opt/lampp/htdocs/hector/application/classes/Controller/Collection.php”) and supplement it with the add action:

class Controller_Collection extends Controller_Template_Sitepage {
.
.
.

	public function action_add()
	{		
		
		/*
		/	Purpose: Action to handle "add collection" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/

		// Grab $_POST array from submitted form
		$post = $this->request->post();
		
		if (isset($post['btnAdd']))
		{
		
			// Add button was pressed
			
			$collection_created_arr = Model::factory('Collection')->create_collection($post);
			if ($collection_created_arr['Success'])
			{

				// Send message indicating successful collection creation
				Session::instance()->set('message', "You've added new collection \"".$collection_created_arr['Clean_Post_Data']['txtCollectionName']."\"!");				

				// Redirect to view action 
				$this->redirect('Collection/view');
			}
			else
			{

				// Validation failed, display form with custom error text
				$accordions_open = array(
					'add_collection' => TRUE
					,'collections' => FALSE
				);
				$this->template->content = ViewBuilder::factory('Page')->view_collections($accordions_open, $this->template, $post, $collection_created_arr['Errors']);
			}
		} // if (isset($post['btnAdd']))
		else	// This "else" is required so that failed validation (above) won't auto redirect!
		{
		
			// Redirect to view action	// Bad URL
			$this->redirect('Collection/view');
		}	
		
	}

.
.
.
} // End Collection

By now, the structure of this function should be starting to look familiar. 🙂 As we’ve done with all actions that create new data of any sort, we call upon a model to do all of the real work, routing the user according to the success or the failure of the results of the model’s work.

16) Go back to the Collection model (“/opt/lampp/htdocs/hector/application/classes/Model/Collection.php”) and add the create_collection() function under the “Business logic functions” section:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Collection extends Model_Database {
.
.
.

	// Business logic functions

	public function create_collection($post)
	{
			  
		/*
		/	Purpose: Create new collection for current user
		/
		/	Parms:
		/		'post' >> Submitted form data from New collection view
		/
		/	Returns:
		/		Array containing:
		/			'Clean_Post_Data' >> Array containing cleaned form data
		/			'Success' >> Boolean indicating success/failure of collection creation
		/		[AND]
		/			'Errors' >> Array containing form validation errors
		*/

		$return_arr = array(
			'Success' => FALSE		  
		);
			  
		// Validate Add Collection form entry
		$validation_results_arr = Validation_Collection::form_fields($post);
		if ($validation_results_arr['Success'])
		{
				  			
			// Validation clean, create new collection							
			$collection_data = array(
				'user_id' => Session::instance()->get('user_id')
				,'descr' => $validation_results_arr['Clean_Post_Data']['txtCollectionName']
				,'coll_type_id' => $validation_results_arr['Clean_Post_Data']['selCollectionType']
			);		

			// Perform collection creation
			if ($this->create($collection_data))
			{
				$return_arr['Clean_Post_Data'] = $validation_results_arr['Clean_Post_Data'];
				
				// All is well!
				$return_arr['Success'] = TRUE;
			}
		}
		else
		{
			$return_arr['Errors'] = $validation_results_arr['Errors'];
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Collection

This function also follows the pattern we’ve been employing to process create data requests. We start off by calling a validation class function to determine if the data is clean. If it is, the “create” function then calls the appropriate SQL function to perform the database transaction and returns a success flag back to the calling code. If it is NOT clean, errors generated by the validation class are passed back to the “create” function and then back to the caller (along with an error flag).

17) Once again, we require a new Validation class. Add it now (name it “Collection.php” and save it in the “/opt/lampp/htdocs/hector/application/classes/Validation” directory):

Collection_Validation_11_7

Go ahead and add the form_fields() static function to it as well:

<?php defined('SYSPATH') or die('No direct script access.');

class Validation_Collection extends Validation {

	public static function form_fields($post)
	{
			
		/*
		/	Purpose: Validate collection form submitted data
		/
		/	Parms:
		/		Array containing:
		/			'txtCollectionName' >> New collection's name
		/
		/	Returns:
		/		Array containing:
		/			'Clean_Post_Data' >> Array containing cleaned form data
		/			'Success' >> Boolean indicating success/failure of validation
		/		[AND]
		/			'Errors' >> Array containing form validation errors
		*/
		
		$return_arr = array(
			'Success' => FALSE		  
		);
			  
		$clean_post_data = Validation::factory($post)
					
			// Collection name must be non-blank
			->rule('txtCollectionName', 'not_empty')
				
			// Collection name must not be more than 75 chars long
			->rule('txtCollectionName', 'max_length', array(':value', 75))
			
		;
		
		if ($clean_post_data->check())
		{
			$return_arr['Clean_Post_Data'] = $clean_post_data;
			
			$return_arr['Success'] = TRUE;
		}
		else
		{	
			$return_arr['Errors'] = $clean_post_data->errors('form_errors');
		}
		
		return $return_arr;
		
	}
	
} // End Validation_Collection

This validation function doesn’t use any rules we haven’t already seen so I won’t belabor them!

18) But astute viewers WILL notice that we have introduced some new errors in the new Validation_Collection class. Why don’t we add them to our custom form errors file (“/opt/lampp/htdocs/hector/application/messages/form_errors.php”) right now:

<?php defined('SYSPATH') or die('No direct script access.');

return array
(
		  
	// Account creation error messages
	'emlEmailAddr' => array(
		'email' => "Oops! That doesn't look like a valid email address."
		,'max_length' => 'Please keep your email address under 100 chars.'
		,'Model_AppUser::unique_emailaddr' => 'Oops! That email address is already being used.'             
	)
	,'pwdConfPassword' => array(
		'matches' => 'Please be sure your new password and confirmed new password match.'
	)
	,'pwdPswd' => array(
		'Model_AppUser::valid_curr_pswd' => 'The Username (or Email Address) and password combo you entered is invalid.'
		,'not_empty' => 'Oops! Looks like you forgot to enter your password.'
	)
	,'txtUsername' => array(
		'max_length' => 'Please keep your Username under 100 chars.'
		,'Model_AppUser::non_blank' => "C'mon now..you can't have a blank Username."             
		,'Model_AppUser::unique_username' => 'Oops! That Username is already being used.'
		,'not_empty' => 'Oops! You forgot to enter your Username.'
	)
	,'txtUsernameOrEmailAddr' => array(
		'Model_AppUser::non_blank' => "C'mon now..you can't have a blank Username or Email Address."              
		,'not_empty' => 'Username or Email Address must be entered.'
	)
	
	// Collection creation error messages           
	,'txtCollectionName' => array (
		'max_length' => 'Collection name must be 75 chars or less.'
		,'not_empty' => "Collection name can't be blank!"
	)
);

19) Returning to step #16 (above), the create() function, as expected, inserts a new row into the collection table. Let’s add it now, to the Collection model (“/opt/lampp/htdocs/hector/application/classes/Model/Collection.php”), under the “SQL functions” section header:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Collection extends Model_Database {
.
.
.

	// SQL functions

	public function create($data)
	{

		/*
		/	Purpose: Create new collection row in database
		/
		/	Parms:
		/		Array containing:
		/			'user_id' >> User ID of creator of new collection
		/			'descr' >> New collection's description
		/			'coll_type_id' >> New collection's type ID
		/		
		/	Returns:
		/		Array containing:
		/			'Row_Created_ID' >> New collection's collection_id
		/		AND
		/			'Rows_Affected' >> Number of rows (1) inserted into table
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/

		$return_arr = array();
		
		try
		{
			list($return_arr['Row_Created_ID'], $return_arr['Rows_Affected']) = 
				DB::insert($this->table_name)
					->columns($this->table_cols)
					->values(array(
						$data['user_id']
						,$data['descr']
						,$data['coll_type_id']
				
						// Default to_delete to FALSE
						,FALSE
				
						// Default last_update_dttm to current datetime
						,date('Y-m-d H:i:s') 
					))
					->execute()
			;
		}
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error
			$error_data = array(
				'problem_descr' => 'Collection->create()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_Collection

20) Once you’ve done all of this, you should now be able to sign in (from “http://localhost/hector”) and see the “No collections” message:

Hector_No_Collections_11_8

21) Expand the Add Collection accordion, select the collection type, and enter a name for your new collection:

Hector_Add_Collection_11_9

22) Press the “Add” button. When the View Collections page refreshes, you should see your newly-added collection smiling back at you:

Hector_One_Collection_11_10

Just don’t click it or you’ll get an error. 🙂 Until our next chapter, that is..

Notes:

  1. The initial status is obviously just that – the user can open and close any accordion on the page, as they please, once the page is rendered.
00

Meet Hector

Now that we’ve finally made it through all of the generic user module stuff, and can create an account, validate it, sign in with it, and reset a forgotten password, let’s build an application that you might actually want to use once you have an account for it!

Hector is a simple way to keep track of things you collect. It has a simple set of functionality: you define collections and add/remove items to/from them. Each item is categorized as something you have, something you want, or something you do not have AND do not want.

There is a lot of opportunity to expand this functionality, as I’m sure you can imagine, but for the purposes of this tutorial, we’ll keep it simple. And, oh by the way, rather than being a contrived example, as you so often find in tutorials, I built Hector because I need it to track things I really do collect. 🙂

Since the extent of our database schema has so far been limited to storing only user data, clearly, we’ll need to make some additions to it.

1) First, we’ll define a table to store collections:

CREATE TABLE collection
(
collection_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT
,user_id INT(10) UNSIGNED NOT NULL
,descr VARCHAR(75) NOT NULL 
,coll_type_id INT(10) UNSIGNED NOT NULL
,to_delete BOOLEAN NOT NULL DEFAULT 0
,last_update_dttm TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00'
,PRIMARY KEY (collection_id)
,INDEX (user_id)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

“collection_id” will be the primary key of this table. It will uniquely identify each collection. Uniqueness is ensured by the AUTO_INCREMENT property which instructs the database to handle generation and assignment of this column upon insert of a new row.

The “user_id” should be familiar to you from back in our second post in the tutorial, where we did our initial database work. It is here to tie a collection to its owner.

“descr” is the free-form description (or name) of the collection.

“coll_type_id” contains a numerical ID which will correspond to a matching value in the collection type table we’ll define below.

“to_delete” is a binary value indicating whether this collection has been soft-deleted. This concept will be discussed later, once we get to implementation of delete functionality.

“last_update_dttm” is just what it sounds like.

We’ll frequently retrieve collection rows for a specified user_id, hence the creation of an index on this column (for performance reasons).

For this table, because we want the ability to rollback transactions1, we need to define the table as InnoDB. Though not critical to do so, we will define all of our tables for Hector as InnoDB2.

2) Build the collection table in the hector database via phpMyAdmin (or whatever SQL tool you’re using)

3) Next, we need a corresponding model to handle SQL operations and business logic related to collections. Create that model now (name it “Collection.php” and save it in the “/opt/lampp/htdocs/hector/application/classes/Model” directory):

Collection_Model_10_1

Add the constructor:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Collection extends Model_Database {
		  
	public function __construct()  
	{
		
		/*
		/	Purpose: Create collection model
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/

		parent::__construct();
		$this->table_name = 'collection';  
		
		// NOTE: Deliberately omit collection_id so it is auto-assigned
		$this->table_cols = array(
			'user_id'
			,'descr'
			,'coll_type_id'
			,'to_delete'
			,'last_update_dttm'
		);		
		
	}

	
	
	// SQL functions

	
	
	// Business logic functions



	// Static functions	

	
	
} // End Model_Collection

When this class is instantiated, the constructor makes the table_name and table_cols variables available to the functions within the class. This is common among all of the models we will define in subsequent steps.

4) Next up, here’s the collection type table we mentioned above:

CREATE TABLE collection_type
(
coll_type_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT
,descr VARCHAR(75) NOT NULL
,PRIMARY KEY (coll_type_id)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

As you can see, this is a very basic table, with both columns being columns we’ve discussed before.

5) Go ahead and build the collection_type table in the hector database with your SQL tool

6) You could get fancy and add a controller, view, and model functions to define and maintain collection types, but for now, we’ll just manually insert a couple of values; I’ve chosen “Comic Books” and “LEGO Sets” but you could make these whatever you actually collect!

INSERT INTO collection_type VALUES (NULL, 'Comic Books');
INSERT INTO collection_type VALUES (NULL, 'LEGO Sets');

7) Create the model to accompany this new table (name it “CollectionType.php” and save it in the “/opt/lampp/htdocs/hector/application/classes/Model” directory):

CollectionType_Model_10_2

Define the constructor:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_CollectionType extends Model_Database {
		  
	public function __construct()  
	{

		/*
		/	Purpose: Create collection type model
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/
		
		parent::__construct();
		$this->table_name = 'collection_type';  
		
		// NOTE: Deliberately omit coll_type_id so it is auto-assigned
		$this->table_cols = array(
			'descr'
		);
		
	}



	// Static functions	
	
	
	
} // End Model_CollectionType

8) Logically, we’ll also need a table to store items belonging to a collection:

CREATE TABLE item
(
collection_id INT(10) UNSIGNED NOT NULL
,seq INT(10) UNSIGNED NOT NULL
,descr VARCHAR(75) NOT NULL 
,status CHAR(1) NOT NULL -- D = Don't want, H = Have, W = Want
,to_delete BOOLEAN NOT NULL DEFAULT 0
,last_update_dttm TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00'
,PRIMARY KEY (collection_id,seq)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

This table, not surprisingly, has many similarities to the collection table.

“collection_id” will combine with a numerical “seq” (sequence) column to form the primary key of this table. It will uniquely identify each row in a collection while tying it back to its respective collection. The “seq” value determines the order of the items in a collection.

The “descr” is again a free-form description (or name) of, this time, the item.

“status” contains a single character code which will correspond to a matching value in the item status table we’ll define below.

“to_delete” is a binary value indicating whether this item has been soft-deleted.

“last_update_dttm” gets stamped when a change is made to an item.

9) Build the item table in the hector database with your SQL tool

10) Create the corresponding model for this new table (name it “Item.php” and save it in the “/opt/lampp/htdocs/hector/application/classes/Model” directory):

Item_Model_10_3

Add the constructor:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_Item extends Model_Database {
		  
	public function __construct()  
	{

		/*
		/	Purpose: Create item model
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/
		
		parent::__construct();
		$this->table_name = 'item';  
		
		$this->table_cols = array(
			'collection_id'
			,'seq'
			,'descr'
			,'status'
			,'to_delete'
			,'last_update_dttm'
		);
		
	}

	
	
	// SQL functions
	

	
	// Business logic functions



	// Static functions	
	
	
	
} // End Model_Item

11) To round things out, we’ll need an item status table:

CREATE TABLE item_status
(
item_status_cd CHAR(1) NOT NULL
,descr VARCHAR(75) NOT NULL
,seq INT(10) UNSIGNED NOT NULL
,PRIMARY KEY (item_status_cd)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

This table is another basic one. The primary key column, “item_status_cd”, is a single character in length and is a character type in order to lend itself to “clever”, admin-defined, acronyms. You’ll see a few of these, shortly, below.

“descr” is just a description. And the numerical “seq” value determines the order of the item status codes when used in a dropdown (HTML “SELECT”) list, for example.

12) Build the item status table in the hector database with your SQL tool

13) Create the model to accompany this new table (name it “ItemStatus.php” and save it in the “/opt/lampp/htdocs/hector/application/classes/Model” directory):

ItemStatus_Model_10_4

Define the constructor:

<?php defined('SYSPATH') or die('No direct script access.');

class Model_ItemStatus extends Model_Database {
		  
	public function __construct()  
	{

		/*
		/	Purpose: Create item status model
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/

		parent::__construct();
		$this->table_name = 'item_status';  
		
		$this->table_cols = array(
			'item_status_cd'
			,'descr'
			,'seq'
		);
		
	}
	
	
	
	// SQL functions
	

	
	// Business logic functions



	// Static functions	
	
	
	
} // End Model_ItemStatus

14) You could go hog-wild and add a controller, view, and model functions to define and maintain item statuses, but for simplicity’s sake, we’ll just manually insert values, for the three status options we detailed in our summary of functionality above, into the table. That is, a row apiece for: a user has an item, doesn’t have it but wants it, or doesn’t have it AND doesn’t want it.

INSERT INTO item_status VALUES ('H', 'Have', 1);
INSERT INTO item_status VALUES ('W', 'Want', 2);
INSERT INTO item_status VALUES ('D', 'Don''t want', 3);

Dust off those comic books, we’ll start adding collections next time!

Notes:

  1. The rationale behind this will become clear once we address physical deletion of collections, and the items they contain, in a later installment.
  2. This is a hot topic of discussion, as you might guess!
00

Sometimes You Need To Reset

Happy Wednesday! Here’s a mid-week morsel to move us through what little of the user module remains to be coded and explained.

With our password reset email sent, we now need to add the code to handle processing the user-specific URL contained in the email. We already have a routing defined to handle the format of the URL (thanks to work completed in our account creation confirmation post) and we have the Validate controller defined too.

1) We do need to add the reset_acct action to it (“/opt/lampp/htdocs/hector/application/classes/Controller/Validate.php”), though:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Validate extends Controller_Template_Sitepage {
.
.
.
	
	public function action_reset_acct()
	{		
		
		/*
		/	Purpose: Action to handle "reset password validation" requests via
		/					call to set_password function
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/
		
		$this->set_password('Validate/reset_acct_prcs');
		
	}	

.
.
.
} // End Validate

This relies on the set_password() function we’ve already defined in the Validate controller. The parameter we pass it (“Validate/reset_acct_prcs”) is the $submit_handler.

2) Revisiting the definition of set_password(), notice that the $submit_handler parameter we pass it gets passed on to the form that it ultimately renders (on the Set Password page):

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Validate extends Controller_Template_Sitepage {
.
.
.

	private function set_password($submit_handler)
	{
			  
		/*
		/	Purpose: Validate authcode for new account or password reset request
		/					and then render "set password" page
		/
		/	Parms:
		/		'submit_handler' >> Controller/action to set as action for form on rendered page 
		/
		/	Returns:
		/		[NONE]
		*/

		// Grab email addr from URL
		$email_addr = $this->request->param('email_addr');
		
		// Grab authcode from URL
		$authcode = $this->request->param('authcode');
	
		$authcode_valid_arr = Model::factory('AppUser')->validate_authcode($email_addr, $authcode);
		if ($authcode_valid_arr['Success'])
		{

			// Render Set Password page
			$this->template->content = ViewBuilder::factory('Page')->set_password($submit_handler, $this->template, NULL, $errors = array());				  
		}
		else
		{
				  
			// Failed validation
				
			// Redirect to invalid_url action
			$this->redirect('Validate/invalid_url');				  
		}
		
	}

.
.
.
} // End Validate

3) This means we need to define a new action, reset_acct_prcs, in the Validate controller (“/opt/lampp/htdocs/hector/application/classes/Controller/Validate.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Validate extends Controller_Template_Sitepage {
.
.
.
	
	public function action_reset_acct_prcs()
	{		
		
		/*
		/	Purpose: Action to handle processing of "reset password validation" requests via
		/					call to set_password_prcs function
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/
		
		$this->set_password_prcs('Validate/reset_acct_prcs');
		
	}
	
.
.
.
} // End Validate

Since this function merely calls the existing set_password_prcs() function (passing a specific action for use by the form), we’re done – everything down this processing path re-uses generic functions we’ve defined already!

If you’d like to review the processing flow that occurs from here, I encourage you to do so by looking through the code you’ve written while working your way through this tutorial. Or, of course, you could also look back through the previous posts for the full commentary explaining each step.

4) To prove we really are done with the user module, go ahead and click on the link contained in the password reset email you received after walking through last post’s steps. If you are confronted with a page resembling this one:

Hector_Invalid_URL_9_1

Remember that, unless you are going through the steps in this post immediately following completion of the prior post, your password reset link will have already expired. So re-generate it (via the “Forgot password?” link) on the Sign In page (“http://localhost/hector”).

5) Once you click on a VALID password reset link, you should be sent to this familiar page:

Hector_Set_Password_9_2

6) Enter your new password and confirm it (or alternatively, leave it blank1). You should receive the familiar message informing you that you’re now successfully signed in:

Hector_Signed_In_9_3

And there you have it – just for planning ahead, we get to go home early today. Nice work everyone!! 🙂

Next time, at long last, an introduction to a helpful friend…

Notes:

  1. Identically to the situation we discussed in the post where we completed new user creation, you may choose to use your password reset link as a single-use password.
00

Feeling Forgetful?

Let’s process a password reset request for an existing user.

1) One last time, we’ll revisit the Sign in Page form (“/opt/lampp/htdocs/hector/application/views/Sign_In.php”), this time focusing in on the “Forgot password?” link toward the bottom of the page:

<?php
	echo Form::open('User/sign_in_prcs', array('name' => 'frmSignIn', 'id' => 'id_frmSignIn'));
?>
				<fieldset>
<?php								
	echo '<legend>'.$page_description.'</legend>';
	echo Form::label('txtUsernameOrEmailAddr', 'Username or Email Address:');
	echo Form::input('txtUsernameOrEmailAddr', $post ? $post['txtUsernameOrEmailAddr']: '', array('id' => 'id_txtUsernameOrEmailAddr', 'size' => '35', 'maxlength' => '100', 'required' => true));
	if (array_key_exists('txtUsernameOrEmailAddr', $errors))
	{
		echo '<small class="error">'.$errors['txtUsernameOrEmailAddr'].'</small>';
	}
	echo Form::label('pwdPswd', 'Password:');
	echo Form::password('pwdPswd', NULL, array('id' => 'id_pwdPswd', 'size' => '35', 'maxlength' => '200', 'required' => true));
	if (array_key_exists('pwdPswd', $errors))
	{
		echo '<small class="error">'.$errors['pwdPswd'].'</small>';
	}
	echo '<br />';
	echo Form::button('btnSignIn', 'Sign In', array('class' => 'small button radius submit'));
	echo '<br />'.HTML::anchor('User/forgot_pswd', 'Forgot password?');
?>
				</fieldset>
<?php
	echo '<br />'.HTML::anchor('User/create_acct', 'Create an account');
	echo Form::close();
?>

As will surprise no one, at this point in our adventure, clicking this link will route the user to the User controller’s forgot_pswd action.

2) So, let’s pop over to the User controller (“/opt/lampp/htdocs/hector/application/classes/Controller/User.php”) and add the forgot_pswd action now:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_User extends Controller_Template_Sitepage {
.
.
.

	public function action_forgot_pswd()
	{		
		
		/*
		/	Purpose: Action to handle "forgot password" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/
		
		// Define arrays
		$errors = array();
				
		// Render Forgot Password page
		$this->template->content = ViewBuilder::factory('Page')->forgot_pswd($this->template, NULL, $errors = array());
		
	}	

.
.
.
} // End User

Since this follows our standard procedure for (thin) controllers, it merely calls the forgot_pswd() function in the ViewBuilder_Page class.

3) We’ll add this function to the ViewBuilder_Page class (“/opt/lampp/htdocs/hector/application/classes/ViewBuilder/Page.php”) here:

<?php defined('SYSPATH') or die('No direct script access.');

class ViewBuilder_Page {
.
.
.

	public function forgot_pswd($template, $post_or_null, $errors)
	{
			  
		/*
		/	Purpose: Build Forgot Password page
		/
		/	Parms:
		/		'template' >> Array containing page header attributes
		/		'post_or_null' >> Submitted form data or NULL
		/		'errors' >> Form validation errors or NULL
		/
		/	Returns:
		/		Page to render
		*/		

		// Render Forgot Password page
		$template->page_description = 'Reset Password';
		$template->title .= $template->page_description;
		$template->set_focus = 'frmForgotPswd.txtUsernameOrEmailAddr';
		$content = ViewBuilder::factory('AppUserView')->forgot_pswd($post_or_null, $errors);
		
		return $content;
		
	}	

.
.
.
} // End ViewBuilder_Page

Again, as we’ve done repeatedly before, this relies on a new function in the ViewBuilder_AppUserView class to move us a step closer to actually rendering the view.

4) That function is the new forgot_pswd() function, which we’ll now add to the ViewBuilder_AppUserView class (“/opt/lampp/htdocs/hector/application/classes/ViewBuilder/AppUserView.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class ViewBuilder_AppUserView {
.
.
.

	public function forgot_pswd($post_or_null, $errors)
	{
			  
		/*
		/	Purpose: Build Forgot Password view
		/
		/	Parms:
		/		'post_or_null' >> Submitted form data or NULL
		/		'errors' >> Form validation errors or NULL
		/
		/	Returns:
		/		View to render
		*/			
		
		$content = 
		
			// Render Forgot Pswd view		
			View::factory('Forgot_Pswd')
				->set('page_description', 'Reset Password')
				->set('post', $post_or_null)
				->set('errors', $errors)
		;
      
		return $content;
		
	}

.
.
.
} // End ViewBuilder_AppUserView

Here we call the view code responsible for actually rendering the Password Reset Request page.

5) Naturally, we need the View code too. Create the “Forgot_Pswd” view (name it “Forgot_Pswd.php” and save it in the “/opt/lampp/htdocs/hector/application/views” directory):

Forgot_Pswd_Views_8_1

Add the following code and markup:

<?php
	echo Form::open('User/forgot_pswd_prcs', array('name' => 'frmForgotPswd', 'id' => 'id_frmForgotPswd', 'class' => 'custom'));
?>
				<fieldset>
<?php								
	echo '<legend>'.$page_description.'</legend>';
	echo '<h5>Please enter your Username or Email Address</h5>';
	echo Form::label('txtUsernameOrEmailAddr', 'Username or Email Address:');
	echo Form::input('txtUsernameOrEmailAddr', $post ? $post['txtUsernameOrEmailAddr']: '', array('id' => 'id_txtUsernameOrEmailAddr', 'size' => '35', 'maxlength' => '100', 'required' => true));
	if (array_key_exists('txtUsernameOrEmailAddr', $errors))
	{
		echo '<small class="error">'.$errors['txtUsernameOrEmailAddr'].'</small>';
	}
	echo '<br />';
	echo Form::button('btnResetPswd', 'Reset Password', array('class' => 'small button radius submit'));
?>
				</fieldset>
<?php
	echo Form::close();
?>

6) Open your browser, navigate to “http://localhost/hector”, and hit the “Forgot password?” link. You should see this:

Hector_Reset_Password_8_2

The Password Reset Request page has no surprises. It simply asks the user for their username or email address. Upon submission of the form, program execution will be passed to the User controller’s forgot_pswd_prcs action.

7) Now it’s time to add the forgot_pswd_prcs action to the User controller (“/opt/lampp/htdocs/hector/application/classes/Controller/User.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_User extends Controller_Template_Sitepage {
.
.
.

	public function action_forgot_pswd_prcs()
	{		
		
		/*
		/	Purpose: Action to handle processing of "forgot password" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/
		
		// Grab $_POST array from submitted form
		$post = $this->request->post();
  		
		if (isset($post['btnResetPswd']))
		{

			// Reset Password button was pressed
			
			$password_reset_arr = Model::factory('AppUser')->reset_user_password($post);
			if ($password_reset_arr['Success'])
			{

				// Successful password reset
		
				// Redirect to forgot pswd done action for cleaner resultant URL
				$this->redirect('User/forgot_pswd_done');					  
			}
			else
			{
			
				// Validation failed, render Forgot Password page with custom error text displayed on appropriate form field(s)
				$this->template->content = ViewBuilder::factory('Page')->forgot_pswd($this->template, $post, $password_reset_arr['Errors']);					  
			}				  
		}
		else	// This "else" is required so that failed validation (above) won't auto redirect!
		{
		
			// Bad URL, redirect to create_acct action
			$this->redirect('User/create_acct');
		}
		
	}

.
.
.
} // End User

If this function is being called and the “Reset Password” button was NOT pressed, this is considered a bad URL and the visitor is routed to the Create Account page. Otherwise, the $_POST array (contained in the $post variable assigned here), is passed to the AppUser model’s reset_user_password() function. If this returns a successful value, process flow is redirected to the forgot_pswd_done action. Otherwise, validation has failed and we’ll render the Password Reset Request page, with the user’s inputted values retained and relevant error messages displayed.

8) Open up the AppUser model (“/opt/lampp/htdocs/hector/application/classes/Model/AppUser.php”) and add the reset_user_password() function in the “Business logic functions” section:

class Model_AppUser extends Model_Database {		  
.
.
.

	// Business logic functions
          
	public function reset_user_password($post)
	{

		/*
		/	Purpose: Reset user's password
		/
		/	Parms:
		/		'post' >> Submitted form data from Forgot Password view
		/
		/	Returns:
		/		Array containing:
		/			'Clean_Post_Data' >> Array containing cleaned form data
		/			'Success' >> Boolean indicating success/failure of password reset request
		/		[AND]
		/			'Errors' >> Array containing form validation errors
		*/
			  
		$return_arr = array(
			'Success' => FALSE		  
		);

		// Validate Forgot Password form entry
		$validation_results_arr = Validation_AppUser::forgot_password($post);
		if ($validation_results_arr['Success'])
		{
				  			
			// Validation clean, reset user's password
			
			$app_user_data = array(
				'username_or_email_addr' => $validation_results_arr['Clean_Post_Data']['txtUsernameOrEmailAddr']		  					  
			);
			$app_user_rowset = $this->read_user_data_flexible($app_user_data);
			if ($app_user_rowset['Rows_Affected'] == 1)
			{
					  
				// This is a valid username  
				
				// Isolate row of data
				$app_user_arr = $app_user_rowset['Rows'][0];
				
				// Generate 32-digit random, alphanumeric authcode         
				$authcode = Text::random('alnum', 32);
				
				// Update DB with new authcode, new authcode_gen_dttm
				$app_user_data['user_id'] = $app_user_arr['user_id'];
				$app_user_data['authcode'] = Bonafide::instance()->hash($authcode);
				
				$authcode_updated_arr = $this->update_user_authcode($app_user_data);
				if ($authcode_updated_arr['Rows_Affected'] == 1)
				{

					// Send email to user with account reset validation URL
					$tran_email_data = array(
						'recipient' => $app_user_arr['email_addr']
						,'sender' => 'pswd_reset.sender'
						,'sender_descr' => 'pswd_reset.sender_descr'
						,'subject' => 'pswd_reset.subject'
						,'body' => 'pswd_reset.body'
						,'placeholder_arr' => array(
							'USERNAME' => $app_user_arr['username']
							,'EMAIL_ADDR' => $app_user_arr['email_addr']
							,'AUTHCODE' => $authcode
						)
					);
					TransactionEmail::send_email($tran_email_data);
					
					// All is well!
					$return_arr['Success'] = TRUE;
				}
				else
				{
						 
					// Failed user update - pretend all is well to discourage hackers
					$return_arr['Success'] = TRUE;
				}
			}
			else
			{
					  
				// User reset attempt for nonexistent user - pretend all is well to discourage hackers
				$return_arr['Success'] = TRUE;
			}
		}
		else
		{
			$return_arr['Errors'] = $validation_results_arr['Errors'];
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_AppUser

Validation of the user-entered form data is performed. If the data is NOT clean, the specific errors generated during validation are returned. Otherwise, the user’s app_user row is read from the database, a random authcode is generated (and hashed) and the user’s row is then updated with both this new authcode and a new authcode generated datetime. If the update is successful, a password reset email (containing the individual URL the user must click to reset their password) is generated and then sent.

9) Let’s define the new validation function, adding the forgot_password() static function to the Validation_AppUser class (“/opt/lampp/htdocs/hector/application/classes/Validation/AppUser.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class Validation_AppUser extends Validation {
.
.
.

	public static function forgot_password($post)
	{
			  
		/*
		/	Purpose: Validate Forgot Password form submitted data
		/
		/	Parms:
		/		Array containing:
		/			'txtUsernameOrEmailAddr' >> User's username or email address 
		/
		/	Returns:
		/		Array containing:
		/			'Clean_Post_Data' >> Array containing cleaned form data
		/			'Success' >> Boolean indicating success/failure of validation
		/		[AND]
		/			'Errors' >> Array containing form validation errors
		*/

		$return_arr = array(
			'Success' => FALSE		  
		);
			  
		$clean_post_data = Validation::factory($post)			
			
			// Username or email address must be non-empty
			->rule('txtUsernameOrEmailAddr', 'not_empty')			

			// Username or email address must not be entirely whitespace chars
			->rule('txtUsernameOrEmailAddr', 'Model_AppUser::non_blank')
		;
		
		if ($clean_post_data->check())
		{
			$return_arr['Clean_Post_Data'] = $clean_post_data;
			
			$return_arr['Success'] = TRUE;
		}
		else
		{	
			$return_arr['Errors'] = $clean_post_data->errors('form_errors');
		}
		
		return $return_arr;
		
	}	

.
.
.
} // End Validation_AppUser

This function relies both on a Kohana-provided validation function (to ensure the username/email address field is not blank) and on the existing custom validation function, non_blank(), we added earlier to the AppUser model. If there are problems, the specific error(s) are passed back to the caller.

10) Now, let’s add the update_user_authcode() function, called by the code we added in step #8 (above), to the “SQL functions” section in the AppUser model (“/opt/lampp/htdocs/hector/application/classes/Model/AppUser.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class Model_AppUser extends Model_Database {
.
.
.

	// SQL functions

	public function update_user_authcode($data)
	{
			  
		/*
		/	Purpose: Set specific user's authcode and authcode gen timestamp
		/
		/	Parms:
		/		Array containing:
		/			'authcode' >> User's (hashed) authcode
		/			'user_id' >> User's ID
		/		
		/	Returns:
		/		Array containing:
		/			'Rows_Affected' >> Number of user rows affected by UPDATE
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/
			  
		$return_arr = array();
		
		try
		{
			$return_arr['Rows_Affected'] = 
				DB::update($this->table_name)
					->set(array(
						'authcode' => $data['authcode']
						,'authcode_gen_dttm' => date('Y-m-d H:i:s')
					))
					->where('user_id', '=', $data['user_id'])
					->execute()
			;
		}
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error
			$error_data = array(
				'problem_descr' => 'AppUser->update_user_authcode()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_AppUser

Nothing magical to explain here! 🙂

11) Next, we need to add a new array key, “pswd_reset”, and its array of keys and values, to the “email_text.php” messages file (“/opt/lampp/htdocs/hector/application/messages/email_text.php”):

<?php defined('SYSPATH') or die('No direct script access.');

return array
(
	'acct_validation' => array(
		'sender' => '[system_email_address]'
		,'sender_descr' => 'Hector'
		,'subject' => 'Hector - Please complete your account creation request within 60 minutes.'
		,'body' => "Hi [USERNAME], <br /><br />Thank you for signing up for a Hector account! <br /><br />In order to complete your account activation, please click on the following link within the next 60 minutes (after which this URL will expire).&nbsp;&nbsp;Doing so will prompt you to choose a password for your account: <br /><br />https://[site]/Validate/new_acct/[EMAIL_ADDR]/[AUTHCODE] <br /><br />Thanks, <br />&nbsp;&nbsp;&nbsp;The Organizer"
	), 
	'pswd_reset' => array(
		'sender' => '[system_email_address]'
		,'sender_descr' => 'Hector'
		,'subject' => 'Hector - Please complete your password reset request within 60 minutes.'
		,'body' => "Hi [USERNAME], <br /><br />Sorry to hear you're unable to sign in to Hector! <br /><br />In order to confirm that you control this email address, please click on the following link within the next 60 minutes (after which this URL will expire).&nbsp;&nbsp;Doing so will prompt you to choose a new password for your account: <br /><br />http://[site]/Validate/reset_acct/[EMAIL_ADDR]/[AUTHCODE] <br /><br />If you did NOT request a password reset, please ignore this email and continue to sign in with your current password. <br /><br />Thanks, <br />&nbsp;&nbsp;&nbsp;The Organizer"
	),
	'system_error_report' => array(
		'sender' => '[system_email_address]'
		,'recipient' => '[admin_email_address]'
		,'sender_descr' => 'Hector'
		,'subject' => 'Hector - ALERT - system problem!'
		,'body' => "Hey there, <br /><br />Sorry to be the bearer of bad news, but the following error occurred:<br /><br />User: [USER_ID]<br />At: [DTTM]<br /><br />Controller: [CONTROLLER]<br />Action: [ACTION]<br /><br />[PROBLEM_DESCR]<br /><br />Please look into this!<br /><br />Thanks,<br />&nbsp;&nbsp;&nbsp;The Innards"
	)
);

12) Back in step #7 (above), within the User controller’s forgot_pswd_prcs action, the user, upon successfully completing their password reset request, is redirected to the forgot_pswd_done action. Let’s add that to the User controller (“/opt/lampp/htdocs/hector/application/classes/Controller/User.php”) now:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_User extends Controller_Template_Sitepage {
.
.
.

	public function action_forgot_pswd_done()
	{		
		
		/*
		/	Purpose: Action to handle post-processing of "forgot password" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/
		
		// Render Informational Message page instructing user to check email for URL to click to complete acct reset process
		$this->template->content = ViewBuilder::factory('Page')->informational_message($this->template, 'Account Confirmation Required', Kohana::message('misc_text', 'pswd_reset_requested'));
		
	}
 
.
.
.
} // End User

13) It looks like we also have a new message to add to “misc_text”, so let’s open it (“/opt/lampp/htdocs/hector/application/messages/misc_text.php”) and add the “pswd_reset_requested” key and value as shown:

<?php defined('SYSPATH') or die('No direct script access.');

return array
(
	'acct_creation_requested' => 'Your account creation request has been submitted.  <br /><br />Please check your email for a note from [system_email_address] and be sure to click the enclosed single-use, expiring link within 60 minutes from submitting your account creation request.'
	,'bad_validation_url' => 'Sorry - it appears that the URL you are using is invalid.  <br /><br />Please double-check it and try again.'
	,'pswd_reset_requested' => 'Your password reset request has been submitted.  <br /><br />Please check your email for a note from [system_email_address] and be sure to click the enclosed single-use, expiring link within 60 minutes from submitting your password reset request.'
);            

14) You should now be able to open your browser, navigate to “http://localhost/hector”, hit the “Forgot password?” link, enter your username or email address, press the “Reset Password” button, and see something similar1:

Hector_Account_Confirmation_Required_8_3

Alrighty then, we have a working password reset request process – sweet!

Of course, naysayers will point out that clicking on the URL sent in the password reset email will presently fail, but that will be taken care of in the next post. I promise!

Notes:

  1. Or just return to your browser, if you still have the page up from step #6 (above), populate the text field with your username or email address, and press the “Reset Password” button.
00