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. Оно, как правило, не будет хорошо работать со стандартным оператором равенства (=). Определение n​otnull или 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

 
 

Все статьи данной серии:

  1. Magento для PHP MVC разработчиков (Alan Storm) – ч.1/11
  2. Magento для PHP MVC разработчиков – разбор контроллера (ч.2/11)
  3. Magento для PHP MVC разработчиков – Макеты, Блоки и Шаблоны (ч.3/11)
  4. Magento для PHP MVC разработчиков – Модели и основы ORM (ч.4/11)
  5. Magento для PHP MVC разработчиков – Инсталлирование Ресурса (ч.5/11)
  6. Magento для PHP MVC разработчиков – Расширенный ORM – EAV (ч.6/11)
  7. Magento для PHP MVC разработчиков – Особая конфигурация системы (ч.7/11)
  8. Magento для PHP MVC разработчиков – Углубленная настройка системы (ч.8/11)
  9. Magento для PHP MVC разработчиков – Коллекции Varien Data (ч.9/11)
  10. Magento для PHP MVC разработчиков – Переопределение и обновляемость системы (ч.10/11)
  11. Magento для PHP MVC разработчиков – Конфигурация системы по умолчанию (ч.11/11)