wiki:Manual/Tutorial_3

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