Дублирование объекта при попытке получить данные из таблицы с ключом DateTime

в 7:26, , рубрики: bug, datetime, Doctrine ORM, Doctrine2, метки: , ,

На днях я столкнулся со странной ошибкой в работе Doctrine2(версия 2.2.2).

Суть проблемы

При попытке получить данные (в виде массива объектов(Entity)) из таблицы с ключом типа DateTime, Doctrine возвращала массив состоящий из одного объекта (первой строки) и ссылок на него же.

Описание ошибки

Есть таблица, назовем её PublisherDailyStatistic, у которой есть два ключа (точнее ключ один, но составной):
1) PublisherID integer
2) StatisticDT DateTime
Соответственно, в эту таблицу записываются данные статистики по издателям, раз в день.
Допустим там уже есть данные:

PublisherID StatisticDT SomeContent
1 2012-04-15 00:00:00 test 15
1 2012-04-16 00:00:00 test 16
1 2012-04-17 00:00:00 test 17

Далее, в репозитории (~PublisherDailyStatisticRepository.php) есть функция которая вытаскивает из базы статистику по издателю за определённый промежуток времени.
Выглядит это так:

  public function getByPublisherAndDate($publisher_id, DateTime $startDt, DateTime $endDt)
  {
    $qb = $this->_em->createQueryBuilder()
              ->select('p')
              ->from('ModelEntityPublisherDailyStatistic, 'p')
              ->where('
p.publisherid=:publisherid')
              ->andWhere('
p.statisticdt>=:startdt')
              ->andWhere('
p.statisticdt<=:enddt')
              ->setParameters(array("publisherid"=>(int)$publisher_id,
                          "startdt"=>$startDt,
                          "enddt"=>$endDt));

    return $qb->getQuery()->getResult();
  }

* This source code was highlighted with Source Code Highlighter.

Вроде всё просто и понятно. Но вот, что мы получим при вызове getByPublisherAndDate(1, 2012-04-15, 2012-04-17):

array (size=3)
 0 =>
  
object(ModelEntityPublisherDailyStatistic)[398]
   private 'publisherid' => int 1
   private 'statisticdt' =>
    object(DateTime)[381]
     public 'date' => string '2012-04-15 00:00:00' (length=19)
     public 'timezone_type' => int 3
     public 'timezone' => string 'Europe/Moscow' (length=13)
   private 'somecontent' => string 'test 15' (length=7)
1=>
  object(ModelEntityPublisherDailyStatistic)[398]
   private 'publisherid' => int 1
   private 'statisticdt' =>
    object(DateTime)[381]
     public 'date' => string '2012-04-15 00:00:00' (length=19)
     public 'timezone_type' => int 3
     public 'timezone' => string 'Europe/Moscow' (length=13)
   private 'somecontent' => string 'test 15' (length=7)
2 =>
  object(ModelEntityPublisherDailyStatistic)[398]
   private 'publisherid' => int 1
   private 'statisticdt' =>
    object(DateTime)[381]
     public 'date' => string '2012-04-15 00:00:00' (length=19)
     public 'timezone_type' => int 3
     public 'timezone' => string 'Europe/Moscow' (length=13)
   private 'somecontent' => string 'test 15' (length=7)

* This source code was highlighted with Source Code Highlighter.

Неожиданно, правда?
При этом если изменить «return $qb->getQuery()->getResult();» на «return $qb->getQuery()->getArrayResult();», мы получим:

array (size=3)
 0 =>
  array (size=3)
   
'publisherid' => int 1
   'statisticdt' =>
    object(DateTime)[384]
     public 'date' => string '2012-04-15 00:00:00' (length=19)
     public 'timezone_type' => int 3
     public 'timezone' => string 'Europe/Moscow' (length=13)
   'somecontent' => string 'test 15' (length=7)
 1 =>
  array (size=3)
   'publisherid' => int 1
   'statisticdt' =>
    object(DateTime)[384]
     public 'date' => string '2012-04-16 00:00:00' (length=19)
     public 'timezone_type' => int 3
     public 'timezone' => string 'Europe/Moscow' (length=13)
   'somecontent' => string 'test 16' (length=7)
 2 =>
  array (size=3)
   'publisherid' => int 1
   'statisticdt' =>
    object(DateTime)[384]
     public 'date' => string '2012-04-17 00:00:00' (length=19)
     public 'timezone_type' => int 3
     public 'timezone' => string 'Europe/Moscow' (length=13)
   'somecontent' => string 'test 17' (length=7)

* This source code was highlighted with Source Code Highlighter.

То есть, верные данные.

Решение проблемы

Я стал разбираться в чем проблема, и сначала грешил на созданные мной описания объекта(entity) «PublisherDailyStatistic», но после того как мой вопрос на http://stackoverflow.com остался без ответа, я стал искать ошибку в коде Doctrine.
И я её нашел, в функции «createEntity», класса «UnitOfWork». Там есть следующий код:

    if ($class->isIdentifierComposite) {
      $id = array();

      foreach ($class->identifier as $fieldName) {
        $id[$fieldName] = isset($class->associationMappings[$fieldName])
          ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
          : $data[$fieldName];
      }

      $idHash = implode(' ', $id);
    } else {
      $idHash = isset($class->associationMappings[$class->identifier[0]])
        ? $data[$class->associationMappings[$class->identifier[0]]['joinColumns'][0]['name']]
        : $data[$class->identifier[0]];

      $id = array($class->identifier[0] => $idHash);
    }

* This source code was highlighted with Source Code Highlighter.

В нашем случае, ошибка проявляется в первой части оператора if (так как, у нас составной ключ, но ошибка есть и во второй части). Как мы видим, в коде идет перебор всех ключей таблицы, и данные этих ключей добавляются в массив $id, и после из этих данных формируется строка $idHash, через обычный «implode». Позже, по значению $idHash, встречались ли такие данные, при создании объекта, ранее (т. к., два объекта с одинаковыми ключами — это один и тот же объект).Вот, тут то всё и ломается, так как в нашем случае, в результате функции implode, мы получим один и тот же $idHash для всех строк, в нашем случае «1 ».
Это вызвано тем, что implode игнорирует объект DateTime (как и любой другой объекты, в котором нет магической функции __toString() ).
Я написал разработчикам об этой ошибки в их багтрекер.
Пока же, вы можете легко исправить эту ошибку в своем проекте добавив в цикл строку проверки:

if ($id[$fieldName] instanceof DateTime)
{
   $id[$fieldName] = $id[$fieldName]->getTimestamp();
}

* This source code was highlighted with Source Code Highlighter.

Это моя первая статья на хабре, прошу не судить строго, все замечания приму к сведению.
И извините за то что написал так много текста о такой маленькой ошибки.

Автор: MaxRaccoon

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js