Giving your symfony apps better urls

I love pretty URLs. I don’t like file name extensions unless they end with .html.

All versions of symfony save me from address bar ugliness, but only for the first application I create. Everything else is ass.php/backwards.

I’m going to give you two ways of beautifying your second, third, and forth applications so you can http://www.get.your/application/looking/sexy. Both methods involve creating a subdirectory.

Put it all in a subdirectory, and symlink your way out

So if you have an app named “admin,” you can do the following:

  1. Create a directory in your web folder called admin
  2. Copy your admin.php and admin_dev.php into this folder
  3. Copy the .htaccess file into this folder
  4. Rename admin.php to index.php
  5. Edit the file now named index.php, and change the require_once statement at the top so it’s an extra directory back
  6. Symbolically link the js, css, images, and uploads directories from within the admin directory to the directory above; so from your app directory: ln -s ../js

This is what I usually do in a Linux environment. However, I’m developing on a Windows box, and symbolic links don’t translate easily between Windows and Linux. So here’s the second option, without using symbolic links:

Put just the .htaccess in a subdirectory

  1. As before, create a subdirectory in your web folder for the admin.
  2. Copy the .htaccess file in, and at the very bottom of the file, change index.php to ../admin.php. It should read RewriteRule ^(.*)$ ../admin.php [QSA,L]
  3. Edit your routing.yml file for the admin app, and append /admin/ to all of your routes.

That should about cover it. Also remember to set no_script_name to on in each of your applications.

Enjoy.

Alternative methods? Please leave comments.

Disabling the submit button with jQuery to prevent extra submissions

It happens.

Not often, but once in a while a user submits the same thing. Twice, maybe three times, and each just a second or two apart.

It’s easy to understand why. They click the submit button and then … nothing happens. If it doesn’t work right away, we assume it’s not working. The solution? Hit the button again! Hit it five times until finally, it seems to work.

There are two things you need to do to stop this from happening.

  1. Assure the user they successfully clicked the button, and they only have to wait a moment for the form to be processed.
  2. Stop the user from hitting the button again.

I’m a jQuery newbie, but it wasn’t long before I was in love with this javascript library. Here is my solution:

  1. Download the jQuery library if you haven’t already, and drop it in your js directory if you’re using Symfony.
  2. Reference the jquery library in your template, like so:
    <?php use_javascript('jquery') ?>
    
  3. And finally, here’s the source code:
<script type="text/javascript">
  $(document).ready(function(){
    $("input[type='submit']").attr("disabled", false);

    $("form").submit(function(){
      $("input[type='submit']").attr("disabled", true).val("Please wait...");
      return false;
    })
  })
</script>

I love the simplicity.

Once the document is ready, make sure every submit button on the page is enabled. In Firefox 3.5, if I click the back button to go back to the form after submitting, the submit button stays disabled. I’m not sure why; the value attribute is correct, but it stays disabled, even if you refresh.

In the next few statements, I add an onsubmit event to the form. If you have javascript validation on your form, you’ll have to execute your validation from within this function.

The function finds all submit buttons within the form, disables them, and changes their value to “Please wait…”. All of this is done dynamically, without altering the form directly in my html. Love it.

Does anyone have any server-side solutions to this problem? If you do I’d love to hear about them.

Country codes in YAML format

Here are all the country codes in YAML format for your downloading pleasure! You may place them in your app.yml (for symfony users) and call the list with sfConfig::get(‘app_countries’).

