На днях я столкнулся со странной ошибкой в работе 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