Skip to content
Barry O'Donovan edited this page Nov 19, 2012 · 1 revision

Doctrine2 Frontend refers to OSS_Controller_Action_Trait_Doctrine2Frontend which is a trait that adds CRUD (create, update, delete and list) functionality to a controller for a database object. This is also known as scaffolding.

It's important to note that we build our applications with Zend, Doctrine2, Smarty (and JQuery, Datatables, etc) and so this trait will be tightly coupled with those. This trait also assumes / requires that you have used the Doctrine2, Smarty, Message traits and, if you are copying and pasting view templates from some of our projects, that certain files are available in the view folder. This documentation assumes this.

Configuration

Firstly, all controllers using this trait as assumed to be enabled unless it has been explicitly disabled in the application.ini by (where the controller name is SiteController for example):

frontend.disabled.site = true;

In any controller using this trait, a _feInit() method is required which configures the controller and, for example, allows you to set what is displayed for different levels of user privileges. The primary purpose of this function is to define the anonymous object _feParams (using an object ensures that the view gets a reference to the object and not a copy of a static array at a point in time):

protected function _feInit()
{
    $this->view->feParams = $this->_feParams = (object)[
        'entity'         => '\\Entities\\Site',  // the ORM entity object that CRUD operations will affect
        'form'           => 'OSS_Form_User',     // the form object to instantiate for add / edit operations
        'pagetitle'      => 'Users',             // page title
    
        'readonly'       => false,               // default. If true, add / edit / delete will be disabled

        'allowedActions' => [],                  // see Restricting Access section below

        'titleSingular'  => 'User',              // a singular title reference for the object
        'nameSingular'   => 'a user',            // a singular narrative reference for the object
    
        'defaultAction'  => 'list',              // OPTIONAL; defaults to 'list'
    
        'listColumns' => [                    // what columns to display in the list view
            'id' => [ 'title' => 'UID', 'display' => false ],
            'username'   => 'Username',
            'email'      => 'Email'
        ],
    
        'listOrderBy'    => 'username',         // how to order columns
        'listOrderByDir' => 'ASC',              // direction of order columns
    ];

    // you can then override some of the above for different user privileges (for example)
    switch( $this->getUser()->getPrivs() )
    {
        case \Entities\User::AUTH_SUPERUSER:
            $this->_feParams->pagetitle = 'Customer Users';

            $this->_feParams->listColumns = [
                'id' => [ 'title' => 'UID', 'display' => false ],

                'customer'  => [
                    'title'      => 'Customer',
                    'type'       => self::$FE_COL_TYPES[ 'HAS_ONE' ],
                    'controller' => 'customer',
                    'action'     => 'overview',
                    'idField'    => 'custid'
                ],
                'username'      => 'Userame',
                'email'         => 'Email'
            ];
            break;
            
        case \Entities\User::AUTH_CUSTADMIN:
            break;
            
        default:
            $this->redirectAndEnsureDie( 'error/insufficient-permissions' );
    }
             
    // display the same information in the single object view as the list of objects
    $this->_feParams->viewColumns = $this->_feParams->listColumns;
}

Templates

All the required templates should be in the view folder under frontend/ and named after the action. For example frontend/list.phtml. You can override this for a specific controller (say the site controller) by creating a template site/list.phtml. As you'll see below, frontend allows for a lot of custom content which you should just place in a controller specific folder unless you want it to apply to all frontend controllers.

The List Action

The list action displays a tabular list of objects from the database.

The list action (at time of writing) is really simple:

public function listAction()
{
    $this->listPreamble();

    $this->view->data = $this->listGetData();
    
    $this->view->listPreamble    = $this->_resolveTemplate( 'list-preamble.phtml'  );
    $this->view->listPostamble   = $this->_resolveTemplate( 'list-postamble.phtml' );
    $this->view->listRowMenu     = $this->_resolveTemplate( 'list-row-menu.phtml' );
    $this->view->listToolbar     = $this->_resolveTemplate( 'list-toolbar.phtml' );
    $this->view->listScript      = $this->_resolveTemplate( 'js/list.js' );
    $this->view->listAddonScript = $this->_resolveTemplate( 'js/list-addon.js' );
    $this->_display( 'list.phtml' );
}