all:
  .lists:
    countries:
      CA:	Canada
      US:	United States of America
      AD: Andorra
      AE:	United Arab Emirates
      AF:	Afghanistan
      AG:	Antigua & Barbuda
      AI:	Anguilla
      AL:	Albania
      AM:	Armenia
      AN:	Netherlands Antilles
      AO:	Angola
      AQ:	Antarctica
      AR:	Argentina
      AS:	American Samoa
      AT:	Austria
      AU:	Australia
      AW:	Aruba
      AZ:	Azerbaijan
      BA:	Bosnia and Herzegovina
      BB:	Barbados
      BD:	Bangladesh
      BE:	Belgium
      BF:	Burkina Faso
      BG:	Bulgaria
      BH:	Bahrain
      BI:	Burundi
      BJ:	Benin
      BM:	Bermuda
      BN:	Brunei Darussalam
      BO:	Bolivia
      BR:	Brazil
      BS:	Bahama
      BT:	Bhutan
      BU:	Burma (no longer exists)
      BV:	Bouvet Island
      BW:	Botswana
      BY:	Belarus
      BZ:	Belize
      CC:	Cocos (Keeling) Islands
      CF:	Central African Republic
      CG:	Congo
      CH:	Switzerland
      CI:	Côte D'ivoire (Ivory Coast)
      CK:	Cook Iislands
      CL:	Chile
      CM:	Cameroon
      CN:	China
      CO:	Colombia
      CR:	Costa Rica
      CS:	Czechoslovakia (no longer exists)
      CU:	Cuba
      CV:	Cape Verde
      CX:	Christmas Island
      CY:	Cyprus
      CZ:	Czech Republic
      DD:	German Democratic Republic (no longer exists)
      DE:	Germany
      DJ:	Djibouti
      DK:	Denmark
      DM:	Dominica
      DO:	Dominican Republic
      DZ:	Algeria
      EC:	Ecuador
      EE:	Estonia
      EG:	Egypt
      EH:	Western Sahara
      ER:	Eritrea
      ES:	Spain
      ET:	Ethiopia
      FI:	Finland
      FJ:	Fiji
      FK:	Falkland Islands (Malvinas)
      FM:	Micronesia
      FO:	Faroe Islands
      FR:	France
      FX:	France, Metropolitan
      GA:	Gabon
      GB:	United Kingdom (Great Britain)
      GD:	Grenada
      GE:	Georgia
      GF:	French Guiana
      GH:	Ghana
      GI:	Gibraltar
      GL:	Greenland
      GM:	Gambia
      GN:	Guinea
      GP:	Guadeloupe
      GQ:	Equatorial Guinea
      GR:	Greece
      GS:	South Georgia and the South Sandwich Islands
      GT:	Guatemala
      GU:	Guam
      GW:	Guinea-Bissau
      GY:	Guyana
      HK:	Hong Kong
      HM:	Heard & McDonald Islands
      HN:	Honduras
      HR:	Croatia
      HT:	Haiti
      HU:	Hungary
      ID:	Indonesia
      IE:	Ireland
      IL:	Israel
      IN:	India
      IO:	British Indian Ocean Territory
      IQ:	Iraq
      IR:	Islamic Republic of Iran
      IS:	Iceland
      IT:	Italy
      JM:	Jamaica
      JO:	Jordan
      JP:	Japan
      KE:	Kenya
      KG:	Kyrgyzstan
      KH:	Cambodia
      KI:	Kiribati
      KM:	Comoros
      KN:	St. Kitts and Nevis
      KP:	Korea, Democratic People's Republic of
      KR:	Korea, Republic of
      KW:	Kuwait
      KY:	Cayman Islands
      KZ:	Kazakhstan
      LA:	Lao People's Democratic Republic
      LB:	Lebanon
      LC:	Saint Lucia
      LI:	Liechtenstein
      LK:	Sri Lanka
      LR:	Liberia
      LS:	Lesotho
      LT:	Lithuania
      LU:	Luxembourg
      LV:	Latvia
      LY:	Libyan Arab Jamahiriya
      MA:	Morocco
      MC:	Monaco
      MD:	Moldova, Republic of
      MG:	Madagascar
      MH:	Marshall Islands
      ML:	Mali
      MN:	Mongolia
      MM:	Myanmar
      MO:	Macau
      MP:	Northern Mariana Islands
      MQ:	Martinique
      MR:	Mauritania
      MS:	Monserrat
      MT:	Malta
      MU:	Mauritius
      MV:	Maldives
      MW:	Malawi
      MX:	Mexico
      MY:	Malaysia
      MZ:	Mozambique
      NA:	Namibia
      NC:	New Caledonia
      NE:	Niger
      NF:	Norfolk Island
      NG:	Nigeria
      NI:	Nicaragua
      NL:	Netherlands
      "NO":	Norway
      NP:	Nepal
      NR:	Nauru
      NT:	Neutral Zone (no longer exists)
      NU:	Niue
      NZ:	New Zealand
      OM:	Oman
      PA:	Panama
      PE:	Peru
      PF:	French Polynesia
      PG:	Papua New Guinea
      PH:	Philippines
      PK:	Pakistan
      PL:	Poland
      PM:	St. Pierre & Miquelon
      PN:	Pitcairn
      PR:	Puerto Rico
      PT:	Portugal
      PW:	Palau
      PY:	Paraguay
      QA:	Qatar
      RE:	Réunion
      RO:	Romania
      RU:	Russian Federation
      RW:	Rwanda
      SA:	Saudi Arabia
      SB:	Solomon Islands
      SC:	Seychelles
      SD:	Sudan
      SE:	Sweden
      SG:	Singapore
      SH:	St. Helena
      SI:	Slovenia
      SJ:	Svalbard & Jan Mayen Islands
      SK:	Slovakia
      SL:	Sierra Leone
      SM:	San Marino
      SN:	Senegal
      SO:	Somalia
      SR:	Suriname
      ST:	Sao Tome & Principe
      SU:	Union of Soviet Socialist Republics (no longer exists)
      SV:	El Salvador
      SY:	Syrian Arab Republic
      SZ:	Swaziland
      TC:	Turks & Caicos Islands
      TD:	Chad
      TF:	French Southern Territories
      TG:	Togo
      TH:	Thailand
      TJ:	Tajikistan
      TK:	Tokelau
      TM:	Turkmenistan
      TN:	Tunisia
      TO:	Tonga
      TP:	East Timor
      TR:	Turkey
      TT:	Trinidad & Tobago
      TV:	Tuvalu
      TW:	Taiwan, Province of China
      TZ:	Tanzania, United Republic of
      UA:	Ukraine
      UG:	Uganda
      UM:	United States Minor Outlying Islands
      UY:	Uruguay
      UZ:	Uzbekistan
      VA:	Vatican City State (Holy See)
      VC:	St. Vincent & the Grenadines
      VE:	Venezuela
      VG:	British Virgin Islands
      VI:	United States Virgin Islands
      VN:	Viet Nam
      VU:	Vanuatu
      WF:	Wallis & Futuna Islands
      WS:	Samoa
      YD:	Democratic Yemen (no longer exists)
      YE:	Yemen
      YT:	Mayotte
      YU:	Yugoslavia
      ZA:	South Africa
      ZM:	Zambia
      ZR:	Zaire
      ZW:	Zimbabwe
      ZZ:	Unknown or unspecified country

