Pull to refresh

Doctrine Behavior на примере собственного плагина

Symfony *
Здравствуй, хабралюд.

Вступление


С выходом symfony 1.4 разработчики фреймворка фактически обязали нас использовать вместо привычного Propel, новую, мною неизведанную ORM Doctrine. Нет, конечно они не заставляют использовать Doctrine, при желании в 1.4 можно подключить и Propel, но как мне показалось — если разработчики такого масштаба сделали Doctrine по–умолчанию в своём фреймворке, то значит это говорит о большей пригодности нежели Propel. Я не стал противиться ещё по той причине, что просто напросто хотелось чего–нибудь нового и стал работать с Doctrine.

В связи с появившейся задачей, с желанием повысить собственную квалификацию и просто из интереса решил попробовать на себе, что такое Doctrine Behaviors, а полученным опытом поделиться с вами. Как писать плагины для symfony framework'a я уже рассказывал, на этот раз хотелось бы рассказать о написании плагина, который использует «Doctrine Behavior».

Поехали


Всё это будет рассматриваться на примере плагина рейтинга. То есть плагин позволяет голосовать (получать средний рейтинг, количество всех голосов и много другого) за любой объект модели у которого есть поле ID. Так как бехейверы имеют тенденцию в своих названиях оканчиваться на «able» (Timestampable, Commentable) я назвал свой плагин (он же по совместительству бехейвер) — Superable (sfSuperablePlugin).

