RSS Мои друзья Контакты

Magento модели от А до Я: 3 кита

Magento написана при помощи Zend фреймворка и библиотеки Varien. Базовый функционал всех моделей сконцентрирован именно в последней. А именно - это 3 кита Magento: Varien_Object, Varien_Data_Collection и Varien_Data_Collection_Db.

Varien_Object

Большинство моделей в Magento наследуют функционал этого класса. Если в двух словах, то этот класс упрощает работу с данными. Конструктор принимает один необязательный аргумент массив данных, который записывается в свойство $_data. По-скольку класс реализовывает интерфейс ArrayAccess, то к данным в объекте можно обращаться при помощи квадратных скобок ([]). Так же в классе реализованы магические методы: __get(), __set(), __call(), благодаря которым также можно получать данные хранящиеся в объекте. Например, чтобы получить данные из объекта

$user = new Varien_Object(array(
    'is_enabled' => true,
    'group_id'   => 1,
    'name'       => 'Вася Пупкин'
));

# first one
echo $user->getIsEnabled(), "\n";
echo $user->getName(), "\n";

# second one
echo $user->is_enabled, "\n";
echo $user->name, "\n";

# third one
echo $user['is_enabled'], "\n";
echo $user['name'], "\n";

Аналогично при помощи тех же конструкций можно установить значение

# first one
$user->setIsEnabled(false);
echo (int)$user->getIsEnabled(), "\n";

# second one
$user->is_enabled = false;
echo (int)$user->is_enabled, "\n";

# third one
$user['is_enabled'] = false;
echo (int)$user['is_enabled'], "\n";

Все выше упомянутые методы работают либо через интерфейс, либо благодаря магическим методам базируясь на функциях класса setData() или getData().

$user->setData('is_enabled', false);
echo (int)$user->getData('is_enabled'), "\n";

Эти методы очень упрощают работу! Например, если приходят данные с формы, то вместо вызова нескольких методов можно просто написать

$user = new Varien_Object();
$user->setData($_POST);

echo $user->getName(), "\n";

А метод getData() позволяет напрямую обращаться к определенному значению в многомерном массиве при помощи пути

$user = new Varien_Object(array(
    'name'       => 'Вася Пупкин',
    'friends'    => array(
        'university' => array(1,2,3,4,5),
        'home'       => array('Petrov' => 1, 'Pupkin' => 2)
    )
));

echo $user->getData('friends/home/Petrov'), "\n";

Все бизнес модели являются потомками этого класса. Например, модели продукта и пользователя

$customer = Mage::getModel('customer/customer');

$customer->setFirstname('Vasya')
    ->setLastname('Pupkin')
    ->save();

var_dump($customer->getId());

Так же очень интересными есть методы setId(), getId() и setIdFieldName(). Благодаря им достаточно просто и удобно работать с любимы именами идентификаторов в базе. Например, в базе поле PRIMARY_KEY називается entity_id, но все равно к нему можно обращаться при помощи методов setId/getId

$user = new Varien_Object(array(
    'entity_id' => 12
));
$user->setIdFieldName('entity_id');

echo $user->getId(), "\n";

Этот класс имеет и ряд методов по конвертации данных. Например, в json, xml, array и string

$user = new Varien_Object(array(
    'name'       => 'Вася Пупкин',
    'id'         => 5
));

echo $user->toString(), "\n";
echo $user->toXml(), "\n";
echo $user->toJson(), "\n";
var_dump($user->toArray());

Каждый из этих методов имеет необязательные параметры, при помощи которых можно отфильтровать данные.

Varien_Data_Collection

Этот класс реализовывает методы упрощающие работу с коллекцией объектов (айтемов), к примеру с массивом объектов класса Varien_Object. В качестве айтемов может быть любой другой объект. Для изменения класса предназначен метод setItemObjectClass(string $className). Этот класс реализовывает 2 интерфейса IteratorAggregate и Countable, что позволяет перебирать его айтемы при помощи цикла foreach и узнавать к-во элементов при помощи ф-ции count.