Securing multi-user CRUD operations through the myUser class in Symfony

Symfony has automated the process of creating a CRUD application, and even has built-in CSRF protection.

However, for a site with multiple users, the generated CRUD code just won’t do.

In my schema, each row of a model is owned by a user, according to their user_id. In the dashboard application I am building, each user has a list of programs.

They can create, read, update and delete each program that they own.

But the generated actions would allow users access to other user’s programs, simply by changing the id in URLs such as /program/edit/id/1.

My solution? Store the user_id of the currently logged in user in myUser.class.php. Since the myUser class has ownership of the id of the currently logged in user, I thought it was appropriate to implement convenience getters and setters for the dashboard here.

So my list action would contain:

$this->programs = $this->getUser()->getPrograms();

The myUser::getPrograms() method is implemented like so:

public function getPrograms()
{
  return Doctrine::getTable('Program')->fetchByOwner($this->getId());
}

public function getProgram($pk)
{
  $program = Doctrine::getTable('Program')->find($pk);
  return $this->hasOwnershipOf($program) ? $program : false;
}

public function hasOwnershipOf($row)
{
  if ( isset($row['user_id']) )
  {
    if ( $row['user_id'] == $this->getId() )
    {
      return true;
    }
  }
  return false;
}

Adding the myUser::fetchByOwner() method leaves the model functionality open to being called from another administrative application other than the dashboard. You could use a Doctrine magic method here, however I would prefer to return programs in alphabetical order (or perhaps allow for other options to be passed in using a Doctrine_Query as a second parameter).

For myUser::getProgram($pk), I simply use the ::find() magic method in doctrine to retrieve the object by primary key, as usual, and then return it only if it matches the user_id of the logged-in user.

Writing for multiple users is common place for web-based applications. However, it’s something I don’t usually deal with at work where I write simple, single-user back ends for editing website content.

Is there a more elegant way of handling user ownership of database content? Perhaps by writing a doctrine behavior? How do you usually go about it?

Please comment.

