Magento для PHP MVC розробників – Колекції Varien Data (ч.9/11)
В основному, якщо програміст PHP хотів зібрати разом групу пов’язаних змінних, у нього був один вибір – використовувати масив. Хоча масив у мові C пов’язаний із масивом адрес пам’яті, у PHP масив є словником загального призначення, як об’єкт, що об’єднаний за відношеннями зміненого масиву з чисельною індексацією.
В інших мовах програмування вибір не такий простий. Адже вам надаються багатоланцюгові структури даних на вибір, кожна з яких має особливі переваги в швидкості, зберіганні та семантиці. Філософія PHP полягала в тому, щоб прибрати цей вибір у клієнтському програмуванні і надати одну повнофункціональну структуру даних, яка буде “достатньо доброю”. Я давно вважав, що PHP масиви є величезною причиною такої популярності платформи.
Все це було причиною започаткування певного типу програмування, і тоді версія PHP 5 вирішила змінити статус-кво, пропонуючи вбудовані класи та інтерфейси, які дозволяють створювати свої власні структури даних.
$array = new ArrayObject(); class MyCollection extends ArrayObject{...} $collection = new MyCollection(); $collection[] = 'bar';
Хоча це як і раніше підтримує певний тип програмування, оскільки ви не маєте доступу до деталей виконання низького рівня, але ви можете створювати об’єкти, подібні до масивів, з методами, які інкапсулюють специфічні функції.
Ви також можете встановити правила рівня типів безпеки, дозволяючи лише певні види Об’єктів у вашій Колекції.
Вас не повинно дивувати, що Magento пропонує теж ряд таких Колекцій. Насправді, кожен об’єкт Моделі, який слідує інтерфейсу Magento, отримує Колекцію типів безкоштовно. Розуміння того, як ці Колекції працюють – це є ключовою частиною, яка вирізняє ефективного Magento-програміста. Ми збираємося оглянути Колекції в Мадженто, починаючи з низу і прямуючи вгору. Налаштуйте екшен у контролері, в якому ви зможете запускати довільний код, і давайте почнемо.
Колекція речей
По-перше, ми збираємося створити кілька нових об’єктів.
$thing_1 = new Varien_Object(); $thing_1->setName('Richard'); $thing_1->setAge(24); $thing_2 = new Varien_Object(); $thing_2->setName('Jane'); $thing_2->setAge(12); $thing_3 = new Varien_Object(); $thing_3->setName('Spot'); $thing_3->setLastName('The Dog'); $thing_3->setAge(7);
Клас Varien_Object
визначає той об’єкт, з якого успадковуються всі Magento-моделі. Це загальний шаблон у об’єктно-орієнтованих систем, і він забезпечує те, що ви завжди будете мати можливість легко додавати методи/функціонал до кожного об’єкта у вашій системі без необхідності редагувати кожен файл класу.
Будь-який об’єкт, який розширюється від Varien_Object
має магічні getter
і setter
методи, які можуть бути використані для встановлення властивостей даним. Давайте спробуємо:
var_dump($thing_1->getName());
Якщо ви не знаєте точне ім’я властивості, ви можете витягнути всі дані у вигляді масиву:
var_dump($thing_3->getData());
Результатом цього стане щось типу:
array 'name' => string 'Spot' (length=4) 'last_name' => string 'The Dog' (length=7) 'age' => int 7
Зверніть увагу на властивість з ім’ям “last_name
“. Якщо у властивості є нижнє підкреслення, її можна подати у вигляді верблюжого регістру і використовувати у магічних методах:
$thing_1->setLastName('Smith');
Можливість такої реалізації – це частина сили PHP5, і стилю розробки певного класу, який люди розуміють під “Об’єктно-орієнтованим Програмуванням”.
Отже, тепер у нас є деякі Об’єкти, давайте додамо їх до Колекції. Пам’ятайте, що Колекція – це все ж масив, але який налаштовується PHP програмістом.
$collection_of_things = new Varien_Data_Collection(); $collection_of_things ->addItem($thing_1) ->addItem($thing_2) ->addItem($thing_3);
Varien_Data_Collection
– це Колекція, від якої успадковуються більшість Колекцій у Magento. Будь-який метод даної Колекції ви можете викликати у Колекцій вище по ланцюгу (ми побачимо це пізніше).
Що ми можемо зробити з Колекцією? З одного боку, можна використовувати у foreach
для ітерацій:
foreach($collection_of_things as $thing) { var_dump($thing->getData()); }
Також є швидкі методи для витягування першої та останньої деталі (item)
var_dump($collection_of_things->getFirstItem()->getData()); var_dump($collection_of_things->getLastItem()->getData());
Вам потрібна ваша Колекція даних у форматі XML? Є такий спосіб:
var_dump( $collection_of_things->toXml() );
Вам потрібен доступ лише до певної властивості?
var_dump($collection_of_things->getColumnValues('name'));
Команда Magento навіть дала нам деякі рудиментарні можливості фільтрації:
var_dump($collection_of_things->getItemsByColumnValue('name','Spot'));
Гарна річ!
Модель Колекції
Так, це цікаво, але чому це так важливо для нас?
Це важливо тому, що весь Magento побудований на даних Колекцій, що успадковані від цього об’єкта. Це означає, що якщо у вас є, скажімо, Колекція товарів, ви можете зробити аналогічне сортування для товарів. Давайте поглянемо:
public function testAction() { $collection_of_products = Mage::getModel('catalog/product')->getCollection(); var_dump($collection_of_products->getFirstItem()->getData()); }
Більшість об’єктів Моделей в Magento мають метод з назвою getCollection
, який буде повертати колекцію, яка, за замовчуванням, ініціалізована повертати кожен Об’єкт цього типу в системі.
Примітка: Колекція даних в Мадженто містить багато складної логіки, яка вирішує, коли використовувати індекс або кеш, так як і логіку для системи EAV сутностей. Послідовний метод викликає ту ж саму Колекцію, що за її терміном життя часто може призвести до несподіваної поведінки. Через це, всі з наведених прикладів загорнуті в одну дію методу. Під час тестування я рекомендую вам робити те ж саме. Крім того, var_dump
XDebug – це чудовий інструмент по роботі з Об’єктами та Колекціями Мадженто, оскільки він (як правило) буде показувати надзвичайно рекурсивні Об’єкти, та все-таки буде відображати вам корисне представлення структури Об’єкта.
Колекція товарів, так як і багато інших Колекцій Мадженто, також має клас Varien_Data_Collection_Db
у своїх предках. Що дає нам багато корисних методів. Наприклад, якщо ви хочете побачити використовуваний запит Колекції:
public function testAction() { $collection_of_products = Mage::getModel('catalog/product')->getCollection(); var_dump($collection_of_products->getSelect()); // може привести до помилки сегментації }
що виведе приблизно таке:
object(Varien_Db_Select)[94] protected '_bind' => array empty protected '_adapter' => ...
Упс! Оскільки Magento використовує шар Zend-абстракції бази даних, ваш Select
також є Об’єктом. Давайте подивимося на більш корисний рядок:
public function testAction() { $collection_of_products = Mage::getModel('catalog/product')->getCollection(); //var_dump($collection_of_products->getSelect()); // може привести до помилки сегментації var_dump( (string) $collection_of_products->getSelect() ); }
Іноді це буде створювати просту вибірку:
'SELECT `e`.* FROM `catalog_product_entity` AS `e`'
Іншим разом це буде щось більш складне:
string 'SELECT `e`.*, `price_index`.`price`, `price_index`.`final_price`, IF(`price_index`.`tier_price`, LEAST(`price_index`.`min_price`, `price_index`.`tier_price`), `price_index`.`min_price`) AS `minimal_price`, `price_index`.`min_price`, `price_index`.`max_price`, `price_index`.`tier_price` FROM `catalog_product_entity` AS `e` INNER JOIN `catalog_product_index_price` AS `price_index` ON price_index.entity_id = e.entity_id AND price_index.website_id = '1' AND price_index.customer_group_id = 0'
Розбіжність залежить від того, які атрибути ви обираєте, а також вищезгаданої індексації та кеш-пам’яті. Якщо ви уважно слідкувати за іншими статтями цієї серії, ви знаєте, що багато моделей Magento (у тому числі Моделі Товару) використовують EAV систему. За замовчуванням, EAV Колекція не буде включати в себе всі атрибути Об’єкта. Ви можете додати їх всі за допомогою методу addAttributeToSelect
:
$collection_of_products = Mage::getModel('catalog/product') ->getCollection() ->addAttributeToSelect('*'); // зірочка це SQL запит типу SELECT * FROM ...
Або ви можете додати лише один потрібний атрибут:
$collection_of_products = Mage::getModel('catalog/product') ->getCollection() ->addAttributeToSelect('meta_title');
Або додати у вибірку декілька атрибутів:
$collection_of_products = Mage::getModel('catalog/product') ->getCollection() ->addAttributeToSelect('meta_title') ->addAttributeToSelect('price');
Відкладене Завантаження
Одна річ, яка збиває з пантелику PHP розробників, які лише починають знайомитись із ORM системою Magento, це коли Magento робить свої запити до бази даних. Коли ви пишете дослівний SQL запит, або навіть коли ви використовуєте базову систему ORM, SQL запити часто викликаються відразу ж після створення екземпляра Об’єкта.
$model = new Customer(); // SQL викликається для заповнення Об'єкта echo 'Done'; // виконання триває
Magento так не працює. Замість цього, використовується концепція Відкладеного Завантаження. У спрощеному вигляді, Відкладене Завантаження означає, що ніякі SQL-запити не робляться поки клієнтській частині не потрібен доступ до даних. Це означає, що коли ви робите щось подібне:
$collection_of_products = Mage::getModel('catalog/product') ->getCollection();
Magento, насправді, не заходить до бази даних. І ви можете сміливо додавати потрібні атрибути пізніше:
$collection_of_products = Mage::getModel('catalog/product') ->getCollection(); $collection_of_products->addAttributeToSelect('meta_title');
і не потрібно турбуватися, що Magento робить запит до бази даних кожного разу, коли додається новий атрибут. Запиту до бази даних не буде, поки ви не спробуєте отримати доступ до елементу Колекції.
Загалом, намагайтесь не надто турбуватися про деталі цієї реалізації в повсякденній роботі. Варто знати, що є SQL бекенд і Мадженто робить SQL-ні штучки, але при кодуванні в майбутньому спробуйте забути про це, і просто розглядайте об’єкти як блочні каркаси, які роблять те, що вам потрібно.
Фільтрація Колекції бази даних
Найбільш важливим методом Колекції бази даних є addFieldToFilter
. Він додає свої WHERE
-умови до SQL запиту, які залишаються за лаштунками. Розглянемо трішки цього коду, який запускається з тестовими даними бази даних (замініть SKU на ваш власний, який ви використовуєте для своїх товарів):
public function testAction() { $collection_of_products = Mage::getModel('catalog/product')->getCollection(); $collection_of_products->addFieldToFilter('sku','n2610'); // інша гарна річ для колекції – це те, що ви можете передати її у функцію count echo "Наша колекція зараз налічує " . count($collection_of_products) . ' позицію(й)'; var_dump($collection_of_products->getFirstItem()->getData()); }
Перший параметр addFieldToFilter
– це атрибут, по якому ви хочете фільтрувати. Другий – значення, яке ви шукаєте. Тут ми додаємо sku
фільтр для значення n2610
.
Другий параметр також може бути використаний для вказівки типу фільтрації, яку ви хочете зробити. Тут речі стають трохи складнішим, і вони варті більш поглибленої уваги.
Так, за замовчуванням, наступний PHP код:
$collection_of_products->addFieldToFilter('sku','n2610');
по-суті еквівалентний SQL умові:
WHERE sku = "n2610"
Погляньте самі. Запускаємо наступне:
public function testAction() { var_dump( (string) Mage::getModel('catalog/product') ->getCollection() ->addFieldToFilter('sku','n2610') ->getSelect()); }
що дасть нам такий SQL-запит:
SELECT `e`.* FROM `catalog_product_entity` AS `e` WHERE (e.sku = 'n2610')'
Майте на увазі, що це може дуже швидко ускладнитися, якщо ви використовуєте атрибут EAV. Додайте атрибут:
var_dump( (string) Mage::getModel('catalog/product') ->getCollection() ->addAttributeToSelect('*') ->addFieldToFilter('meta_title','my title') ->getSelect() );
і отримаєте щось дуже нерозбірливе:
SELECT `e`.*, IF(_table_meta_title.value_id>0, _table_meta_title.value, _table_meta_title_default.value) AS `meta_title` FROM `catalog_product_entity` AS `e` INNER JOIN `catalog_product_entity_varchar` AS `_table_meta_title_default` ON (_table_meta_title_default.entity_id = e.entity_id) AND (_table_meta_title_default.attribute_id='103') AND _table_meta_title_default.store_id=0 LEFT JOIN `catalog_product_entity_varchar` AS `_table_meta_title` ON (_table_meta_title.entity_id = e.entity_id) AND (_table_meta_title.attribute_id='103') AND (_table_meta_title.store_id='1') WHERE (IF(_table_meta_title.value_id>0, _table_meta_title.value, _table_meta_title_default.value) = 'my title')
Намагайтеся не думати занадто багато про SQL, якщо у вас піджимають строки здачі проекту.
Інші оператори порівняння
Я впевнений, що вас цікавить питання: “а що, якщо я хочу щось інше, ніж запит рівності”? Не дорівнює, більше, менше і т.д. Другий параметр методу addFieldToFilter
вирішує це питання. Він підтримує альтернативний синтаксис, де замість передачі рядку ви передаєте умови в простому елементі масиву.
Ключ цього масиву – тип порівняння, який ви хочете зробити. Значення, що пов’язане з цим ключем є значенням, яким ви хочете фільтрувати. Давайте переробимо наш фільтр з використанням іншого синтаксису:
public function testAction() { var_dump( (string) Mage::getModel('catalog/product') ->getCollection() ->addFieldToFilter('sku', array('eq'=>'n2610')) ->getSelect() ); }
Викликаємо наш фільтр кодом:
addFieldToFilter('sku',array('eq'=>'n2610'))
Як ви можете бачити, другий параметр є масивом. Його ключ «eq» означає «рівність». Значення цього ключа «n2610», по ньому і відбувається фільтрація.
Нижче перераховані всі фільтри, з наведенням прикладу їх SQL еквівалентів:
array("eq"=>'n2610') WHERE (e.sku = 'n2610') array("neq"=>'n2610') WHERE (e.sku != 'n2610') array("like"=>'n2610') WHERE (e.sku like 'n2610') array("nlike"=>'n2610') WHERE (e.sku not like 'n2610') array("is"=>'n2610') WHERE (e.sku is 'n2610') array("in"=>array('n2610')) WHERE (e.sku in ('n2610')) array("nin"=>array('n2610')) WHERE (e.sku not in ('n2610')) array("notnull"=>true) WHERE (e.sku is NOT NULL) array("null"=>true) WHERE (e.sku is NULL) array("gt"=>'n2610') WHERE (e.sku > 'n2610') array("lt"=>'n2610') WHERE (e.sku < 'n2610') array("gteq"=>'n2610') WHERE (e.sku >= 'n2610') array("lteq"=>'n2610') WHERE (e.sku <= 'n2610') array("finset"=>array('n2610')) WHERE (find_in_set('n2610',e.sku)) array('from'=>'10','to'=>'20') WHERE e.sku >= '10' and e.sku <= '20'
Більшість з них говорять самі за себе, але деякі заслуговують особливої уваги
in, nin, find_in_set
“in” та “nin” – умови, які дозволяється передавати в масив значень. Тобто сама частина значення вашого масиву фільтрів повинна бути дозволеною в якості масиву.
array("in"=>array('n2610','ABC123') WHERE (e.sku in ('n2610','ABC123'))
notnull, null
Ключове слово NULL
– особливе для більшості дистрибутивів SQL. Воно, як правило, не буде добре працювати зі стандартним оператором рівності (=). Визначення notnull
або null
в якості типу фільтра допоможе вам правильно порівнювати з NULL, ігноруючи будь-яке значення, що ви передаєте:
array("notnull"=>true) WHERE (e.sku is NOT NULL)
Фільтр from - to
Це ще один спеціальний формат, який руйнує стандартне правило. Замість єдиного елемента масиву ви задаєте масив з двох елементів. Один елемент має ключ “from”, другий елемент має ключ “to”. Коли ключі ідентифіковані, цей фільтр дозволяє охопити діапазон значень від/до без необхідності вказувати символи більше ніж та менше ніж:
public function testAction { var_dump( (string) Mage::getModel('catalog/product') ->getCollection() ->addFieldToFilter('price',array('from'=>'10','to'=>'20')) ->getSelect() ); }
що буде аналогічним:
WHERE (_table_price.value >= '10' and _table_price.value <= '20')'
AND чи OR, або це те ж, що і OR та AND?
Нарешті, ми підійшли до булевих операторів. Таке буває дуже рідко, коли ми робимо фільтрацію лише по одному атрибуту. На щастя, Колекція Magento нас прикриває. Ви можете використати декілька addFieldToFilter
, щоб отримати ряд “AND” запитів:
function testAction() { echo (string) Mage::getModel('catalog/product') ->getCollection() ->addFieldToFilter('sku',array('like'=>'a%')) ->addFieldToFilter('sku',array('like'=>'b%')) ->getSelect(); }
Зв’язуючи разом множинні виклики, як зазначено вище, ми робимо where
умови, що виглядатиме на кшталт такого:
WHERE (e.sku like 'a%') AND (e.sku like 'b%')
Для тих з вас, хто тільки що підняв руку: так, наведений вище приклад буде завжди повертати 0 записів. Жоден SKU не може починатися одночасно і з а, та і з b. Ймовірно, що тут краще підійде умова OR. Це підводить нас до іншого заплутаного аспекту другого параметра addFieldToFilter
.
Якщо ви хочете побудувати OR-запит, необхідно передати масив з масивом фільтрів в якості другого параметра. Я вважаю, що краще призначити окремі масиви для фільтрів змінним:
public function testAction() { $filter_a = array('like'=>'a%'); $filter_b = array('like'=>'b%'); }
а потім використовувати змінні в якості масиву всіх моїх фільтрів:
public function testAction() { $filter_a = array('like'=>'a%'); $filter_b = array('like'=>'b%'); echo (string) Mage::getModel('catalog/product') ->getCollection() ->addFieldToFilter('sku', array($filter_a, $filter_b)) ->getSelect(); }
Цікавим буде згадати, що вищевказаний масив з масивом фільтрів:
array($filter_a, $filter_b)
дасть нам щось на кшталт такого:
WHERE (((e.sku like 'a%') or (e.sku like 'b%')))
Короткий підсумок
Після прочитання цієї статті ви стали дуже потужним та серйозно озброєним розробником Magento. Не маючи більше потреби писати рядки SQL запитів, ви тепер знаєте, як зробити запит в Мадженто до будь-якої Моделі в вашому магазині чи додатку.
Автор: Alan Storm (http://alanstorm.com/magento_collections/)
Переклад українською: Ненько Сергій
Адаптація перекладу: SebWeo
- Magento для PHP MVC розробників (Alan Storm) – ч.1/11
- Magento для PHP MVC розробників – розбір Контролера (ч.2/11)
- Magento для PHP MVC розробників – Макети, Блоки та Шаблони (ч.3/11)
- Magento для PHP MVC розробників – Моделі та основи ORM (ч.4/11)
- Magento для PHP MVC розробників – Інсталювання Ресурсу (ч.5/11)
- Magento для PHP MVC розробників – Розширений ORM – EAV (ч.6/11)
- Magento для PHP MVC розробників – Особлива конфігурація системи (ч.7/11)
- Magento для PHP MVC розробників – Поглиблене налаштування системи (ч.8/11)
- Magento для PHP MVC розробників – Колекції Varien Data (ч.9/11)
- Magento для PHP MVC розробників – Перевизначення і оновлюваність системи (ч.10/11)
- Magento для PHP MVC розробників – Конфігурація системи за замовчуванням (ч.11/11)