Doctrine 2: аксессоры, мутаторы, fromArray(), toArray()

// Февраль 20th, 2012 // Doctrine 2

Всем хороша новая Doctrine, вот только нет там многих удобных функций, решающих повседневные проблемы. В этой статье я расскажу о том, как можно сделать Doctrine 2 более комфортной для разработки.

Основное нововведение Doctrine 2 в том, что классы моделей (entities) теперь ни от кого не наследуются. А значит нет функций, добавляемых раньше через Doctrine_Record. Особенно сбивает с толку отсутствие аксессоров и мутаторов для свойств модели. Я поначалу создавал их вручную, но потом понял, что лучше сделать один раз и качественно доступ к свойствам модели. Итак вот пошаговое руководство. Проверено на Doctrine 2.2

  1. Создаём класс DoctrineExtra\ORM\DomainObject.php (содержание файла ниже).
  2. Наследуем все классы модели от него.
  3. Обозначаем все свойства модели как public. Возможно лучше будет сделать их protected.
  4. Теперь вам становятся доступны аксессоры, мутаторы, функции fromArray() и toArray(). Причём toArray() работает рекурсивно по связям модели.

DoctrineExtra\ORM\DomainObject.php

<?php
namespace DoctrineExtra\ORM;

abstract class DomainObject implements \ArrayAccess
{
    public function offsetExists($offset) {
        return isset($this->$offset);
    }

    public function offsetSet($offset, $value) {
        $this->$offset = $value;
    }

    public function offsetGet($offset) {
        return $this->$offset;
    }

    public function offsetUnset($offset) {
        $this->$offset = null;
    }

    public function __get($offset) {
        return $this->offsetGet($offset);
    }

    public function __set($offset, $value = NULL) {
        if($this->offsetExists($offset)) {
            if($value == NULL) {
                $this->offsetUnset($offset);
            } else {
                $this->offsetSet($offset, $value);
            }
        }
    }

    public function fromArray($array = array())
    {
        $entity = $this;
        \array_walk($array, function(&$value, &$key) use(&$entity) {
            $entity->offsetSet ($key, $value);
        });
        return $this;
    }

    /**
     * Работает и для составных ключей тоже
     */
    public function getPK()
    {
        $em = \Zend_Registry::get('entityManager');
        $meta = $em->getClassMetadata(get_class($this));
        $pk = $meta->getIdentifierFieldNames();
        return $pk;
    }

    /**
     * Возвращает только непосредственный свойства модели
     * (без обхода связей)
     */
    public function toArrayDirectly()
    {
        return get_object_vars($this);
    }

/**
 * Рекурсивно по модели и её связям
 */

public function toArray() {
    $em = \Zend_Registry::get('entityManager');
    $pks = $this->getPK();
    $entity = $this;
    $values = array();
    $entityState = $em->getUnitOfWork()->getEntityState($this);

    // Для записей, не сохранённых в БД, возвращаем просто массив значений свойств
    if($entityState == \Doctrine\ORM\UnitOfWork::STATE_NEW) {
        return $this->toArrayDirectly();
    }
    array_walk($pks, function ($pk) use (&$entity, &$values) {
        $values[$pk] = $entity[$pk];
    });

    if($pks == NULL)
            throw new \Exception('Can not doing toArray() to model without loaded PK (id)');
    $tmpMergedMappings = array();
    $tmpFieldMappings = array();
    $tmpAssocMappings = array();
    $testObj = $em->getRepository(get_class($this))->findOneBy($values);
    $testJob = $testObj->job;
    $tmpFieldMappings = $em->getClassMetadata(get_class($this))->fieldMappings;
    $tmpAssocMappings = array_keys($em->getClassMetadata(get_class($this))->associationMappings);
    foreach($tmpFieldMappings as $fmKey => $fmValue) {
        if(is_object($this->$fmKey)) {
            if (get_class($this->$fmKey) == "DateTime" ) {
                switch ($tmpFieldMappings[$fmKey]["type"]) {
                    case "sndatetype":
                        $tmpMergedMappings[$fmKey] = $this->$fmKey->format('m/d/Y');
                        break;
                    // handle any custom types..
                    default:
                        $tmpMergedMappings[$fmKey] = $this->$fmKey->format('Y-m-d H:i:s');
                        break;
                }
            } else {
                // presume the default _id mapping...
                $key_id = $fmKey."_id";
                $tmpMergedMappings[$key_id] = $this->$key_id->id;
            }
        } else {
            $tmpMergedMappings[$fmKey] = $this->$fmKey;
        }
    }
    foreach($tmpAssocMappings as $amKey => $amValue) {
        $tmpKey = $amValue."_id";
        switch (get_class($this->$amValue)) {
            case "Doctrine\ORM\PersistentCollection":
                // dont do anything with these right now..
                break;
            default:
                // Trigger the loading via the proxy.
                if(method_exists($this->$amValue, 'forceEagerLoad')) {
                    $forced = $this->$amValue->forceEagerLoad();
                } else {
                    // Note: these classes dont have/inherit a forceEagerLoad() method,
                    // or we are trying to call it on something not set yet.
                    //var_dump(get_class($this->$amValue));
                    //var_dump($amValue);
                }
                if($this->$amValue) {
                    if($this->$amValue->id != null) {
                        $tmpMergedMappings[$tmpKey] = $this->$amValue->id;
                    }
                }
            break;
        }
    }
    return $tmpMergedMappings;
 }
}

