Owner Credential & Propel Admin Generator

It would be great to be able to specify in generator.yml a per object owner credential, so the generator only showed that actions to the owner of each object. Lets see how we can achieve this behavior easily.

Setting up the project.

Install and Configure Symfony

Follow the instructions of the great jobeet tutorial to install symfony and create the frontend application. We will use the propel ORM.

Instaling sfGuardPlugin

Following the symfony philosophy, we do not need to reinvent the wheel.

$ symfony plugin:install sfGuardPlugin

Preparing schema and some sample data

# config/schema.yml
propel:
  post:
    id:
    title:    { type: varchar(255), required: true }
    content:  { type: longvarchar, required: false }
    owner_id: 
      type: integer
      foreignTable: sf_guard_user
      foreignReference: id
      required: true
# data/fixtures/fixtures.yml
sfGuardUser:
  andrew:
    username: andrew
    password: andrew
  valentine:
    username: valentine
    password: valentine

Post:
  -
    title: Andrew's Post
    content: This is the content of the first Andrew's post.
    owner_id: andrew
  -
    title: Valentine's Post
    content: This is the content of the first Valentine's post.
    owner_id: valentine

Build models, load data and create the post module

$ symfony generate:app frontend
$ symfony propel:build --all --and-load
$ symfony propel:generate-admin frontend Post
$ symfony plugin:publish-assets
$ symfony clear cache

The funny work

At this point, if you go to the url ocag.localhost/post, we can see the list of posts, with two actions attached to each post: edit and delete.

What we want is that the admin-generator only shows the actions edit and/or delete to the owner of each post.

Enabling login module and securing post module

Add the following to settings.yml

# apps/frontend/settings.yml
# ...
all:
  .settings:
    # ...
    # Modules
    enabled_modules: [default, sfGuardAuth]

  .actions:
    # ...
    login_module:   sfGuardAuth
    login_action:   signin
    secure_module:  sfGuardAuth
    secure_action:  secure

Secure the post module

# apps/frontend/modules/post/config/security.yml
default:
  is_secure: true

Edit the generator.yml

First, we set the owner credential to an object action.

What we woult like the admin generator to do with this, is to hide that action to everybody but the creator of the object being procesed.

Saddly this is not way the default admin generator works, but we will soon see how to build our own customized admin theme with this behavior.

Don’t be afraid! It’s easyer than it appears.

# apps/modules/post/config/generator.yml
generator:
  class: sfPropelGenerator
  param:
    model_class:           Post
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              Post
    plural:                Posts
    route_prefix:          post
    with_propel_route:     1
    actions_base_class:    sfActions

    config:
      actions: ~
      fields:  ~
      list:
        #
        # setting owner credential
        #
        object_actions:
          _edit:
            credentials: owner
          _delete:
            credentials: owner
      filter:  ~
      form:    ~
      edit:    ~
      new:     ~

Creating myadmin theme

Create the folder data/generator/sfPropelModule/myadmin and copy the admin theme files in order to customize them:

$ mkdir -p data/generator/sfPropelModule/myadmin
$ cp -r lib/vendor/symfony/lib/plugins/sfPropelPlugin/data/generator/sfPropelModule/admin/* \
data/generator/sfPropelModule/myadmin

Configure the generator.yml to use myadmin theme

# apps/modules/post/config/generator.yml
generator:
  #...
  param:
    #...
    theme:                 myadmin

The partial-generator responsible of the object_actions is data/generator/sfPropelModule/myadmin/template/templates/_list_td_actions.php. We are going to add a line that calls a user function that dinamically adds or revoques the owner credential depending on the object being processed.

<?php echo '<?php $sf_user->addOwnerCredentials($' . $this->getSingularName() . ') ?>' ?>
<td>
  <ul class="sf_admin_td_actions">
<?php foreach ($this->configuration->getValue('list.object_actions') as $name => $params): ?>
<?php if ('_delete' == $name): ?>
    <?php echo $this->addCredentialCondition('[?php echo $helper->linkToDelete($'.$this->getSingularName().', '.$this->asPhp($params).') ?]', $params) ?>

<?php elseif ('_edit' == $name): ?>
    <?php echo $this->addCredentialCondition('[?php echo $helper->linkToEdit($'.$this->getSingularName().', '.$this->asPhp($params).') ?]', $params) ?>

<?php else: ?>
    <li class="sf_admin_action_<?php echo $params['class_suffix'] ?>">
      <?php echo $this->addCredentialCondition($this->getLinkToAction($name, $params, true), $params) ?>

    </li>
<?php endif; ?>
<?php endforeach; ?>
  </ul>
</td>

Now edit myUser.class.php to code the addOwnerCredentials method

// apps/frontend/lib/myUser.class.php
class myUser extends sfGuardSecurityUser
{
  protected static $OWNER_CREDENTIAL = 'owner';

  /**
   * If the user is the owner of the $object adds owner credentials,
   * otherwise remvoques it.
   */
  public function addOwnerCredentials($object)
  {
    if (!$this->isAuthenticated())
    {
      return false;
    }

    if (!$object)
    {
      return false;
    }

    if (get_class($object) == 'sfOutputEscaperObjectDecorator')
    {
      $object = $object->getRawValue();
    }

    if (method_exists($object, 'isOwner') && $object->isOwner($this->getGuardUser()))
    {
      $this->addCredential(self::$OWNER_CREDENTIAL);
    } else
    {
      $this->removeCredential(self::$OWNER_CREDENTIAL);
    }
  }
}

Tunning Post model

The only thing we have to do now is to add the isOnwer method to each model that we want to be checked against owner credential, Post model in our case. So lets edit lib/model/Post.php

