Doctrine Templates / Behaviours (for symfony/ullright)

http://doctrine.readthedocs.org/en/latest/en/manual/behaviors.html
http://darrendev.blogspot.co.at/2010/03/creating-doctrine-custom-behaviour-part.html
 

Templates

Templates allow you to add fields and methods to multiple models.
Templates are also the base class for more sophisticated behaviours including additional tables with relations.
The base class is "Doctrine_Template".

Add a column

plugins/ullCorePlugin/lib/behaviours/ullTestable/ullTestable.class.php or anywhere in a lib directory.

Let's add a column to the base model.

class ullTestable extends Doctrine_Template 
{
    public function setTableDefinition()
    {
        $this->hasColumn('my_column', 'string', 128);
    }
}

Usage in schema.yml

TestTable:
  actAs:
    ullTestable:

Rebuild model and TestTable has now a string column "my_column"

Using options

Set the option defaults in the behaviour template and use options in the template

class ullTestable extends Doctrine_Template 
{
  // Defaults
  protected $_options = array(
    'column_name'    => 'my_column',
    'column_type'    => 'string',
  ); 

  public function setTableDefinition()
  {
    $this->hasColumn(
      $this->_options['column_name'], 
      $this->_options['column_type']
    );
  }
}

Usage in schema.yml

TestTable:
  actAs:
    ullTestable:
      column_name: my_foo_column

Rebuild model and TestTable has now a string (from defaults) column called "my_foo_column"

Delegate Methods

Add methods to the invoking model.

class ullTestable extends Doctrine_Template 
{
  public function setTableDefinition()
  {
    $this->hasColumn('my_column', 'string', 128);
  }

  // Delegated method
  public function getMyColumnUpper()
  {
    // invoker = the record
    $invoker = $this->getInvoker();
    return strtoupper($invoker->my_column);
  }
}

Usage:

<?php
$record = new TestTable;
$record->my_column = 'Foo';
echo $record->getMyColumnUpper(); // Return "FOO"

Delegate Table Methods

class ullTestable extends Doctrine_Template 
{
  public function setTableDefinition()
  {
    $this->hasColumn('my_column', 'string', 128);
  }

  // Delegated table method - Add "TableProxy" suffix!
  public function findByMyColumnTableProxy($string)
  {
    $q = new Doctrine_Query;
    $q
      ->from('TestTable t')
      ->where('t.my_column = ?', $string)
    ;

    return $q->execute(); 

    //use something like this to access table functions of generated submodels
    //$subrecord = Doctrine::getTable($this->_plugin->getOption('className'))
    //  ->findOneById($subid);
  }
}

Usage:

<?php
$record = new TestTable;
$record->my_column = 'Foo';
$record->save();

$records = Doctrine::getTable('TestTable')->findByMyColumn('Foo');
var_dump($records->toArray());


Behaviours

Behaviours allow more complex functionality with extra tables and relations.

Record generator

Doctrine provides "Doctrine_Record_Generator".

Example: allow to add multiple email addresses to a base model

plugins/ullCorePlugin/lib/behaviours/ullTestable/ullTestableRecordGenerator.class.php or anywhere in a lib directory.

class ullTestableRecordGenerator extends Doctrine_Record_Generator
{
  /**
   * Merge options with template options
   * 
   * @param string $options 
   */
  public function __construct($options)
  {
    $this->_options = Doctrine_Lib::arrayDeepMerge($this->_options, $options);
  }  
  
  /**
   * Set additional options
   */
  public function initOptions()
  {
    // When used on a model named "TestTable" the result is a model called "TestTableEmail"
    $this->setOption('className', '%CLASS%Email');
  }

  public function setTableDefinition()
  {
    $this->hasColumn('email', 'string', 255, array(
      'email'  => true,
      // Email as primary key to allow multiple email addresses per parent model  
      'primary' => true 
    ));
    $this->hasColumn('created_at', 'timestamp', null, array('notnull' => true));
  }

  public function buildRelation()
  {
    $this->buildForeignRelation('Emails');
    $this->buildLocalRelation();
  }
}

 Now configure the behaviour in the template:

class ullTestable extends Doctrine_Template 
{
  /**
   * Register the behaviour = record generator
   * @param array $options
   */
  public function __construct(array $options = array())
  {
    parent::__construct($options);

    $this->_plugin = new ullTestableRecordGenerator($this->_options);
  }

  /**
   * Initialize the behavior
   */
  public function setUp()
  {
    $this->_plugin->initialize($this->_table);
  }
} 

 Usage:

$record = new TestTable;
$record['Emails'][]['email'] = 'bar@example.com';
$record['Emails'][]['email'] = 'baz@example.com';
$record->save();

Note: the model "TestTableEmail" is created on the fly and not instanciable.
To allow this, configure to generate model files on disk:

plugins/ullCorePlugin/lib/behaviours/ullTestable/ullTestableRecordGenerator.class.php:

class ullTestable extends Doctrine_Template 
{
  ...

  /**
   * Set additional options
   */
  public function initOptions()
  {
    $this->setOption('className', '%CLASS%Email');
    $this->setOption('generateFiles', true);
    $this->setOption('generatePath', sfConfig::get('sf_lib_dir') . '/model/doctrine');
  }

  ...

Record Listeners

Record listeners allow behaviours to use Doctrine record event and hooks like "preUpdate()" and so on.

plugins/ullCorePlugin/lib/behaviours/ullTestable/ullTestableRecordListener.class.php or anywhere in a lib directory.

class ullTestableRecordListener extends Doctrine_Record_Listener
{
  protected 
    $_options = array()
  ;


  /**
   * Constructor
   * 
   * @param array $options
   */
  public function __construct(array $options)
  {
    $this->_options = Doctrine_Lib::arrayDeepMerge($this->_options, $options);
  }


  /**
   * preInsert hook
   * 
   * Set the email's created_at date 
   * @see Doctrine_Record_Listener::preInsert()
   */
  public function preInsert(Doctrine_Event $event)
  {
    // record is the associated owner record of the behaviour 
    // in our example "TestTable"
    $record = $event->getInvoker();

    if (count($record->Emails))
    {
      foreach ($record->Emails as $email)
      {
        if (!$email->exists())
        {
          $email->created_at = date('c');
        }
      }  
    }
  }

 

 Now configure the listener in the template:

class ullTestable extends Doctrine_Template 
{

...

  /**
   * Add record listener
   *
   * @see Doctrine_Template::setTableDefinition()
   */
  public function setTableDefinition()
  {
    $this->addListener(new ullTestableRecordListener($this->_options));
  } 


...

} 

 

More about events and hooks: http://doctrine.readthedocs.org/en/latest/en/manual/event-listeners.html

Record Filters

Record filters allow to transparently set and get additional non-native properties of a record.

A classic example would be internationalisation (i18n). One could transparently access translated record properties which are delegated to a translation table.

Template:

class ullTestable extends Doctrine_Template 
{
  public function setUp()
  {
    $this->_table->unshiftFilter(new ullTestableRecordFilter($this->_options));
  }
}

RecordFilter

class ullTestableRecordFilter extends Doctrine_Record_Filter
{
 
  protected
    $_options = array()
  ;
 
  public function __construct(array $options)
  {
    $this->_options = Doctrine_Lib::arrayDeepMerge($this->_options, $options);
  }

  public function filterSet(Doctrine_Record $record, $name, $value)
  {
    echo "Setting $name $value\n"; 
   
    return $record;
  }

  public function filterGet(Doctrine_Record $record, $name)
  {
    echo "Getting $name\n";
   
    return 'foo';
  }
 
}