Create a dynamic CSS module in Symfony 1.2 fast

I’m sure there are a number of ways you can handle this. There are also tons of modifications you can do to make it more useful, or better adapted to your particular needs. This post will quickly illustrate one way to create a dynamic stylesheet in Symfony 1.2.

I just finished writing a module that allows my users to change their background color. So I thought I’d share a similar way of doing this with an example.

Create a new doctrine model. Since we’re keeping things simple, our style sheet will only have one editable element: the body tag’s background-image.

DynamicCSS:
  columns:
    identifier: string(255)
    background_color: string(255)

Add a fixture and load it up:

/data/fixtures/fixtures.yml

DynamicCSS:
  black:
    identifier: "main"
    background_color: "#000000"
$ symfony doctrine:build-all-reload

The identifier will be the stylesheet’s name. You’ll see it in the route, which you’ll add now:

/apps/frontend/config/routing.yml

dynamiccss:
  url: /dynamiccss/:identifier.css
  param: { module: dynamiccss, action: render }

Create a new module:

$ symfony init-module frontend dynamiccss

This module will be used to render a dynamic style sheet. You’ll want to make sure it doesn’t render the layout, and that it returns a content type of ‘text/css’. So edit the actions file and add a preExecute() method. Of course, you’ll also need a method to fetch the style sheet and render it.

/apps/frontend/modules/dynamiccss/actions/actions.class.php

...
  public function preExecute()
  {
    $this->setLayout(false);
    $this->getResponse()->setContentType('text/css');
  }

  public function executeRender()
  {
    $this->stylesheet = Doctrine::getTable('DynamicCSS')
      ->getByIdentifier($request->getParameter('identifier'));
    $this->forward404Unless($this->stylesheet);
  }
...

Next we’ll add the template, which is at this point is very simplistic. You’ll need to have that ::getByIdentifier() method implemented in the DynamicCSSTable class, as well.

/apps/frontend/modules/dynamiccss/templates/renderSuccess.php

body {
  background-color: <?php echo $stylesheet->getBackgroundColor() ?>;
}

/lib/model/doctrine/DynamicCSSTable.class.php

...
public static function getByIdentifier($identifier)
{
  return $this->createQuery('c')
    ->addWhere('c.identifier = ?', $identifier)->fetchOne();
}
...

Now you can add your dynamic style sheet to the view. Here is an example:

/apps/frontend/config/view.yml

default:
  ...
  stylesheets: [/dynamiccss/main.css: { raw_name: true }]
  ...

That “raw_name” option allows you to override the typical way of creating an asset’s file name.

Now whenever someone edits the background color for the “main” style sheet, from the database, the background color can change.

You’ll still need to add an interface for editing the background color (or however many other options you add). That is up to you.

Complaints? Pointers? Thought of a better way?

Please post.

How to add image uploading to your forms with Symfony 1.2 and Doctrine, from beginning to end

I’m still very new to Symfony 1.2 and its form framework. Reading through documentation, discussion forums and blog posts, I couldn’t find a tutorial which had everything I needed in an image upload. Having finished an image upload for the first time, I decided to write such a tutorial.

This tutorial requires Symfony 1.2, the Doctrine ORM plugin and the PHP GD library. You should already have all of these elements installed, and be familiar with them.

We’ll start at the beginning. I’m going to make an uploader for a background image to attach to a Website model. Make sure you have a field in your model to store the image file name:

Website:
  columns:
    background_image:  string(255)

And rebuild your model:

symfony doctrine:build-all-reload

Now open the form class generated from your schema.

You’re going to add a widget and a validator for the image upload. The widget will also allow the user to view the image after it has been uploaded, and delete it if necessary. The validator will make sure the image upload is not required (it is required by default). There is also a second validator, sfValidatorPass, because the widget we are using adds a checkbox widget giving the user the option to delete the file.

You’ll also override the doSave() method and updateObject() methods. The doSave() method will handle the upload (if there is one), create a new file name for the image, and save the image both to the file system and the background_image field automatically. Since the background_image field is automatically set with the full path of the file, you will use updateObject() to remove the path and just store the file name. If you move your project to another system, the file path will change and so we want to store just the file name.

Also in doSave(), you will handle the case where the user has checked off the “delete image” checkbox, which is automatically named background_image_delete.

