Нашел сегодня еще одну очень неприятную багу. На самом деле она очень серьезная, и в отдельных случаях может не только доставить неудобства, но и к серьезным ошибкам привести.
Когда мы выполняем $modx->getCollection($class), мы получаем массив объектов. Но это не простой нумерованный массив (типа array(0 => object, 1 => object) и т.д.). В этом массиве у нас каждый ключ — это id объекта, то есть ключи часто идут не по порядку (3,7,9,57 и т.д.). Те, кто использует ->getMany(), знают — этот метод возвращает связанные объекты. По сути этот метод внутри себя выполняет тот же самый getCollection(). Но проблема кроется всего в одной строчке:
$this->_relatedObjects[$alias]= array_merge($this->_relatedObjects[$alias], $collection);
То есть все связанные объекты попадают в общий массив $this->_relatedObjects. Уточняю: getMany() не просто делает выборку getCollection(), но и набивает эти объекты в общий пулл связанных объектов. И с этим массивом связанных объектов $this->_relatedObjects можно (а иногда и нужно) работать. Так вот, проблема в том, что array_merge не сохраняет ключи-id объектов, а набивает все в новый нумерованный массив. Фокус хорошо демонстрирует вот этот пример:
<?php $a = array( 2=>'object' ); $b = array_merge($a, array( 5=>'object', 7 => 'object', )); print_r($b);
На выходе мы получаем:
Array ( [0] => object [1] => object [2] => object )
Последствия (от неудобств до проблем)
Неудобство заключается вот в чем: Допустим, мы знаем, что уже получали все зависимые объекты, и теперь просто хотим проверить наличие объекта по его id. У наших объектов есть id-шники 7,9 и 12. По идее мы имеем возможность по id обратиться к ключу массива, то есть $object->_relatedObjects[$alias][7] должен нам вернуть нужный нам объект. Ан нет. Из-за этой баги у нас там теперь массив с ключами 0,1,2… То есть объект мы уже потеряли.
А что произойдет, если мы опять выполним $object->getMany()? По сути 3 объекта он уже в себе имеет. Но из-за этой фишки с array_merge, мы получим уже 6 объектов, так как в новом массиве объектов ключи 7,9 и 12, а в текущем массиве 0,1,2, и теперь станет еще и 3,4,5. А если у нас id-шники 3453,6757,90980 и мы выполним getMany() много раз? :-)
Еще пример.
if(!$room = $modx->getObject('modResource', 107)){return '';} // Получаем все связанные объекты $vars = $room->getMany('Variables'); print count($room->_relatedObjects['Variables']); // Считаем - 2 объекта // Получаем один из имеющихся объектов $var = current($room->_relatedObjects['Variables']); // Добавляем его же $room->addMany($var); print count($room->_relatedObjects['Variables']); // Уже 3 объекта
По сути это тоже самое, просто другим путем. Но суть в чем: вот одна строчка из этого метода
$this->_relatedObjects[$alias][$objpk]= $obj;
То есть затирается элемент массива объектов с ключем id этого объекта. Но по нашей схеме в зависимости от того, какой id у объекта, он во-первых, может затереть вообще другой объект (а тот мог быть измененным, а значит при сохранении главного объекта этот объект уже не будет сохранен), а во-вторых, этот новый объект может встать или впереди, или позади своей же копии, но эти две копии могут отличаться (новый объект перед добавлением изменили), так вот в зависимости от их очередности, при сохранении главного объекта, эти объекты будут сохранены, и не факт, что в правильной исторической последовательности.
Вот как-то так…
Если такая возможность появится штатно, будет очень здорово! Как всегда, Спасибо тебе за статью!
Привет, Илья! Да, результат addExtensionPackacge() — это как раз запись в эту настройку. Так что и так, и так можно.
Надо попробовать добавить в extension_packages строчку и для процессоров. Тогда вообще не придется задумываться, что у меня какие-то левые объекты или процессоры — весь код будет стандартным))))
Не получится здесь одной строчкой обойтись. Задача того, что я делал — возможность подгружать любой процессор из любого пакета, при чем с автоформированием пути. Там слишком много тонкостей. Плюс мой метод возвращает реальное имя класса (сам знаешь, в процессорах используется return 'classname';) Все это никак не укладывается в одну строчку. Еще момент — ты вот своим методом подгрузишь мой процессор, а у меня там используется $this->loadClass(), а у тебя $this будет другим объектом. И все, код разваливается. Здесь любое решение будет костылями (и лучше использовать одни костыли). Решить это можно только тогда, когда Джейсон новый релиз выпустит, учитывающий все эти моменты. Мы вчера с ним основательно пообщались на этот счет. Уверен, он и других людей мнение учитывает, так что будет стандартное решение.
Кстати, есть второй способ прописать пакет, чтобы он подключался при инициализации MODx (по идее это тот же способ, только ручками): заходим в настройки системы, раздел «Система и сервер» и находим там параметр «Пакеты расширений» (extension_packages). Если мы до этого хоть раз запускали addExtensionPackage(), то в его значении уже будут все параметры в JSON — их можно исправить, что-нибудь добавить к ним… Если addExtensionPackage() не запускали, то можно там прописать ручками:
[{"Rehab": { "path":"[[++core_path]]components/rehab/model/", "tablePrefix":"modx_rehab_", "serviceName":"Rehab", "serviceClass":"Rehab" } }]
(конечно, в одну строчку)
Для разных нужных методов, которые используются в разных местах я создал отдельный класс Rehab (в папке пакета) и в нем прописываю функционал. Благодаря этому я в любом месте могу написать
$modx->Rehab->phonesToJSON($phones);
и массив обработается с нужными проверками, дефолтными значениями. И результат будет каким надо. Если потребуется изменить алгоритм обработки телефонов, я всегда знаю, где искать этот код.
А вот пути для процессоров прописывать приходится, конечно… Надо попробовать добавить в extension_packages строчку и для процессоров. Тогда вообще не придется задумываться, что у меня какие-то левые объекты или процессоры — весь код будет стандартным))))
Да. Но дело не только в радости глаза. Уверен, Джейсон сделает все красиво, с неймспейсами и т.п., и будет полная автоподгрузка. На сколько я слышал, у него наработки есть с существенными плюсами по производительности.
Для отступов использовать 4 пробела вместо табуляции.
всегда юзал пробелы, где мог) табуляция меня раздражает))
Именовать классы НадоТак
а я ещё люблю префиксы-аббревиатуры делать, так сказать группируя по неким общим признакам (напр. имя пакета — нпмВотТак).
практически все остальное совпадает со стандартами, наверно работая с modx/xpdo само-собой подстроилось) такой код всегда радует глаз
Не так давно мы с Иваном vanchelo часа два убили, пытаясь разобраться в одной мистике, которая до конца нам так и не далась. Плюс к этому на облаке при вызове метода exit() постоянно вываливаются ошибка Catchable fatal error: Argument 1 passed to xPDOObject::load() must be an instance of xPDO, instance of modX given in www/core/xpdo/om/xpdoobject.class.php on line 404. Скорее всего эти оба момента взаимосвязаны, и Иван правильно предполагал по поводу APC, во всяком случае сейчас получил ответ от Шона:
It's because of APC and sessions. You need to call this before calling exit() or die(): @session_write_close();
Так что имеем ввиду, на всякий случай.
Вот потому пусть лучше related остается на месте :-)