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