You can override the listPreamble() hook to do any pre-list code / tasks.

All you are really required to do is define the function listGetData() which is used also for viewing a single object (in which case it takes an id parameter). For the list action, this function needs to return an array of rows for display in tabular format. For example (using the site controller):

protected function listGetData( $id = null )
{
    $qb = $this->getD2EM()->createQueryBuilder()
            ->select( 's.id as id, s.name as name, s.shortname as shortname,
                c.id as customerId, c.name as customer' )
            ->from( '\\Entities\\Site', 's' )
            ->leftJoin( 's.Customer', 'c' ); 
    
    if( $this->getUser()->getType() == \Entities\User::TYPE_CUSTADMIN )
    {
        $qb->where( 's.Customer = ?1' )
            ->setParameter( 1, $this->getUser()->getCustomer() );
    }
     
    if( isset( $this->_feParams->listOrderBy ) )
        $qb->orderBy( $this->_feParams->listOrderBy, isset( $this->_feParams->listOrderByDir ) ? $this->_feParams->listOrderByDir : 'ASC' );

    if( $id !== null )
        $qb->andWhere( 's.id = ?2' )->setParameter( 2, $id );
                    
    return $qb->getQuery()->getResult();
}

Note the following:

  • the above is aware of whether we are doing a list of objects or a view of a single object;
  • the name of the columns in the result must match those as defined in _feInit();
  • you can add logic such as user privilege checking.

The code then looks for common or overridden content to:

  • display before the table (list-preamble.phtml);
  • display after the table (list-postamble.phtml);
  • per row tools (such as edit, delete) (list-row-menu.phtml);
  • a toolbar for the page header (list-toolbar.phtml);
  • custom JavaScript to replace the default (js/list.js).
  • additional JavaScript (js/list-addon.js).

The View Action

The view action follows from the list action above and also uses listGetData( $id ) where $id is the id of the object to display and is typically passed in in the URL such as /$controller/view/id/$id.

Similar also to the list action, the view action is quite simple but offers some additional hooks:

public function viewAction()
{
    $this->viewPrepareData();

    $this->view->viewPreamble  = $this->_resolveTemplate( 'view-preamble.phtml'  );
    $this->view->viewPostamble = $this->_resolveTemplate( 'view-postamble.phtml' );
    $this->view->viewToolbar   = $this->_resolveTemplate( 'view-toolbar.phtml' );
    $this->view->viewScript    = $this->_resolveTemplate( 'js/view.js' );
    $this->_display( 'view.phtml' );
}

The sequence is:

  1. viewPrepareData() can be overridden in your own controller but the default version simply takes the object ID to view from the request ($this->_getParam( 'id' )) and then calls preView( viewGetData( $id ) ).
  2. viewGetData( $id ) can also be overridden but in its default form it calls listGetData( $id ) and ensures there is a result (if not, it redirects to the default page with a not found error). This function must return a object for display.
  3. preView() does nothing by default but allows you to override it to mangle the object prior to display if you so desire.

The code then looks for common or overridden content to:

  • display before the object view (view-preamble.phtml);
  • display after the object view (view-postamble.phtml);
  • a toolbar for the page header (view-toolbar.phtml);
  • custom JavaScript (js/view.js).

The default isn't overly useful except to display more columns than you'd typically display in a table format. The default uses a <dl>..</dl> format. Defining your own $controller/view.phtml will allow you to do much more useful things.

The Delete Action

The delete action deletes an object from the database. There are a lot of available hooks you can add to add / change functionality and the sequence of events in the deleteAction() function is:

  1. First, call deletePreamble() to perform any set up tasks
  2. Get the $id of the object to delete via deleteResolveId() which you can override. This, by default, returns the ID parameter from the request.
  3. Load the object via loadObject( $id )` which:
    1. allows a translation / alteration of the $id via a preLoadObject( $id ) hook;
    2. load the object from the database (and redirect to default controller action with error if it does not exist);
    3. allow alteration of the object via a postLoadObject( $object ) hook.
  4. Call a possible preDelete( $object ) hook which must return true to continue (it should redirect itself to abort the delete);
  5. removes the object from the database and flushes the database;
  6. calls a possible postDelete( $object ) hook which should return with true (and which by default calls the more generic postFlush() hook);
  7. redirect to the default controller action after logging the deletion and adding a success message unless you override the deleteDestinationOnSuccess() function to add a custom message and issue your own redirect()/redirectAndEnsureDie().

Adding and Editing Actions

This is the more complex of all the actions. Also not that edit is an alias for add and in fact performs an internal _forward() to addAction(). The view and hooks will all receive a parameter $isEdit to indicate if we are editing or adding an object.

Note that this action is also more complex as it requires a Zend_Form object to add / edit objects.

There are a lot of available hooks you can add to add / change functionality and the sequence of events in the addAction() function is:

    1. First, call addPreamble() to perform any set up tasks
  1. Call the editResolveId() function which can be overridden. This is where the add / edit decision is taken. By default, editResolveId() returns $this->_getParam( 'id', false ). I.e. if there is an id parameter, we assume it is the id of an object to edit, otherwise we assume we are adding an object.
  2. If editing, we:
    1. we call loadObject( $id ) just like in delete step (2) above (please refer there for the available hooks);
    2. we instantiate a form object by calling $this->getForm( $isEdit, $object )
    3. assign the object's existing values to the forms elements.
  3. if adding, we instantiate a form via $this->getForm( $isEdit, $object ) which will use any defined default values.
  4. the $this->getForm( $isEdit, $object ) call:
    1. instantiates the form object as defined by _feInit();
    2. calls the formPostProcess( $form, $object, $isEdit, $options = null, $cancelLocation = null ) hook which is a better option to override rather than getForm(). By default, this does nothing but this is where you might, for example, remove elements for less privileged users / add elements for more privileged users.
  5. the addPrepare( $form, $object, $isEdit) hook is now called just before we process a possible POST / submission and here we can change / alter the form or object.
  6. if the form has been submitted($this->getRequest()->isPost()), we:
    1. call the addPreValidate( $form, $object, $isEdit ) hook which should return true to continue processing the form;
    2. calls the form's isValid() mathod and continues if true;
    3. calls the addProcessForm( $form, $object, $isEdit ) hook which can be overridden but by default:
      • calls the addPostValidate( $form, $object, $isEdit ) hook which must return true to continue;
      • assigns the form's values to the object (NB: ensure there are not form elements named the same as object elements that you do not mean to have assigned);
      • calls the addPreFlush( $form, $object, $isEdit ) hook just before the object is added / changes are persisted to the database which must return true to continue;
      • calls the addPostFlush( $form, $object, $isEdit ) hook just after flushing the database which must return true to continue (and which by default calls the more generic postFlush() hook);
      • logs the action and returns;
    4. if addProcessForm() returns true, a success message is added to the session and a redirect to the default controller action is issued unless you override the addDestinationOnSuccess() function to add a custom message and issue your own redirect()/redirectAndEnsureDie().

When displaying the add / edit form, some standard templates are checked:

The code then looks for common or overridden content to:

  • display before the form (add-preamble.phtml);
  • display after the form (add-postamble.phtml);
  • a toolbar for the page header (add-toolbar.phtml);
  • custom JavaScript (js/add.js).

Restricting Access

One method of restricting access is shown above via the switch() statement on the user's permissions.

Another method is to use the xxxPreamble() action hooks for the defined CRUD functions.

A third method is via the feParams allowedActions[] array. If this is not set, then this method does not apply and will be ignored.

If however it is set, then only the actions listed in the array will be allowed. Here is a concrete example:

switch( $this->getUser()->getPrivs() )
{
    case \Entities\User::AUTH_SUPERUSER:
        // insert list and view column setup...
        break;
        
    case \Entities\User::AUTH_CUSTUSER:
        $this->_feParams->allowedActions = [ 'configuration', 'export' ];
        $this->_feParams->defaultAction = 'configuration';
        break;
        
    default:
        $this->redirectAndEnsureDie( 'error/insufficient-permissions' );
}

Here, the super use is allowed full access, users with AUTH_CUSTUSER are allowed access to only two actions: configurationAction and exportAction, while any other user is redirected to the error handler.

If the CUSTUSER tries to access non-allowed actions, they too will be redirected to error/insufficient-permissions.