Symfony2: Propel Behavior [Перевод]

Behavior’ы являются отличным способом упаковать расширения моделей для повторного использования. Это мощный, универсальный, быстрый способ организации кода.

Хуки

Методы save() и delete(), для сгенерированных объектов также просто переопределить.
Propel ищет один из следующих методов в ваших объектах и выполняет их, когда это необходимо:

<?php
// save() hooks
preInsert()            // код выполняется перед вставкой нового объекта
postInsert()           // после вставки нового объекта
preUpdate()            // code executed before update of an existing object
postUpdate()           // code executed after update of an existing object
preSave()              // code executed before saving an object (new or existing)
postSave()             // code executed after saving an object (new or existing)
// delete() hooks
preDelete()            // code executed before deleting an object
postDelete()           // code executed after deleting an object

Например, можно отслеживать дату создания (created_at) каждой строки в таблице book. Для этого добавим для таблицы book поле created_at в файле структуры schema.xml.

<table name="book">
  ...
  <column name="created_at" type="timestamp" />
</table>

Теперь опишем поведение модели — устанавливать дату в поле created_at при каждой вставке записи в табл. book:

<?php
class Book extends BaseBook
{
  public function preInsert(PropelPDO $con = null)
  {
    $this->setCreatedAt(time());
    return true;
  }
}

После этого при каждом сохранении объекта, вызове метода save(), — Propel будет выполнять метод preInsert() для этого объекта и соответственно устанавливать дату создания записи. Проверим?

<?php
$b = new Book();
$b->setTitle('PHP за 24 часа');
$b->save();
echo $b->getCreatedAt(); // 2012-12-17 11:30:00
Все pre методы должны возвращать значение типа boolean. Возвращая значение отличное от true — вы отменяете выполнение базовых методов save() и delete(). Таким образом можно не только дополнять поведения, но и полностью переопряделять логику.

Behaviors

Если Вы создали несколько классов моделей со схожими методами — настало самое время провести рефакторинг.
Например, добавим поведение установки даты при сохранении для других моделей.
Назовем это поведение Timestampable behavior, поскольку все записи имеют отметку о дате создания.
Чтобы получить такое поведение для других моделей, мы можем скопипастить метод в каждый класс модели или предотвратить относительно скорый кошмар, который настанет от дублирования кода. Представьте какой объем кода может быть продублирован в реальном проекте.

Propel предлагает три способа для рефакторинга общего поведения моделей:

  1. Использование кастомных builder’ов в процессе сборки. Это работает, если все модели имеют одно и то же поведение.
  2. Наследование таблиц. Однако наследование методов часто ограничивает возможности.
  3. И последний — использование Propel behaviors, самый правильный подход для организации общей логики в моделях.

Behaviors (бихейверы, поведение) — это специальные объекты, которые вызывают события в процессе сборки для расширения сгенерированных моделей. Бихейверы позволяют добавлять свойства и методы в классы моделей и Peer классы, могут изменять/дополнять логику некоторых сгенерированных методов, бихейверы могут даже менять структуру БД, добавляя новые таблицы и поля.

Вообще Propel’а имеет стандартный timestampable behavior, который делает то же самое, что мы реализовали выше — установка даты создания и обновления записи. Так, вместо добавления поля и метода вручную, нам нужно только добавить атрибут <behavior> в схему БД (schema.xml) для нужной таблицы:

<table name="book">
  ...
  <behavior name="timestampable" />
</table>
<table name="author">
  ...
  <behavior name="timestampable" />
</table>

Теперь нужно сгенерить заново модели (php app/console propel:model:build) и мы увидим 2 дополнительных поля: created_at, updated_at. Базовые классы моделей будут содержать код для автоматической установки даты при вставке и обновлении записи (объекта).

Дополнительные Behavior бандлы Propel

Также можно посмотреть behavior‘ы предоставляемые пользователями.

Кастомизация Behaviors

Бихейверы часто предлагают некоторые параметры для настройки их действий. Например, timestampable бихейвер позволяет задать кастомные имена для полей хранящих даты создания и обновления записи. Изменим имена полей на created_on и updated_on:

<table name="book">
  ...
  <behavior name="timestampable">
    <parameter name="create_column" value="created_on" />
    <parameter name="update_column" value="updated_on" />
  </behavior>
</table>

Если поля уже описаны (добавлены) в схеме таблицы, бихейвер достаточно умен чтобы не пытаться добавить эти поля повторно:

<table name="book">
  ...
  <column name="created_on" type="timestamp" />
  <column name="updated_on" type="timestamp" />
  <behavior name="timestampable">
    <parameter name="create_column" value="created_on" />
    <parameter name="update_column" value="updated_on" />
  </behavior>
</table>

Использование сторонних behavior’ов

В Propel бихейверы могут быть упакованы в один класс, поэтому их не только легко применять для разных моделей, но и переносить между разными проектами. Достаточно скопировать файл-класс behavior’а и объявить его build.properties:

propel.behavior.timestampable.class = propel.engine.behavior.timestampable.TimestampableBehavior
# Добавляем путь к классу кастомного бихейвера
propel.behavior.formidable.class = path.to.FormidableBehavior

Propel будет искать FormidableBehavior класс каждый раз, когда в схеме используется formidable бихейвер.

<table name="author">
  ...
  <behavior name="formidable" />
</table>

Применение behavior’а для всех таблиц БД

Бихейвер можно применить к тегу <database> и тем самым установить behavior для всех таблиц БД:

<database name="propel">
  <behavior name="timestampable" />
  <table name="book">
    ...
  </table>
  <table name="author">
    ...
  </table>
</database>

..а можем применить behavior для всех БД:

propel.behavior.default = archivable, timestampable

Пишем свой behavior

Лучшим началом при написании кастомных бихейверов будет просмотр стандартных behaviors бандлов.

Бихейвер изменения модели

Бихейвер может изменить свою таблицу или добавить другую таблицу путем реализации метода modifyTable. В этом методе можно получить текущую таблицу для изменения через метод $this->getTable().

<?php
class MyBehavior extends Behavior
{
  // default parameters value
  protected $parameters = array(
    'column_name' => 'foo',
  );

  public function modifyTable()
  {
    $table = $this->getTable();
    $columnName = $this->getParameter('column_name');
    // добавить поле, если не существует
    if( !$this->getTable()->containsColumn($columnName)) {
      $column = $this->getTable()->addColumn(array(
        'name'    => $columnName,
        'type'    => 'INTEGER',
      ));
    }
  }
}

Бихейвер изменения модели

Бихейвер может добавить код в сгенерированной объектной модели путем реализации одного из следующих методов:

objectAttributes()     // add attributes to the object
objectMethods()        // add methods to the object
preInsert()            // add code to be executed before insertion of a new object
postInsert()           // add code to be executed after  insertion of a new object
preUpdate()            // add code to be executed before update of an existing object
postUpdate()           // add code to be executed after  update of an existing object
preSave()              // add code to be executed before saving an object (new or existing)
postSave()             // add code to be executed after  saving an object (new or existing)
preDelete()            // add code to be executed before deleting an object
postDelete()           // add code to be executed after  deleting an object
objectCall()           // add code to be executed inside the object's __call()
objectFilter(&$script) // do whatever you want with the generated code, passed as reference

Бихейвер изменения Query-классов

queryAttributes()     // add attributes to the query class
queryMethods()        // add methods to the query class
preSelectQuery()      // add code to be executed before selection of a existing objects
preUpdateQuery()      // add code to be executed before update of a existing objects
postUpdateQuery()     // add code to be executed after  update of a existing objects
preDeleteQuery()      // add code to be executed before deletion of a existing objects
postDeleteQuery()     // add code to be executed after  deletion of a existing objects
queryFilter(&$script) // do whatever you want with the generated code, passed as reference

Бихейвер изменения Peer-классов

staticAttributes()   // add static attributes to the peer class
staticMethods()      // add static methods to the peer class
preSelect()          // adds code before every select query
peerFilter(&$script) // do whatever you want with the generated code, passed as reference

Добавление новых классов

Бихейвер может добавить совершенно новые классы на основе модели данных. Для создания нового класса, бихейвер должен вернуть массив имен builder-класса в методе getAdditionalBuilders() и сам builder классов.

<?php
require_once 'AddChildBehaviorBuilder.php';

class AddChildBehavior extends Behavior
{
    protected $additionalBuilders = array('AddChildBehaviorBuilder');
}

Дальше пишем builder-расширение OMBuilder класса и реализуем методы getUnprefixedClassName(), addClassOpen() и addClassBody():

<?php
class AddChildBehaviorBuilder extends OMBuilder
{
  public function getUnprefixedClassname()
  {
    return $this->getStubObjectBuilder()->getUnprefixedClassname() . 'Child';
  }

  protected function addClassOpen(&$script)
  {
    $table = $this->getTable();
    $tableName = $table->getName();
    $script .= "
/**
 * Test class for Additional builder enabled on the '$tableName' table.
 *
 */
class " . $this->getClassname() . " extends " . $this->getStubObjectBuilder() . "
{
";
  }

  protected function addClassBody(&$script)
  {
    $script .= "  // no code";
  }

  protected function addClassClose(&$script)
  {
    $script .= "
}";
  }
}

По умолчанию, классы добавленные бихейвером генерируются каждый раз, когда мы генерируем классы моделей: php app/console propel:model:build.
Чтобы отменить перезапись и генерацию нового класса, можно добавить public свойство $overwrite в бюилдер и установить ему значение false.

Также с помощью бихейверов можно заменить или удалить существующие методы и добавлять новые интерфейсы для созданного класса.
Подробнее..