Here is the code so far under lib/form/doctrine/WebsiteForm.class.php:

class WebsiteForm extends BaseWebsiteForm
{
  public function configure()
  {
    $this->widgetSchema['background_image'] = new sfWidgetFormInputFileEditable(array(
      'file_src' => '/'.basename(sfConfig::get('sf_upload_dir')).'/'.$this->getObject()->getBackgroundImage(),
      'is_image' => true,
      'edit_mode' => strlen($this->getObject()->getBackgroundImage()) > 0,
      'template'  => '
<div>%file%
%input%
%delete% %delete_label%</div>
'
    ));

    $this->validatorSchema['background_image'] = new sfValidatorFile(array(
      'required' => false,
      'mime_types' => 'web_images'
    ));
    $this->validatorSchema['background_image_delete'] = new sfValidatorPass();
  }

  protected function doSave ( $con = null )
  {
    $upload = $this->getValue('background_image');

    if ( $upload )
    {
      $filename = sha1($upload->getOriginalName().microtime().rand()).$upload->getExtension($upload->getOriginalExtension());
      $filepath = sfConfig::get('sf_upload_dir').'/'.$filename;

      $oldfilepath = sfConfig::get('sf_upload_dir').'/'.$this->getObject()->getBackgroundImage();
      if ( file_exists($oldfilepath) )
      {
        unlink($oldfilepath);
      }

      $upload->save($filepath);
    }

    $delete = $this->getValue('background_image_delete');
    if ( $delete )
    {
      $filename = $this->getObject()->getBackgroundImage();
      $filepath = sfConfig::get('sf_upload_dir').'/'.$filename;
      @unlink($filepath);
      $this->getObject()->setBackgroundImage(null);
    }

    return parent::doSave($con);
  }

  public function updateObject($values = null)
  {
    $object = parent::updateObject($values);
    $object->setBackgroundImage(str_replace(sfConfig::get('sf_upload_dir').'/', '', $object->getBackgroundImage()));
    return $object;
  }

}

This should work well for the most part. But what if the image is 2000 pixels in width? It will be very disruptive to display such a large image.

That’s where sfThumbnailPlugin comes in.

Install the plugin if you haven’t already done so:

$ symfony plugin:install sfThumbnailPlugin
$ php symfony cc

Now create a new directory to store your thumbnails:

$ mkdir /web/uploads/thumbnails

You’ll probably want to use this directory in other modules or applications in your project. Add a new setting called sf_thumbnail_dir in your project configuration:

config/ProjectConfiguration.class.php

...
class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    ...
    sfConfig::set('sf_thumbnail_dir', sfConfig::get('sf_upload_dir').'/thumbnails');
    ...
  }
}

Now all that’s left to do is create and store the thumbnail from the doSave() method of your form class, and make sure that your form displays the thumbnail and not the source image in your configure() method.

Modify the widget settings so that it displays the thumbnail. Note that I’ve …’d most of the parts that don’t change:

lib/form/doctrine/WebsiteForm.class.php

class WebsiteForm extends BaseWebsiteForm
{
  public function configure()
  {
    ...
    $this->widgetSchema['background_image'] = new sfWidgetFormInputFileEditable(array(
      'file_src' => '/'.basename(sfConfig::get('sf_upload_dir')).'/'.basename(sfConfig::get('sf_thumbnail_dir')).'/'.$this->getObject()->getBackgroundImage(),
      'is_image' => true,
      'edit_mode' => strlen($this->getObject()->getBackgroundImage()) > 0,
      'template'  => '
<div>%file%
%input%
%delete% %delete_label%</div>
'
    ));
    ...
  }

  protected function doSave ( $con = null )
  {
    $upload = $this->getValue('background_image');

    if ( $upload )
    {
      ...
      $thumbnailpath = sfConfig::get('sf_thumbnail_dir').'/'.$filename;
      ...
      $oldthumbnailpath = sfConfig::get('sf_thumbnail_dir').'/'.$this->getObject()->getBackgroundImage();
      if ( file_exists($oldthumbnailpath) )
      {
        unlink($oldthumbnailpath);
      }

      ...
      $thumbnail = new sfThumbnail(150, 150, true, true, 75, 'sfGDAdapter');
      $thumbnail->loadFile($filepath);
      $thumbnail->save($thumbnailpath);
    }

    $delete = $this->getValue('background_image_delete');

    if ( $delete )
    {
      ...
      $thumbnailpath = sfConfig::get('sf_thumbnail_dir').'/'.$filename;
      @unlink($thumbnailpath);
      ...
    }
  }