Обратите внимание, что в процессе загрузки (bootstrap) в реестр (Zend_Registry) должен добавляться entityManager.
Удачного вам кодинга!

Ссылки

Implementing ArrayAccess for Domain Objects

Share

Спасибо!


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


15 Responses to “Doctrine 2: аксессоры, мутаторы, fromArray(), toArray()”

  1. Описка у тебя в конце:
    > в реестр (Zend_Gegistry)

  2. Andrew:

    На мой взгляд, выглядит нескоклько хардкорно.

  3. merlex:

    Fatal error: Cannot use object of type Entity\Student as array in /home/horoshev/www-data/kurs/www/app/Entity/Base.php on line 133

  4. merlex:

    Разобрался. Все таки забыл интерфейс заимплементить.

  5. merlex:

    Не работает рекурсивная сериализация

    просто нет никаких вложенных элементов

    смущает вот этот кусок кода

    $key_id = $fmKey.»_id»;
    108.$tmpMergedMappings[$key_id] = $this->$key_id->id;

    у меня внешние ключи имеют другой формат названия (совпадающий с названием праймари кея в присоединяемой таблице)

    • google.com Андрей Токарчук:

      Хм, у меня вроде работала. На досуге надо будет ещё разок протестить.

  6. merlex:

    А вот это вообще странный код.

    Это вообще проверялось?

    public function toArrayDirectly()
    {
    return get_object_vars($this);
    }

    данная штука просто вывалит ВСЕ свойства объекта, включая совсем неимеющие отношения к данным свойства. Добавленные уже самой доктриной.

    • google.com Андрей Токарчук:

      Да, она делает именно это. Потом либо разбирать их по маппингу модели, либо по другому получать :-)

  7. merlex:

    «Теперь вам становятся доступны аксессоры, мутаторы, функции fromArray() и toArray(). Причём toArray() работает рекурсивно по связям модели.»

    Вот еще интересно на какую глубину. Ведь приналичии двухсторнних связей вы легко получите бесконечную рекурсию.

    Статья содержит связь с Коментариями.

    Каждый Комментарий имеет связь со Статьей.

    Предположим мы сериализуем в массив статью рекурсивно, также сериализуем всее ее комментарии, котрые в свою очередь содержат статью, содержащую этиже комментарии …. и так бесконечно.

    Я вижу выход только в том чтобы явно указывать какие связи для данной сущьности мы будем раскрывать.

    • google.com Андрей Токарчук:

      Да, обработки циклических ссылок нет. Хорошая идея указывать раскрываемые сущности или глубину связей.

Комментировать