Сам по себе этот класс нигде не используется, а просто реализовывает базовый функционал. Достаточно странно, что он не абстрактный. Потому как он реализовывает базовый функционал пейджинга, но сам его нигде не использует (getCurPage(), getLastPageNumber(), getPageSize() и getSize()). Разница между методом getSize() и вызовом ф-цию count() на объекте в том, что при вызове первого к-во записывается в свойство объекта $_totalRecords и потом не пересчитывается. При втором способе будет каждый раз пересчитываться.

Для работы с элементами доступны методы: addItem(), getFirstItem(), getLastItem(), getNewEmptyItem(), removeItemByKey и getItems(). Очистить коллекцию можно при помощи метода clear. Также есть набор методов (set/get/has) по установке флагов коллекции (это могут быть не только булевские значения). Например

$user = new Varien_Object(array(
    'name'       => 'Вася Пупкин',
    'id'         => 5
));


$collection = new Varien_Data_Collection();

$collection->addItem($user)
    ->setFlag('is_new')
    ->setFlag('is_test', 'yes');

echo (int)$collection->hasFlag('is_new'), "\n";
echo $collection->getFlag('is_test'), "\n";

Можно получить массив значений определенного поля из всех элементов при помощи метода getColumnValues($colName). Или же получить все айтыми, у которых значение определенного поля равно заданному getItemsByColumnValue($column, $value). Также можно получить идентификаторы всех элементов при помощи метода getAllIds().

В коде Magento очень часто фигурируют массивы такой структуры

# first: Option Hash
$data = array(
    $value => $label
..........................
);

# second: Option Array
$data = array(
..........................
 array( 'label' => $label, 'value' => $value ) );

По-этому неудивительно, что данный класс реализовывает 2 метода: toOptionHash() и toOptionArray(), которые преобразовывают данные в коллекции именно к таким структурам соответственно. Поля $value и $label настраиваются для каждого класса отдельно при помощи защищенных методов _toOptionHash() и _toOptionArray().

Varien_Data_Collection_Db

Этот класс является фундаментом для всех коллекций в Magento. Для взаимодействия с базой используются расширенные Zend-овские классы. Запрос к базе создается при помощи объекта класса Zend_Db_Select.

Конструктор коллекции принимает один необязательный параметр - объект соединения, который является экземпляром потомка класса Zend_Db_Adapter_Abstract. Получить объект селекта позволяет метод getSelect(), объект соединения с базой - getConnection(), getData() позволяет получить вместо объектов массивы данных из базы, setOrder($field, $direction) - устанавливает порядок сортировки, load() - загружает коллекцию из базы данных (вызывается автоматически перед перебором элементов или вызовом метода getItems()) и самый интересный addFieldToFilter($field, $condition) - добавляет в запрос фильтр по полю.

Последний метод стоит рассмотреть более детально, поскольку значение $condition может быть достаточно разнообразным. Например

$collection->addFieldToFilter('sku', array('eq' => 'ZH12b5'));

то же самое

$collection->addFieldToFilter('sku', 'ZH12b5');

Чтобы разобраться с этим составим таблицу: выражения в PHP и соответствующий ему SQL.

array("eq" => 'ZH12b5')
 WHERE (sku = 'ZH12b5')
array("neq" => 'ZH12b5')
 WHERE (sku <> 'ZH12b5')
array("like" => 'ZH12b5')
 WHERE (sku LIKE 'ZH12b5')
array("nlike" => 'ZH')
 WHERE (sku NOT LIKE 'ZH')
array("is" => 'zh')
 WHERE (sku IS 'zh')
array("in" => array('zh', 'pl'))
 WHERE (sku IN ('zh', 'pl'))
array("nin" => array('zh', 'pl'))
 WHERE (sku NOT IN ('zh', 'pl'))
array("notnull" => true )
 WHERE (sku IS NOT NULL)
array("null" => true)
 WHERE (sku IS  NULL)
array("gt" => 5)
 WHERE (sku > 5)
array("lt" => 10)
 WHERE (sku < 10 )
array("gteq" => 10)
 WHERE (sku >= 5)
