Doctrine ORM behaviors, или как эффективно использовать трейты

    С момента релиза php 5.4 уже прошло некоторое время, и мы решили поэкспериментировать с трейтами (traits), оценить их практическое применение. Как их можно использовать вместе с объектами Doctrine2?

    Трейты


    Трейты в php — это набор свойств и методов, которые можно добавить в класс.
    Они реализованы на уровне интерпретатора и являются абсолютно прозрачными для Doctrine.

    Трейты спроектированы для горизонтального повторного использования и идеально подходят для добавления общего поведения в несколько объектов.

    Общее поведение


    Часто нам необходимо сохранять время создания и обновления объектов, используя атрибуты created_at и updated_at. Этo поведение может быть применено к любым типам объектов. В таких ситуациях мы спрашиваем себя: «Как мне избежать повторения кодa каждый раз?».

    Timestampable behavior


    Timestampable — это простой трейт, который можно применить к объектам Doctrine:

    <?php
    
    use Doctrine\ORM\Mapping as ORM;
    
    use Knp\DoctrineBehaviors\ORM as ORMBehaviors;
    
    /**
     * @ORM\Entity
     */
    class Category
    {
        use ORMBehaviors\Timestampable\Timestampable;
    
        /**
         * @ORM\Id
         * @ORM\Column(type="integer")
         * @ORM\GeneratedValue(strategy="NONE")
         */
        protected $id;
    }
    

    Обратите внимание на оператор use внутри класса.

    Этот код добавит два свойства с Doctrine типом DateTime и два публичных метода для получения значений createdAt и updatedAt:

    <?php
    
    $category = new Category;
    $entityManager->persist($category);
    
    $category->getCreatedAt();
    $category->getUpdatedAt();
    

    После изменения объекта getUpdatedAt вернет дату его последнего обновления.

    Установка


    Timestampable и другие трейты собраны вместе в github репозитории KnpLabs/DoctrineBehaviours.

    Вы можете легко установить их, используя composer. Добавьте эти строки в файл composer.json в корневой папке вашего проекта.

    {
        "require": {
            "knplabs/doctrine-behaviors": "dev-master",
        }
    }
    

    Затем запустите composer:

    curl -s http://getcomposer.org/installer | php
    php composer.phar install
    

    Listeners


    Все это возможно благодаря Doctrine listeners, которые ожидают события persist или update для всех объектов, которые используют Timestampable.

    Но для того, чтобы все заработало, необходимо их зарегистрировать. Если вы используете Symfony2, это будет просто! Импортируйте файл с определениями сервисов.

    # app/config/config.yml
    
    imports:
        - { resource: ../../vendor/knplabs/doctrine-behaviors/config/orm-services.yml }
    

    Translatable behavior


    Очень часто наши приложения используют объекты, свойства которых должны быть доступны на нескольких языках. Мы постарались сделать трейт для таких случаев максимально простым.

    Для того, чтобы получить объект, свойства которого можно переводить, необходимо выполнить 2 шага.

    1. Используйте трейт Translatable

    <?php
    
    use Doctrine\ORM\Mapping as ORM;
    
    use Knp\DoctrineBehaviors\ORM as ORMBehaviors;
    
    /**
     * @ORM\Entity
     */
    class Category
    {
        use ORMBehaviors\Translatable\Translatable;
    
        /**
         * @ORM\Id
         * @ORM\Column(type="integer")
         * @ORM\GeneratedValue(strategy="NONE")
         */
        protected $id;
    }
    

    2. Создайте объект CategoryTranslation, которая будет использовать трейт Translation:

    <?php
    
    use Doctrine\ORM\Mapping as ORM;
    
    use Knp\DoctrineBehaviors\ORM as ORMBehaviors;
    
    /**
     * @ORM\Entity
     */
    class CategoryTranslation
    {
        use ORMBehaviors\Translatable\Translation;
    
        /**
         * @ORM\Column(type="string")
         */
        protected $name;
    }
    

    Вот и всё! TranslatableListener определит связь между этими двумя объектами автоматически. Вы можете работать с ними также, как с обычными связями OneToMany (например, вы можете использовать left join для получения переводов).

    <?php
    
    $category = new Category;
    $category->translate('ru')->setName('Обувь');
    $category->translate('en')->setName('Shoes');
    $em->persist($category);
    
    $category->translate('en')->getName();
    

    Tree


    Tree использует реализацию materialized path для работы с деревьями. Все ноды содержат свой полный путь от корня:

     | id  | name       | path       |
     +-----+------------+------------+
     | 1   | fr         | /1         |
     | 2   | villes     | /1/2       |
     | 4   | subNantes  | /1/2/3/4   |
     | 7   | en         | /7         |
     | 8   | villes     | /7/8       |
     | 9   | Nantes     | /7/8/9     |
     | 10  | subNantes  | /7/8/9/10  |
     | 11  | Lorient    | /7/8/11    |
     | 12  | Rouen      | /7/8/12    |
     | 6   | Rouen      | /1/2/6     |
     | 3   | Nantes     | /1/2/3     |
     | 5   | Lorient    | /1/2/5     |
    

    Для представления ваших объектов в виде дерева использует трейт Tree\Node

    <?php
    
    use Doctrine\ORM\Mapping as ORM;
    
    use Knp\DoctrineBehaviors\ORM as ORMBehaviors;
    
    /**
     * @ORM\Entity(repositoryClass="CategoryRepository")
     */
    class Category
    {
        use ORMBehaviors\Tree\Node;
    
        /**
         * @ORM\Id
         * @ORM\Column(type="integer")
         * @ORM\GeneratedValue(strategy="NONE")
         */
        protected $id;
    }
    

    Также необходимо использовать трейт Tree\Tree в соответствующем EntityRepository:

    <?php
    
    use Doctrine\ORM\EntityRepository;
    
    use Knp\DoctrineBehaviors\ORM as ORMBehaviors;
    
    class CategoryRepository extends EntityRepository
    {
        use ORMBehaviors\Tree\Tree;
    }
    

    Теперь объект предоставляет набор методов для работы с дочерними объектами, родителями и т.д.

    $root = $em->getRepository('Category')->getTree();
    
    $root->getParentNode();
    $root->getChildren();
    $root[0][1]; // array access of children
    $root->isLeafNode();
    $root->isRootNode();
    


    Заключение


    В данной статье мы рассмотрели несколько примеров практического применения трейтов. Помимо описанных трейтов вы можете ознакомиться с полным их перечнем в README-файле на github.

    Примечание. Данная статья является русской версией статьи, опубликованной в нашем блоге: Doctrine ORM behaviors, or how to use traits efficiently. Авторы английской версии: Константин Кудряшов (@everzet), Florian Klein, Leszek Prabucki. Перевод: Александр Торченко (@torchello).
    Share post

    Comments 17

      +1
      Для полной картины не хватает только ORMBehaviors\ActiveRecord :)
      Впрочем, никто не спорит, что трейты это вкусняшка.
        +3
        Главное — четко понимать, что трейты это миксины, а не полноценное множественное наследование.
          +4
          Трейты это не миксины. Это легализированный копипаст =)

          Что жаль, что трейты не связанные с интерфейсом. Т.е. нельзя проверить, например, включен ли в класс тот или иной трейт по интерфейсу. А это было бы очень даже неплохо.
            0
            Вообще-то, проверить можно, есть функция get_declared_traits
              0
              Если так, то скорее class_uses

              Но. По интерфейсу несомненно удобнее. По сути, интерфейсы и трейты решают одну и ту же проблему, странно, что им не дано работать вместе.

              Кроме того, существенное ограничение: This function returns an array with the names of the traits that the given class uses. This does however not include any traits used by a parent class.
                0
                >> По сути, интерфейсы и трейты решают одну и ту же проблему…
                По сути, совсем разные. Интерфейс описывает возможности класса, а трейт — их реализацию. Используя реализацию трейта в классе, вы можете переименовывать его методы или использовать только некоторые из них, таким образом, использование трейта классом никоим образом не гарантирует то, что класс будет реализовывать какой-то интерфейс.
                  0
                  имеется ввиду что-то типа.
                  trait Timestampable implements Timestampable {
                    function setCreatedAt();
                    function setUpdatedAt();         
                  }
                  


                  Пусть использование интерфейса будет опционально. Но было бы неплохо, если бы он был.
                  Просто хотелось бы адекватно реагировать в зависимости от того какой модуль подгружен.
                    0
                    class Order implements TimestampableInterface
                    {
                        use TimestampableTrait;
                    }
                    
                    if ($object instanceof TimestampableInterface) {
                        // here you go
                    }
                    


                    Посмотрите это и это, и поймёте, о чём я говорил в предыдущем комментарии.
                      0
                      Да, я смотрел. Но это не объясняет почему нет возможности привязывать интерфейс к трейтам. А прописывать для каждого трейта его же интерфейс, это немного напряжно: нужно знать какой интерфейс он реализует, и потом знать как это в коде проверить.

                      Короче, пока что это всё не принципиально. Просто трейты + интерфейсы были бы отличной независимой связкой кода с огромными возможностями.
                        0
                        Почему не объясняет?
                          0
                          Как это объясняет?
                          Класс должен реализовать интерфейс. Интерфейс передан с трейтом командой use. Всё. В чем конфликт?
                            0
                            Конфликт в том, что в таком случае нельзя будет переименовывать методы трейта и комбинировать одноимённые методы из разных трейтов. А сейчас такая возможность есть.
                              0
                              Почему же, можно, пока класс реализует интерфейс трейта.
              –3
              зашел сюда увидеть этот комментарий.
                0
                Увидели? Давайте обратно :)
              • UFO just landed and posted this here
                  0
                  Для этого достаточно описать интерфейс отдельно от трейта и реализовывать его в классах, которые используют трейт.

            Only users with full accounts can post comments. Log in, please.