Итак, в директории «plugins» создаем директорию «sfSuperablePlugin». В ней «lib/doctrine/template», а внутри этой директории файл «Superable.class.php» — это и есть шаблон для расширения основной модели к которой привязан этот самый бехейвер. Содержание файла таково:
  1. <?php
  2.  
  3. /**
  4.  * Superable (rateable) behavior
  5.  *
  6.  * @author Igor S. Chernyshev
  7.  */
  8. class Superable extends Doctrine_Template {
  9.   protected $_options = array();
  10.   protected $_plugin;
  11.  
  12.   public function __construct(array $options = array())
  13.   {
  14.     parent::__construct($options);
  15.   }
  16.  
  17.   public function setTableDefinition()
  18.   {
  19.     $this->hasColumn('average_rating', 'float', '4', array(
  20.       'notnull' => true,
  21.       'default' => 0
  22.     ));
  23.     $this->hasColumn('total_votes', 'integer', '4', array(
  24.       'notnull' => true,
  25.       'default' => 0
  26.     ));
  27.     $this->hasColumn('votes_sum', 'integer', '4', array(
  28.       'notnull' => true,
  29.       'default' => 0
  30.     ));
  31.   }
В конструктор передаются различного рода опции, которые мы отправляем в родительский конструктор. Метод setTableDefinition () описывает необходимые, дополнительные поля к основной модели (average_rating — средний рейтинг, total_votes — общее количество голосов, votes_sum — общая сумма всех голосов). Метод setTableDefinition () вызывается автоматически при построении моделей (php symfony doctrine: build-model (forms, filters)).

Далее в вашу schema.yml к модели, которая нуждается в бехейвере дописываем actAs, выглядит это примерно так:
  1. Photo:
  2.   actAs:
  3.     Superable:
  4.   columns:
  5.     id:
  6.       type: integer(4)
  7.       primary: true
  8.       autoincrement: true
Затем
  1. php symfony cc
  2. php symfony doctrine:build-model
  3. php symfony doctrine:build-forms
  4. php symfony doctrine:build-filters
  5. php symfony doctrine:build-sql
После этих не хитрых действий в «data/sql/schema.sql» можно увидеть, что к таблице Photo добавлено три дополнительных поля описанных в setTableDefinition ().
Что ж, расширить модель у нас уже получилось, но нужна ещё дополнительная таблица, которая будет хранить в себе всю историю голосов за объекты модели Photo. Для это понадобиться создать файл «plugins/sfSuperablePlugin/lib/doctrine/generator/SuperableGenerator.class.php» со следующим содержанием:
  1.  
  2. <?php
  3.  
  4. /**
  5.  * Superable (rateable) behavior
  6.  *
  7.  * @author Igor S. Chernyshev
  8.  */
  9. class SuperableGenerator extends Doctrine_Record_Generator
  10. {
  11.   public function  __construct(array $options = array())
  12.   {
  13.     $this->_options = Doctrine_Lib::arrayDeepMerge($this->_options, $options);
  14.   }
  15.  
  16.   public function initOptions()
  17.   {
  18.     $builderOptions = array('suffix' => '.class.php',
  19.                             'baseClassesDirectory' => 'base',
  20.                             'generateBaseClasses' => true,
  21.                             'generateTableClasses' => true,
  22.                             'baseClassName' => 'sfDoctrineRecord');
  23.  
  24.     $this->setOption('builderOptions', $builderOptions);
  25.     $this->setOption('className', '%CLASS%Superable');
  26.     $this->setOption('generateFiles', true);
  27.     $this->setOption('generatePath', sfConfig::get('sf_lib_dir') .
  28.                                      DIRECTORY_SEPARATOR . 'model' .
  29.                                      DIRECTORY_SEPARATOR.'doctrine');
  30.   }
  31.  
  32.   public function setTableDefinition()
  33.   {
  34.     $this->setTableName($this->_options['table']->getTableName() . '_superable');
  35.     $this->hasColumn('id', 'integer', 4, array(
  36.       'type' => 'integer',
  37.       'primary' => true,
  38.       'autoincrement' => true,
  39.       'length' => '4',
  40.     ));
  41.     $this->hasColumn($this->getRelationLocalKey(), 'integer', 4, array(
  42.       'type' => 'integer',
  43.       'length' => '4',
  44.     ));
  45.     $this->hasColumn('user_id', 'integer', 4, array(
  46.       'type' => 'integer',
  47.       'length' => '4',
  48.     ));
  49.     $this->hasColumn('vote', 'integer', 4, array(
  50.       'type' => 'integer',
  51.       'length' => '4',
  52.     ));
  53.     $this->hasColumn('created_at', 'date');
  54.  
  55.     $this->addListener(new SuperableListener());
  56.   }
  57.  
  58.   public function generateClassFromTable(Doctrine_Table $table)
  59.   {
  60.     $definition = array();
  61.     $definition['columns']   = $table->getColumns();
  62.     $definition['tableName'] = $table->getTableName();
  63.     $definition['actAs']     = $table->getTemplates();
  64.     $definition['relations'] = $table->getRelations();
  65.  
  66.     return $this->generateClass($definition);
  67.   }
  68.  
  69.   public function getRelationLocalKey()
  70.   {
  71.     return $this->_options['table']->getTableName() . '_id';
  72.   }
  73.  
  74.   public function buildRelation()
  75.   {
  76.     $this->buildForeignRelation('Votes');
  77.     $this->buildLocalRelation(ucfirst($this->_options['table']->getTableName()));
  78.   }
  79. }
  80.  
  81. ?>
  82.  
Этот генератор как раз отвечает за генерирование новых моделей (в моём случае PhotoSuperable.class.php, PhotoSuperableTable.class.php), форм, фильтров и так далее. В setTableDefinition () мы опять таки описываем поля, которые будут сгенерированы для новой таблицы. Метод $this→_options['table']→getTableName () возвращает имя таблицы модели к которой привязан бехейвер (в данном случае, если actAs стоит у таблицы photo — метод соответственно вернет photo). В методе initOptions () всё прозрачно, как мне кажется. В нём описывается как будет назван класс модели, куда будет сохранен и так далее. Все остальные методы, опять же, с говорящими названиями, которые я описывать не буду.

Если вы заметили в методе setTableDefinition () вызывается метод $this→addListener (new SuperableListener ()) — это слушатель, а что он делает вы сейчас узнаете. Создайте файл «plugins/sfSuperablePlugin/lib/doctrine/listener/SuperableListener.class.php» с таким содержанием:
  1. <?php
  2.  
  3. /**
  4.  * Superable (rateable) behavior
  5.  *
  6.  * @author Igor S. Chernyshev
  7.  */
  8. class SuperableListener extends Doctrine_Record_Listener
  9. {
  10.   public function preInsert(Doctrine_Event $event)
  11.   {
  12.     $event->getInvoker()->created_at = date('Y-m-d', time());
  13.   }
  14. }
  15.  
  16. ?>
Я думаю, что тут прекрасно видно, что слушатель этот при сохранение записи в таблицу с историей каждому объекту устанавливает актуальную дату на момент сохранения.

Теперь для того что бы новая модель (PhotoSuperable) генерировалась при построении моделей, форм и фильтров необходимо инициализировать её в шаблоне (Superable.class.php) добавив метод:
  1.   public function setUp()
  2.   {
  3.     $this->hasMany($this->getTable()->getComponentName() . 'Superable as Votes',
  4.                    array('local'   => 'id',
  5.                          'foreign' => $this->getTable()->getTableName() . '_id'));
  6.  
  7.     $this->_plugin->initialize($this->_table);
  8.   }
И строчку в конструктор:
  1.   public function __construct(array $options = array())
  2.   {
  3.     parent::__construct($options);
  4.     $this->_plugin  = new SuperableGenerator();
  5.   }
После всех этих действий опять приступаем к:
  1. php symfony cc
  2. php symfony doctrine:build-model
  3. php symfony doctrine:build-forms
  4. php symfony doctrine:build-filters
  5. php symfony doctrine:build-sql
И видим, что в сгенерированном schema.sql появилась новая таблица photo_superable со следующими полями: id, photo_id, user_id, vote, created_at.

Осталось только в шаблон добавить непосредственно методы для голосования и так далее, для этого в Superable.class.php:
  1.   /********************
  2.    * Template methods *
  3.    ********************/
  4.  
  5.   protected $_names = array();
  6.   protected $_vote;
  7.   protected $_userId;
  8.  
  9.   /**
  10.    * Initialize properties for superable methods
  11.    *
  12.    * @param int $vote Values of vote
  13.    * @param int $userId User's ID
  14. <div style="font: normal normal 1em/1.2em monospac
Tags:
Hubs:
Total votes 16: ↑14 and ↓2 +12
Views 159
Comments Comments 36