  ...

}

And that’s all there is to it. Note that there is no template file here because the form framework handles the view of the form inputs.

Questions? Complaints? Leave a comment.

Here are some related resources:

Here are some other posts you might be interested in:

Table-level behaviors will remove global behaviors in Doctrine schemas

Here is something to watch out for. If you define global behaviors in your schema, and then define table-level behaviors, the global behavior will be removed (unless you declare it again on the table level).

Here is a quick example:

actAs: [Timestampable]

Program:
  actAs:
    Sluggable:
      fields: [name]
  columns:
    name: string(255)
    user_id: integer(4)
  relations:
    sfGuardUser:
      foreignType: many
      type: one
      local: user_id
      foreign: id

For a short while I couldn’t figure out why the created_at and updated_at fields weren’t showing up in the Program table. Then I realized that it was removed at the table level, and I had to add it in again.

The official documentation does not mention this:

http://www.symfony-project.org/doctrine/1_2/en/04-Schema-Files#chapter_04_global_schema_information

Hopefully this will save you 15 minutes of scratching your head.

Here is more information on Symfony 1.2 and doctrine:

http://www.symfony-project.org/doctrine/1_2/en/04-Schema-Files

Using Symfony filters and CSS to present a design in-context

This post assumes you have some experience with Symfony 1.0.

At my work I needed to create several different versions of the same site. This mainly entailed changing background images to get a slightly different look and feel, and changing the content category, depending on a request parameter.

For this tutorial, I will use the company I work for as an example. Momentum IT Group has two distinct divisions: ELearning and Web. So let’s say I wanted to make slight design changes depending on which division I’m looking at.

We’ll start where all things Symfony begin: with the routes. Open up apps/frontend/config/routing.yml and enter the following:

division_page:
  url:               /:division/:slug
  param:          { module: cms, action: display }
  requirements: { division: elearning|web }

I leave the cms module for you to implement. The display action would take a unique slug as a key to look up the page content from the database for display.

In this route, the division is required to be either elearning or web, and these two options will be used to display a different background design (and possibly select from a different content category in your cms module).

The next step is to implement the switch which determines which background to apply, if any. You can always write this code in your cms module, but you may have other modules other than the cms module which need to change design elements depending on which division the user is viewing. To handle this situation, you can create a filter.

A filter is applied once for each request. Start by opening apps/frontend/config/filters.yml and adding a new filter class:

rendering:    ~
web_debug: ~
security:     ~

# generally, you will want to insert your own filters here
cssFilter:
  class:       cssFilter

cache:       ~
common:    ~
flash:         ~
execution:  ~

Create the filter class in apps/frontend/lib/cssFilter.class.php:

<?php

class cssFilter extends sfFilter
{
  public function execute($filterChain)
  {
    if ( $this->isFirstCall())
    {
      $response = $this->getContext()->getResponse();
      switch ( sfContext::getInstance()->getRequest()->getParameter('division') )
      {
        case 'elearning':
          $response->addStylesheet('elearning', 'last');
        break;
        case 'web':
          $response->addStylesheet('web', 'last');
        break;
      }
    }
  }
}

Make sure you place two new CSS files in your web/css directory.

For this example, they are elearning.css and web.css. They will be the last css files in your HTML header and will override your main.css where needed. So, you might override a background image like so:

web/css/main.css

body {
  background: url(../images/background.jpg) no-repeat top left;
}

web/css/elearning.css

body {
  background-image: url(../images/elearning.jpg);
}

Now if you direct your browser to [your_site]/elearning/[page_slug], you will have taken advantage of CSS cascading and Symfony filters to present a slightly different design depending on the context.

Adding to the sfDoctrineGuardPlugin

Here are some excellent resources for working with sfDoctrineGuardPlugin:

Plugin page on symfony (README is possibly out of date)
http://www.symfony-project.org/plugins/sfDoctrineGuardPlugin

Tutorial on how to add a profile to work with sfDoctrineGuardPlugin
http://www.symfony-project.org/blog/2008/11/12/call-the-expert-customizing-sfdoctrineguardplugin

