22

Write your own PHP MVC Framework (Part 2)

If you haven’t already, please read Part 1 of the PHP MVC tutorial. A lot of changes have been incorporated in this part. Unlike the last tutorial, I have not pasted the code directly here, and I suggest you download the framework before reading on. In the example, we have implemented a simple e-commerce website consisting of categories, subcategories, products and tags (for products). The example will help you understand the various relationships between the tables and most of the new functionality provided by this part of the framework.

What’s New

config/inflection.php
The inflection configuration file enables us to use irregular words i.e. words which do not have a standard plural name. This file is used in conjunction with library/inflection.class.php

config/routing.php
The routing configuration file enables us to specify default controller and action. We can also specify custom redirects using regular expressions. Currently I have specified only one redirect i.e. http://localhost/framework/admin/categories/view will become http://localhost/framework/admin/categories_view where admin is the controller and categories_view is the action. This will enable us to create an administration centre with pretty URLs. You can specify others as per your requirements. For more information on how to use regular expressions have a look at one of these tutorials.

library/cache.class.php
The cache class is in its infancy. Currently there are two simple functions- set and get. The data is stored in a flat text-file in the cache directory. Currently only the describe function of the SQLQuery class uses this cache function.

library/html.class.php
The HTML class is used to aid the template class. It allows you to use a few standard functions for creating links, adding javascript and css. I have also added a function to convert links to tinyurls. This class can be used only in the views e.g. $html->includeJs(‘generic.js’);

library/inflection.class.php
In the previous part, plural of words were created by adding only “s” to the word. However, for a more full-fledged version, we now use the inflection class originally created by Sho Kuwamoto with slight modifications. If you have a look at the class, it makes use of simple regular expressions. It would be nice to add a cache function to this class in future.

library/template.class.php
I have added a new variable called doNotRenderHeader which will enable you to not output headers for a particular action. This can be used in AJAX calls when you do not want to return the headers. It has to be called by the controller e.g. $this->doNotRenderHeader = 1;

library/vanillacontroller.class.php & vanillamodel.class.php
Pretty much unchanged, but I have added a prefix vanilla to them to emphasize on its simplicity ๐Ÿ˜‰

library/sqlquery.class.php
The SQLQuery class is the heart of this framework. This class will enable you to use your tables as objects.

Let us understand the easy functions first – save() and delete()

The save() function must be used from the controller. The save() function can have two options- if an id is set, then it will update the entry; if it is not set, then it will create a new entry. For example let us consider the categories class i.e. application/controllers/categoriescontroller.php. We can have a function like the one below.

function new() {
   $this->Category->id = $_POST['id'];
   $this->Category->name = $_POST['name'];
   $this->Category->save();
}

If $this->Category->id = null; then it will create a new record in the categories table.

The delete() function enables you to delete a record from the table. A sample code could be as below. Although you should use POST instead of GET queries for deleting records. As a rule idempotent operations should use GET. Idempotent operations are those which do not change the state of the database (i.e. do not update/delete/insert rows or modify the database in anyway)

function delete($categoryId) {
   $this->Category->id = $categoryId;
   $this->Category->delete();
}

Now let us look at the search() function. I know it is a bit intimidating at first. But it is pretty straight forward after we break it down. If you are unaware of database table relationships (1:1, 1:Many and Many:Many) have a quick look here. During database design, we use the following convention:

1:1 Relationship
For a one is to one relationship, suppose we have two tables students and mentors and each student hasOne mentor, then in the students table we will have a field called ‘mentor_id’ which will store the id of the mentor from the mentors table.

1:Many Relationship
For a one is to many relationship, suppose we have two tables parents and children and each parent hasMany children, then in the children table we will have a field called ‘parent_id’ which will store the id of the parent from the parents table.

Many:Many Relationship
For a many is to many relationship, suppose we have two tables students and teachers and each student hasManyAndBelongsToMany teachers, then we create a new table called students_teachers with three fields: id, student_id and teacher_id. The naming convention for this table is alphabetical. i.e. if our tables are cars and animals, then the table should be named animals_cars and not cars_animals.

Now once we have created our database as per these conventions, we must tell our framework about their existence. Let us have a look at models/product.php.

class Product extends VanillaModel {
	var $hasOne = array('Category' => 'Category');
	var $hasManyAndBelongsToMany = array('Tag' => 'Tag');
}

The first Category is the alias and the second Category is the actual model. In most cases both will be the same. Let us consider the models/category.php where they are not.

<?php

class Category extends VanillaModel {
		var $hasMany = array('Product' => 'Product');
		var $hasOne = array('Parent' => 'Category');

}

Here each category has a parent category, thus our alias is Parent while model is Category. Thus we will have a field called parent_id in the categories table. To clearly understand these relationships, I suggest you create a couple of tables and test them out for yourself. In order to see the output, use code similar to the following in your controller.

function view() {
   $this->Category->id = 1;
   $this->Category->showHasOne();
   $this->Category->showHasMany();
   $this->Category->showHMABTM();
   $data = $this->Category->search();
   print_r($data);
}

Now let us try and understand the search() function. If there are no relationships, then the function simply does a select * from tableName (tableName is same as controllerName). We can influence this statement, by using the following commands:

