Symfony World blog is not maintained anymore. Check new sys.exit() programming blog.

Many-to-many relations with on delete cascade

Scene from "Ghostbusters" by Ivan Reitman (1984)

The following article is based on a section from the symfony doctrine book, where an example schema of many-to-many relation is defined. The part that is interesting is defining relations of BlogPostTag:

relations:
    BlogPost:
      foreignAlias: BlogPostTags
    Tag:
      foreignAlias: BlogPostTags

In many situations, it is very useful to add the ON DELETE CASCADE option there, since the BlogPostTag (or any other many-to-many relation table) will have constraints disabling deletion of related records. In other words, if you leave the schema the way it is now, you may find (using an admin generator for example) you can't delete a BlogPost or a Tag if there's at least one BlogPostTag joining the record you want to delete. Example database error message will look like:

Cannot delete or update a parent row: a foreign key constraint fails
(`table`, CONSTRAINT `index` FOREIGN KEY (`column`) REFERENCES `table` (`id`))]

This is because of the constraint which is created in the database - it wants you to delete joining records (BlogPostTag) first, then there are no constraints on both Tag and BlogPost tables. To solve the problem (make the database automatically delete related BlogPostTag records whenever a Tag or BlogPost is deleted), simply add two lines to the text:

relations:
    BlogPost:
      foreignAlias: BlogPostTags
      onDelete: CASCADE
    Tag:
      foreignAlias: BlogPostTags
      onDelete: CASCADE
Rerun building your model and recreate your database. From now on, there shall be no constraint fails on your database.

Custom admin generator filter example

Scene from "The Silence of the Lambs" by Jonathan Demme (1991)

Once upon a time I needed to make custom filters for my admin generator...

Example case

Let's analyse a simple study case. Suppose we are running an online shop. The schema includes a product table which has two properties (among all other properties):

  • quantity - the number of products we have on our store
  • quantity_alarm - the shop manager should get alarmed when the number of products go below this

For example, we have such row in the product table:

name: bicycle
quantity: 1294
quantity_alarm: 100
This means we're having 1294 bikes on the store at the moment and the shop manager gets alarmed when the number of bikes reaches 100.

Simply, the manager of the shop wants to find the products he's running out of. He just wants to have something like a checkbox in his product admin generator filters. When he runs the filter with alarming attribute checked, he wants to see only those products which number is below the quantity_alarm.

Programming part

Configure the filters section of your admin generator:

filter:
  display: [ alarming ]
  fields:
    alarming:
      label: alarming quantity
      help: products needing supplies

Modify ProductFormFilter class. Let's begin with the configure method:

  public function configure()
  {
    // ...
 
    $this->manageFieldAlarming();
  }
 
  protected function manageFieldAlarming()
  {
    $this->widgetSchema ['alarming'] =
      new sfWidgetFormChoice(array(
        'choices' => array(
          '' => 'yes or no',
          1 => 'yes',
          0 => 'no'
    )));
    $this->validatorSchema ['alarming'] =
      new sfValidatorPass();
  }

Update the getFields method:

public function getFields()
{
  $fields = parent::getFields();
  $fields['alarming'] = 'alarming';
  return $fields;
}

Add the following method which will handle the database stuff:

public function addAlarmingColumnQuery($query, $field, $value)
{
  Doctrine::getTable('Product')
    ->applyAlarmingFilter($query, $value);
}

Now we need to create the table class method that we just called in the code above. Go to ProductTable model class and add the following:

  /**
   * Applies the alarming attribute to a given query retrieving products.
   *
   * @param Doctrine_Query $query - query to have alarming attribute applied.
   * @param Integer $value - alarming?
   */
  static public function applyAlarmingFilter($query, $value)
  {
    $rootAlias = $query->getRootAlias();
    switch ($value)
    {
      case '0':
        $query->where($rootAlias.'.quantity > '.$rootAlias.'.quantity_alarm');
        break;
      case '1':
        $query->where($rootAlias.'.quantity <= '\.$rootAlias.'.quantity_alarm');
        break;
    }
    return $query;
  }