Tutorial on how to add a registration module
http://www.symfony-project.org/blog/2008/12/03/call-the-expert-simple-registration-with-sfdoctrineguardplugin

First of all, the schema code in the documentation for sfDoctrineGuard appears to be old:

(be mindful of the lack of proper YAML spacing; I haven’t figured out how to incorporate code into wordpress.com posts yet)

sf_guard_user_profile:
_attributes: { phpName: sfGuardUserProfile }
id:
user_id:     { type: integer, foreignTable: sf_guard_user, foreignReference: id, required: true, onDelete: cascade }
first_name:  varchar(20)
last_name:   varchar(20)
birthday:    date

I replaced it with:

sfGuardUserProfile:
columns:
user_id:     { type: integer, notnull: true, required: true }
first_name:  string(20)
last_name:   string(20)
email:       string(75)
relations:
sfGuardUser: { local: user_id, foreign: id, onDelete: CASCADE }

This schema could be parsed by Doctrine in Symfony 1.2. I ran the symfony doctrine:build-all-reload task again. However it coughed up an error from MySQL:


SQLSTATE[HY000]: General error: 1005 Can't create table 'register.#sql-ae0_25'
(errno: 150). Failing Query: ALTER TABLE sf_guard_user_profile ADD FOREIGN KEY
(user_id) REFERENCES sf_guard_user(id) ON DELETE CASCADE

This seems to be a problem with the field definition. It has to do with user_id being integer (bigint in the SQL), and the id field in the plugin’s schema set to integer(4). Others have run into this error:

http://forums.mysql.com/read.php?22,19755,19755#msg-19755

Anyhow, I changed the definition for user_id to be the primary key. This is how it should be anyway; each user should only have one profile.

sfGuardUserProfile:
columns:
user_id:     { type: integer(4), primary: true }
...etc...

I ran symfony doctrine:build-all-reload again, and everything was fine…

Until I started using fixtures to load some default users and profiles:

sfGuardUser:
sgu_admin:
username:       admin
password:       mr9o1ob7
is_super_admin: true
normal_user:
username:       user
password:       password
is_super_admin: false</code>

sfGuardUserProfile:
sgup_admin:
sfGuardUser:    sgu_admin
first_name:     Jonathan
last_name:      Montgomery
email:          jjmontgo@gmail.com
sgup_user:
sfGuardUser:    normal_user
first_name:     Normal
last_name:      User
email:          noone@gmail.com

sfGuardPermission:
sgp_admin:
name:           admin
description:    Administrator permission

sfGuardGroup:
sgg_admin:
name:           admin
description:    Administrator group

sfGuardGroupPermission:
sggp_admin:
sfGuardGroup:       sgg_admin
sfGuardPermission:  sgp_admin

sfGuardUserGroup:
sgug_admin:
sfGuardGroup:       sgg_admin
sfGuardUser:        sgu_admin

This loaded to the database with user_id set to 0 in the user profile.

I tinkered around a bit more, altered the sfGuardUser relation under the profile schema (which didn’t seem to help), and eventually removed the primary key from user_id.

That worked. I’m not sure why. I don’t see why sfGuardUserProfile should have its own id field as a primary key. Please comment on this if you know the answer.

Here is my final sfGuardUserProfile schema:

sfGuardUserProfile:
actAs: [Timestampable]
columns:
user_id:
type: integer(4)
first_name:  string(20)
last_name:   string(20)
email:       string(75)
relations:
sfGuardUser:
local:     user_id
foreign:   id
type:      one
foreignType: one

Why you should separate each query into its own function

With a query in its own function, you can write calling functions that:

a). Retrieve a collection for that type of query.

b). Retrieve one result for that type of query.

c). Extend that type of query with more conditions.

d). Pass the query to other objects that take a query as a parameter (eg. sfPager).

e). Distribute processing to the appropriate ORM classes.

For example, you have a database of classes and students.

If you need to get a classes’ students, you make a request to the classroom->getStudents() query function.

The classroom->getStudents() function adds its criteria (eg. class.id = #), and then passes that criteria to the StudentTable->getStudents() query function.

In short, you reduce repetition.  You enhance maintainability.  Because each query has a single responsibility.

More info:

http://en.wikipedia.org/wiki/Single_responsibility_principle