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