The whole thing is complete!

The code could be written in many different ways, of course. The sfValidatorPass is used to pass unchanged filter values. There are three distinct values possible: empty string for 'yes or no', '0' for 'no' and '1' for 'yes'. If empty string is passed, we ignore it. If '0' or '1' is passed, we check it inside the switch statement.

If you're wondering about sfValidatorBoolean, it can't be used, since there are 3 options (yes, no, yes or no) and the boolean validator can handle only two values (but it can be used in other custom filters with no problems).

Notes

The above article is based on a magnificent symfony forum post by dlepage. It has been tested on symfony 1.4 but should work also with versions 1.2 and 1.3.

configuring symfony application on shared hosting

Scene from "Ashes and Diamonds" by Andrzej Wajda (1958)

Notes

The following article has been tested many times on symfony version 1.4 (and has been updated few months after initial publishing).

Introduction

Sometimes a symfony project has to be deployed on shared hosting. It may be especially difficult if no SSH access is provided - this is because one of the main symfony tool is the Command Line Interface. But there are more problems, such as the directory structure of the user's account on shared hosting, which may not be changed. You are forced to modify your app structure as well then. So let's face those problems!

Alternative directory structure

The main thing is to hide the application structure (especially configuration files) from the outside. This means that all directories except for web shall not be accessible from outside. I suggest splitting the whole symfony application into 3 separate directories:

  • one for symfony core libs
  • second one for our application content
  • and the last one for the public web stuff

Example application structure would look like this:

/home/user
  /public_html
    /images
    /css
    /js
    ...
  /symfony_1_4
    /data
    /lib
    /license
    /test
    ...
  /symfony_PROJECT_NAME
    /apps
    /config
    /data
    ...

Apache document root

As you can see, the web directory has been renamed to public_html. This has been done because usually the document root apache directory is named something different than web (which is used by symfony). Now the application has to be informed of what we've just done. Modify the config/ProjectConfiguration.class.php of your application:

public function setup()
{
  ...
  $this->setWebDir($this->getRootDir().'/../public_html');
}

This part is done.

Application controllers

You'll have to modify your all controllers (all applications and all environments) to update the project configuration class path. The original web project directory has been renamed (this doesn't matter) and moved one directory up (and this matters - the path needs to be updated).

The original require_once code looks something like this:

require_once dirname(__FILE__).'/../config/ProjectConfiguration.class.php';
and should be replaced with:
require_once dirname(__FILE__).'/../symfony_PROJECT_NAME/config/ProjectConfiguration.class.php';

Symfony core lib

Now it's time to link symfony core libs to the project. There are some alternative ways to do that, one of them is to change the config/ProjectConfiguration.class.php again:

require_once dirname(__FILE__).'/../lib/vendor/symfony/lib/autoload/sfCoreAutoload.class.php';
shall be replaced with the appropriate path to the lib. But it's not the easiest way, since you'll force some difficulties with the symfony autoload mechanism. Better way is simply to create a symlink (execute from the lib/vendor directory):
ln -s ../../../symfony_1_4/ symfony
So our application is linked to the symfony lib which is inaccessible from the web, as well as the symfony application code (apps, config, lib, etc.) is inaccessible from the outside.

Alternative symfony core libs

Maybe you're curious why the symfony core lib directory is named symfony_1_4. In case you want to host many applications on one shared hosting account, there's no need to upload symfony libs into different paths (especially when their version is the same). Other applications may use this lib as well. Or you may want to host many applications on different versions of symfony, then you may have separate directories for each separate version of symfony:

/home/user
  /symfony_1_2
  /symfony_1_4 

If you want to host many symfony apps on one account, it's better to modify above structure. The best solution is to create subdomains (or any other way to access the site from a different URL) but never put everything (entire application structure) into apache's document root directory (e.g. public_html).