// lib/model/Post.php
class Post extends BasePost {
  public function isOwner($sf_guard_user) {
    return $this->owner_id == $sf_guard_user->getId();
  }
}

Try it

And thats all. Clear caches, login as Andrew or Alice and see how the admin generator only shows the action links to the owner of each post. See the post list logged as Andrew:

Very immportant final comment

Remember that the only thing that we have done with this is to automatize the view layer generated by the admin generator. You still must securize the edit and delete actions of each module in order to avoid undeserable people to be able to execute them.

Snippet: sfWidgetFormJQueryDate for Spanish culture

  1. Be sure that sfFormExtraPlugin is installed:

    $ php symfony plugin:install sfFormExtraPlugin
  2. In the template:

    <?php use_javascript('ui/i18n/jquery-ui-i18n.js') ?>
  3. In the filter or form:

    $this->widgetSchema['my_date_field'] = new sfWidgetFormJQueryDate(array(
          'culture' => 'es',
          'format'  => '%day%/%month%/%year%', 
          'config'  => '{"showMonthAfterYear": false, "firstDay": 1 }'
        ));
      

More info on ‘config’ option in http://jqueryui.com/demos/datepicker.

Symfony Partials: Accessing Raw Data defined in an Action

Sometimes we need to pass some message from the action to a partial via template. If this message only contains plain text there is no problem, we simply define the property in the action, and in the template we pass it to the partial, like this:

In the action:

// modules/mymodule/actions/actions.class.php
class mymoduleActions extends sfActions
{
  public function executeShow(sfWebRequest $request)
  {
    $this->msg = "My plain info message";
  }
}

In the template:

<!-- modules/mymodule/templates/showSuccess.php -->
<hr />
<h1>My Module</h1>
<?php include_partial('mypartial', array('msg' => $msg)); ?>
<hr />

And in the partial:

<!-- modules/mymodule/templates/_mypartial.php -->
<p><? echo $msg ?></p>

The problem comes when we include a little of html in the message, for example, lets change the action:

class mymoduleActions extends sfActions
{
  public function executeShow(sfWebRequest $request) 
  {
    $this->msg = "My <b>html</b> info message";
  }
}

Now, if we leave the template and the partial unchanged, you get this in the navigator:


My Module

My &lt;b&gt;html&lt;/b&gt; info message


Ok, it seems that we forgot to get raw data, so lets change the template:

<!-- modules/mymodule/templates/showSuccess.php -->
<hr />
<h1>My Module</h1>
<?php include_partial('mypartial', array('msg' => $sf_data->getRaw($msg))) ?>
<hr />

And now the result is:


My Module

My <b>html</b> info message


That is a little better, but it is not still the desired result. To get it, we have to also change the partial:

<!-- modules/mymodule/templates/_mypartial.php -->
<p><? echo $sf_data->getRaw($msg) ?></p>

And finally we get what we wanted:


My Module

My html info message


The conclusion is that if we want to send raw data from the action to the partial, we will have to raw it in both, the template and the partial.

Setting a value to a hidden form symfony field

The case

Sometimes, when creating a new object, we need to give a default value to a specific field but we don’t want this field to be visible in the form.
A tipical example would it be a blog -> post relationship. Where we want the blog_id field to be set automatically but we want it to be hidden.

Blog->Post

The mistake

When creating a new post for a blog, we do not want the blog_id field to be visible, so we typically go to PostForm.class.php and unset the field blog_id:

//myproject/lib/form/PostForm.class.form
class PostForm extends BasePostForm
{
  public function configure()
  {
    unset( $this['blog_id'] );
  }
}

Then, we edit the action class and set a default value for the blog_id field:

// mypoject/modules/post/actions/actions.class.php
class postActions extends sfActions
{
  public function executeNew(sfWebRequest $request)
  {  
    // create the form
    $form = new PostForm();

    // get blog_id parameter from the request
    // and set the default value
    $form->setDefault("blog_id", $request->getParameter('blog_id') );
    
    $this->form = $form;
  }
  //... rest of actions here ...
}

Finally, when we try to create a new post, we realize that the blog_id has not been set correctly.

The solution

Instead of unsetting the blog_id field, we have to change its widget to a sfWidgetFormInputHidden:

//PostForm.class.form
class PostForm extends BasePostForm
{
  public function configure()
  {
    // the wrong way in this case
    // unset( $this['blog_id'] );

    // the right way in this case
    $this->widgetSchema['blog_id'] = new sfWidgetFormInputHidden();
  }
}

This way the field blog_id remains hidden but do exists in the form, so the setDefault method works correctly.

Snippet: symfony simple frecuently used commands

Syntaxis for some simple but frecuently used symfony commands

Database

Configure symfony to use especific mysql database

$ symfony configure:database "mysql:host=localhost;dbname=my_database" user_name password

Rebuild all. This command rebuild models, form, and tables, so destroy all data in database.

$ symfony propel:build-all

Rebuild all and load test data. This command rebuild models, form, and tables, so destroy all data in database and reload it from fixture files.

$ symfony propel:build-all-load

Frontend and Backend

Frontend Aplication. Generate frontend application

$ symfony generate:app --escaping-strategy=on --csrf-secret=FrontendSecret frontend

Frontend Module. Generate a module called module_name in frontend application. The module will have a show action in addition to the new, edit and delete ones.

$ symfony propel:generate-module --with-show --non-verbose-templates frontend module_name ModelObject

BackEnd Aplication. Generate backend application

$ symfony generate:app --escaping-strategy=on --csrf-secret=BackendSecret backend

BackEnd Module. Generate a fully functional module called ‘module_name’ in the backend, based on the model object named ‘ModuleObject’.

$ symfony propel:generate-admin backend ModuleObject --module=module_name