Turbulences Tutorial - Chapter 3 : More models : validation, model relationships and access control
In this chapter we will :
- Create a new module. Let's create an Administrator module. It will use the existing (provided by turbulences) user and address modules.
- An administrator has all the same atributes as a basic user of the website
- An administrator has specific attributes (level of administration and an address)
- Manage all errors (administrator, user and address) at the same time
- Manage attribute access for models and for actions of controllers
Generation of the admin module
- On the command line, use the generator tool (for more info, see the section in Chapter 1):
script/generate_module administrator
Create the table
We will add a table for specific attributes of an administrator, including all foreign keys for user and address. Just create a schema SQL script in the module directory in modules/administrator/DB/administrator.schema.sql.
DROP TABLE IF EXISTS `administrator`; CREATE TABLE `administrator` ( `id` INT( 10 ) NOT NULL AUTO_INCREMENT , `user_id` INT( 10 ) NOT NULL , `level` ENUM( 'super-admin', 'moyen-admin', 'petit-admin' ) NOT NULL , `address_id` INT( 10 ) NOT NULL , PRIMARY KEY ( `id` ) , INDEX ( `user_id` , `level` ) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
To install the new table, you must execute this new schema script. You can reinstall the whole application by running the install script. Please note that this will re-create all the application's tables and you will thus lose any data in them. If you want to avoid this, simply execute it, either manualy like this :
mysql <NAME OF YOUR DATABASE> < modules/administrator/DB/administrator.schema.sql
or with your favourite SQL admin tool (like PHPMyAdmin). An easier way will be implemented in an upcoming release.
Declare the relationships between objects
* Now we have to declare the relationships between Administrator, User and Address. We do this by declaring the joins in Administrator.php
var $_joins = array(
'user' => array('User', array('user_id' => 'id')),
'address' => array('Address', array('address_id' => 'id'))
);
We will use this in our code to get the User of an administrator, very simply with : $admin->user. The same works for address. We call this a direct join. // TODO describe reverse and indirect joins too
Create the controller
* Now let's have a look at the controller
// this function return the administrator object
// if $_GET['id'] or $_POST['id'] is given, load the object
// else return an empty object
public function getAdministratorFromRequest()
{
if(isset($this->__get['id']) && is_numeric($this->__get['id']))
$id = $this->__get['id'];
elseif(isset($_POST['id']) && is_numeric($_POST['id']))
$id = $_POST['id'];
else
$id = NULL;
if(!is_null($id))
$u = Administrator::getByPKey('Administrator',$id);
else
$u = new Administrator();
return $u;
}
// action for general editing
public function action_edit()
{
return $this->_edit('edit');
}
// action for access_level editing
public function action_set_level()
{
return $this->_edit('edit');
}
// function to save the object for all action
private function _edit($view)
{
$v = new AdministratorView();
// get the object from request
$obj = $this->getAdministratorFromRequest();
// if request type is post
if($this->isPost())
{
// hydrate object (and dependants)
$obj->setFromArray($_POST);
// try to save the object (and dependants)
$obj->saveDependantObjects();
// if no arrors , redirect to action_list
if(isset($obj->id) && !$obj->hasError())
return $this->action_list();
}
// render the asked view and pass the object
$view = 'html_'.$view;
return $v->$view($obj);
}
// action for listing
public function action_list()
{
$v = new AdministratorView();
$objs = Administrator::getFor('Administrator');
return $v->html_list($objs);
}
We can now list all admininstrators by going to /administrator/list
Some CRUD
* Write views, they simply pass the entire objects or array of object
public function _edit($obj)
{
$this->assign('administrator',$obj);
}
public function _list($objs)
{
$this->assign('administrators',$objs);
}
* The template edit.tpl -- (the widgets are smarty plugins)
<!-- Widget to print all errors on our object, including dependant objects -->
{wdgt_print_object_error object=$administrator}
<form name="" method="post" action="" enctype="multipart/form-data">
<!-- keep the object id in form -->
<input type="hidden" name="id" value="{$administrator->id}" />
<!-- hardcode the user type -->
<input type="hidden" name="user[user_type]" value="administrator" />
<fieldset>
<legend>
<div class="title">
<div class="title_right"></div>
Administrateur
</div>
</legend>
<!-- following widget print all inputs, regarding the access_level defined in models (cf following points) -->
{wdgt_input object=$administrator->user field='lastname' label='Nom'}
{wdgt_input object=$administrator->user field='firstname' label='Prénom'}
{wdgt_input object=$administrator->user field='email' label='Email'}
{wdgt_select object=$administrator field='level' label='Niveau d\'admistration'}
</fieldset>
<fieldset>
<legend>
<div class="title">
<div class="title_right"></div>
Adresse
</div>
</legend>
<!-- following widget print all inputs, regarding the access_level defined in models (cf following points) -->
{wdgt_input field=address object=$administrator->address prefix_name=address label='Adresse'}
{wdgt_input field=city object=$administrator->address prefix_name=address label='Ville'}
{wdgt_input field=zip_code object=$administrator->address prefix_name=address label='Code postal'}
</fieldset>
<hr />
<ul>
<li class="reset"><input type="reset" value="Annuler" /></li>
<li class="submit"><input type="submit" value="Enregistrer les modifications" /></li>
</ul>
</form>
* Now, we have an insert/edit application for the administrator module.
Access control
* Let's manage access for fields in our model. We will use a simple level-based Access Control List (ACL) security model.
First the controller must propagate the _access_level to the object (_access_level is the type of the user in session)
private function _edit($view)
{
$v = new AdministratorView();
$obj = $this->getAdministratorFromRequest();
// HERE
$obj->setAccessLevel($this->_access_level); // transmission of the access level to the object
if($this->isPost())
{
$obj->setFromArray($_POST);
$obj->saveDependantObjects();
if(isset($obj->id) && !$obj->hasError())
return $this->action_list();
}
$view = 'html_'.$view;
return $v->$view($obj);
}
Then, we have to define rules for reading an editing fields If your are in _access_level admin, you'll have to define two entries in the following array : admin_read, and admin_write Here we see that only an administrator can set the level of an administrator, the default access_level cannot set it
var $_access_limit = array(
'default_read' => array(),
'default_write' => array('level'),
'admin_read' => array(),
'admin_write' => array(),
);
* Then you can define access_level for all actions of the controller. If the current acces_level cannot access the action, it will be redirected to ErrorController::access_not_allowed
var $_allowed_access_level = array(
'edit' => array('admin','default'),
'set_level' => array('admin'),
);
public function action_set_level()
{
return $this->_edit('edit');
}
You can now test the access control : only admins should be able to access /administratot/set_level