where(‘fieldName’,’value’) => Appends WHERE ‘fieldName’ = ‘value’
like(‘fieldName’,’value’) => Appends WHERE ‘fieldName’ LIKE ‘%value%’
setPage(‘pageNumber’) => Enables pagination and display only results for the set page number
setLimit(‘fieldName’,’value’) => Allows you to modify the number of results per page if pageNumber is set. Its default value is the one set in config.php.
orderBy(‘fieldName’,’Order’) => Appends ORDER BY ‘fieldName’ ASC/DESC
id = X => Will display only a single result of the row matching the id

Now let us consider when showHasOne() function has be called, then for each hasOne relationship, a LEFT JOIN is done (see line 91-99).

Now if showHasMany() function has been called, then for each result returned by the above query and for each hasMany relationship, it will find all those records in the second table which match the current result’s id (see line 150). Then it will push all those results in the same array. For example, if teachers hasMany students, then $this->Teacher->showHasMany() will search for teacher_id in the students table.

Finally if showHMABTM() function has been called, then for each result returned by the first query and for each hasManyAndBelongsToMany relationship, it will find all those records which match the current result’s id (see line 200-201). For example, if teachers hasManyAndBelongsToMany students, then $this->Teacher->showHMABTM() will search for teacher_id in students_teachers table.

On line 236, if id is set, then it will return a single result (and not an array), else it will return an array. The function then calls the clear() function which resets all the variables (line 368-380).

Now let us consider an example to enable pagination on products. The code in the controllers/productscontroller.php should look something similar to the following.

function page ($pageNumber = 1) {
  $this->Product->setPage($pageNumber);
  $this->Product->setLimit('10');
  $products = $this->Product->search();
  $totalPages = $this->Product->totalPages();
  $this->set('totalPages',$totalPages);
  $this->set('products',$products);
  $this->set('currentPageNumber',$pageNumber);
}

Now our corresponding view i.e. views/products/page.php will be something like below.

<?php foreach ($products as $product):?>
<div>
<?php echo $product&#91;'Product'&#93;&#91;'name'&#93;?>
</div>
<?php endforeach?>

<?php for ($i = 1; $i <= $totalPages; $i++):?>
<div>
<?php if ($i == $currentPageNumber):?>
<?php echo $currentPageNumber?>
<?php else: ?>
<?php echo $html->link($i,'products/page/'.$i)?>
<?php endif?>
</div>
<?php endfor?>

Thus with a few lines of code we have enabled pagination on our products page with pretty URLs!
(/products/page/1, products/page/2 …)

If you look at the totalPages() function on line 384-397, you will see that it takes the existing query and strips of the LIMIT condition and returns the count. This count/limit gives us the totalPages.

Now suppose that we are in the categories controller and we want to implement a query on the products table. One way to implement this is using a custom query i.e. $this->Category->custom(‘select * from products where ….’);

Alternatively, we can use the performAction function (line 49-57 in shared.php) to call an action of another controller. An example call in categoriescontroller.php would be something like below.

function view($categoryId = null, $categoryName = null) {
  $categories = performAction('products','findProducts',array($categoryId,$categoryName));
}

And the corresponding code in productscontroller.php should be as below.

function findProducts ($categoryId = null, $categoryName = null) {
  $this->Product->where('category_id',$categoryId);
  $this->Product->orderBy('name');
  return $this->Product->search();
}

The above more or less sums up all the functionality of the SQLQuery class. You can easily extend this as per your requirements e.g. to cache results, add conditions to hasMany, hasOne, hasMABTM queries also etc.

I have also laid the foundation for implementing user registration functionality by specifying a beforeAction and afterAction function which will be executed for each controller action. This will enable us to call a function which checks whether the user cookie is set and is a valid user.

One more feature that I have implemented is to have an administration centre. For this purpose we create a dummy model called models/admin.php and set a variable called $abstract = true. This will tell our VanillaModel to not look for a corresponding table in the database. As stated before I have created a routing for admin/X/Y as admin/X_Y. Thus we can have URLs like admin/categories/delete/15 which will actually call the categories_delete(15) function in the controllers/admincontroller.php file.

Where do we go from here?

I hope this tutorial is not too complicated. I have tried to break it down as much as possible. A couple of functions that we need to implement include -> redirectAction which will be similar to performAction, but instead redirect. performAction can only return results. redirectAction will perform only the redirected action and display that corresponding view only. This will be helpful during user authentication. If the user is not logged in, then we call a function like -> redirectAction(‘user’,’login’,array($returnUrl));

Various classes are in their infancy like the cache class, HTML class etc. These classes can be expanded to add more functionality.

More scripts can be added like dbimport, clearcache etc. can be added. Currently there is only one script dbexport.php which dumps the entire database. Classes for managing sessions, cookies etc. also need to be developed.

Download link here

Download Framework (Part 2) (ZIP File)

Make sure you edit config/config.php and add your database details. Point your browser to localhost/FRAMEWORKDIR to see output. Also before executing, import the database that I have created in the db folder.

Suggestions?

Do let me know your suggestions on how we can improve this code/any particular features you would like to see implemented/any other unrelated topic you would like a tutorial on.

Spread The Word

If you like what you are reading, then please help spread the word by re-tweeting, blogging and dzone upvoting or use the ShareThis button below. Thank you.


2000 Words
179946 Views