array("lteq" => 22)
 WHERE (sku =< 5)
array("finset" => array('test'))
 WHERE (find_in_set('test', sku))
array('from' => 2, 'to' => 30)
 WHERE (sku >= '2' AND sku <= '30')

С этим все предельно ясно, но интересно узнать как комбинировать условия при помощи AND/OR. Достаточно просто:

$collection->addFieldToFilter('name', array('like' => 'ZH%'))
    ->addFieldToFilter('is_enabled', true);

Такой вызов приведет к созданию следующего запроса

 WHERE (name LIKE 'ZH%') AND (is_enabled = 1)

Для использования OR в запросе все не так очевидно:

$collection->addFieldToFilter('name', array(
    array('like' => 'ZH%'),
    array('like' => '%P')
));

Такая конструкция создаст следующий запрос

 WHERE ((name LIKE 'ZH%') OR (name LIKE '%P')) 

Если у таблицах совпадают имена полей, значит нужно сделать маппинг для фильтра. Например, есть таблицы пользователей (users) и ролей(user_roles), каждая из таблиц имеет поле name. Хотим сделать фильтр по полю из таблицы пользователей. Тогда

$collection->addFilterToMap('user_name', 'users.name')
    ->addFilterToMap('role_name', 'user_roles.name')
    ->addFieldToFilter('user_name', array('like' => 'Admin%'));

Теперь немного об оптимизации. Коллекция предоставляет функционал кэширования при помощи метода:

public function initCache($cacheInstance, $cachePrefix, array $cacheTags);

Объект $cacheInstance может быть каким угодно, он даже не должен реализовывать какой-либо интерфейс, НО должен иметь методы load($cacheId) и save(string $data, $cacheId, $cacheTags).

Метод load() коллекции загружает все строки из базы данных соответствующие заданным критериям, но что если таких строк будет 100000 - тогда это 100000 объектов. Можно с 100% увереностью сказать, что не каждый сервер сможет поместить такое к-во объектов в памяти. Именно тогда и нужно использовать метод fetchItem() (доступный с версии 1.5.1). Если его вызвать он возвратит только один элемент за раз, если вызвать еще возвратит следующий.

while ($item = $collection->fetchItem()) {
   $data = $item->callSomeMethod();
   // do some operations
   unset($item);
}

Это позволит работать с большими данными и при этом целесообразно использовать оперативную память сервера.

Проблема с памятью относится и к таким методам как toOptionHash() и toOptionArray(). По-этому был написан аналог первого (про второй видимо забыли), но с использованием метода fetchItem() и называется он _toOptionHashOptimized(). Это защищенный метод, по-этому доступ к нему можно получить только внутри класса. Но ничто не мешает создать наследника от этого класса и написать публичный метод toOptionHashOptimized().

P.S.: в следующей статье начнем разбираться с "реальными" классами в Magento. Узнаем общую структуру моделей, что такое Magento-path и многое другое

Добавить комментарий

Комментариев: 7

  • Dmitry
    Ответить 24 февраля 2012 г., 12:56
    Очень понравились Ваши статьи о Magento. Жду продолжения. Спасибо.
  • Krek
    Ответить 12 апреля 2012 г., 12:01
    Но почему в маженто все так сложно )
    • Сергей (Администратор)
      Ответить 12 апреля 2012 г., 12:44
      Могу Вас обрадовать. Это еще просто=)
  • Павел
    Ответить 27 августа 2012 г., 14:16
    Спасибо вам огромное, очень хорошо написано...
  • It
    Ответить 30 августа 2012 г., 13:16
    Спасибо БОЛЬШОЕ, Сергей за ваши труды! Очень бы хотелось увидеть несколько простых но реальных примеров по этой статье. Ещё раз спасибо :)
  • Xeni
    Ответить 21 октября 2012 г., 14:54
    Спасибо! Вы очень хорошо все написали, очень ясно и понятно.
  • Anton
    Ответить 18 декабря 2012 г., 17:49
    Спасибо большое, по-тихоньку разбираемся.