Все началось с того, что я взял на доработку очередной фриланс проект. Заданием было доделатьреализовать мультиязычный модуль Галереи. Проблема возникла, как всегда внезапно…
Галерея состоит из альбомов и айтемов. И альбом и айтем имеют такие основные поля:
Получается, что title и description должны зависеть от StoreView, то есть store_id в базе данных. Был сначала вариант добавить просто еще одно поле store_id, так я и сделал вначале, но потом увидел, что это решение просто нелепое! Объясню почему:
Вроде и неплохо, но совсем не ориентировано на конечного пользователя.
Так как я в последнее время часто использую Symfony1.4 + Doctrine1.2, решение долго искать не пришлось. Я решил реализовать функционал для Magento аналогичный Doctrine I18n behavior.
А потому что удобно и просто! Плюс ко всему я не смог найти стандартный функционал, который бы реализовал это. Хотя была идея сделать все через EAV (создать entity_type, атрибуты и все что там еще нужно), как по мне — сложно и запутано.
Вся прелесть этого решения в том, что коллекция и модель, какой были такими и остались по внешнему api, но теперь не нужно думать о сохранении и разделение данных для нескольких StoreView.
Сама реализация заключается в создании еще одного слоя абстракции для Source моделей (коллекции и самой модели). И потом просто наследуемся от них, если нужно реализовать сохранение и работу с данными для нескольких StoreView.
Вот сами классы:
— app/code/local/Sj/Gallery/Model/Mysql4/Translation.php
— app/code/local/Sj/Gallery/Model/Mysql4/Translation/Collection.php
Все до безумия просто. Раньше, при создании Source модели, нужно было наследоваться от Mage_Core_Model_Mysql4_Abstract, сейчас же — от Sj_Gallery_Model_Mysql4_Translation.
И нужно создать таблицы для переводов самому в install файле вашего модуля. На одну таблицу еще одна из суффиксом "_translation" (это значение является константой класса и его можно изменить).
Единственный и очень важный момент — всегда нужно в модель устанавливать store_id перед вызовом метода load!
Пример использования коллекции:
Пример использования модели:
Исходный код install файла модуля Галереи:
Исходники можно скачать здесь.
P.S.: я не ставил перед собой цель реализовать полноценный i18n функционал. Я просто решил задачу и мне решение понравилось, так как оно переносимое и легкое для понимания. Этого достаточно, чтобы прозрачно работать с multi store view, но можно сделать и еще лучше. Например, организовать все в виде модуля, создать setup модель, которая сама будет создавать дополнительные таблицы, вынести имена полей, которые зависят от store, в конфигурацию.
В этой статье код был подсвечен при помощи Source Code Highlighter.
Двигатель прогресса
Галерея состоит из альбомов и айтемов. И альбом и айтем имеют такие основные поля:
- id
- title
- description
- file
Получается, что title и description должны зависеть от StoreView, то есть store_id в базе данных. Был сначала вариант добавить просто еще одно поле store_id, так я и сделал вначале, но потом увидел, что это решение просто нелепое! Объясню почему:
- альбомы на разных StoreView имели разный id (аналогично с айтемами)
- пользователю приходилось бы на каждый StoreView грузить один и то же рисунок (поле file)
- если нужно будет потом добавить еще какие-то общие поля для всех языков, могут возникнут аналогичные проблемы, как с рисунком
Вроде и неплохо, но совсем не ориентировано на конечного пользователя.
Freak idea
Так как я в последнее время часто использую Symfony1.4 + Doctrine1.2, решение долго искать не пришлось. Я решил реализовать функционал для Magento аналогичный Doctrine I18n behavior.
Зачем?
А потому что удобно и просто! Плюс ко всему я не смог найти стандартный функционал, который бы реализовал это. Хотя была идея сделать все через EAV (создать entity_type, атрибуты и все что там еще нужно), как по мне — сложно и запутано.
Вся прелесть этого решения в том, что коллекция и модель, какой были такими и остались по внешнему api, но теперь не нужно думать о сохранении и разделение данных для нескольких StoreView.
Дешево и сердито
Сама реализация заключается в создании еще одного слоя абстракции для Source моделей (коллекции и самой модели). И потом просто наследуемся от них, если нужно реализовать сохранение и работу с данными для нескольких StoreView.
Вот сами классы:
— app/code/local/Sj/Gallery/Model/Mysql4/Translation.php
abstract class Sj_Gallery_Model_Mysql4_Translation extends Mage_Core_Model_Mysql4_Abstract
{
const TABLE_SUFIX = '_translation';
protected
$_translatableFields = array();
/**
* Standard resource model initialization
*
* @param string $mainTable
* @param string $idFieldName
* @return Mage_Core_Model_Mysql4_Abstract
*/
protected function _init($mainTable, $idFieldName)
{
if (empty($this->_translatableFields)) {
throw new Exception('You must specify translatable fields');
}
$this->_setMainTable($mainTable, $idFieldName);
}
/**
* Retrieve select object for load object data
*
* @param string $field
* @param mixed $value
* @return Zend_Db_Select
*/
protected function _getLoadSelect($field, $value, $object)
{
$tableName = $this->getMainTable();
$select = parent::_getLoadSelect($field, $value, $object);
$select->joinLeft(
array('trnslt' => $this->getTranslationTableName()),
'trnslt.id = ' . $tableName . '.' . $field . '
AND trnslt.store_id = ' . (int)$object->getStoreId(),
$this->getTranslatableColumns()
);
return $select;
}
/**
* Set multilang field names
*
* @param array $fields
* @return Sj_Gallery_Model_Mysql4_Translation
*/
public function setTranslatableFields($fields)
{
if (!is_array($fields)) {
return false;
}
$this->_translatableFields = $fields;
return $this;
}
/**
* Get multilang field names
*
* @return array
*/
public function getTranslatableFields()
{
return $this->_translatableFields;
}
/**
* Get multilang columns
*
* @return array
*/
public function getTranslatableColumns()
{
$columns = $this->getTranslatableFields();
$columns['translation_id'] = 'trnslt.id';
$columns['store_id'] = 'trnslt.store_id';
return $columns;
}
/**
* Get translation table name
*
* @return string
*/
public function getTranslationTableName()
{
return $this->getMainTable() . self::TABLE_SUFIX;
}
/**
* Save object object data
*
* @param Mage_Core_Model_Abstract $object
* @return Mage_Core_Model_Mysql4_Abstract
*/
public function save(Mage_Core_Model_Abstract $object)
{
$adapter = $this->_getWriteAdapter();
$adapter->beginTransaction();
try {
$data = $object->getData();
$translations = array();
foreach ($this->_translatableFields as $field) {
if (isset($data[$field])) {
$translations[$field] = $data[$field];
unset($data[$field]);
}
}
$onDuplicate = array_keys($translations);
$translations['id'] = $object->getId();
$translations['store_id'] = $object->getStoreId();
$adapter->insertOnDuplicate(
$this->getTranslationTableName(),
$translations,
array_combine($onDuplicate, $onDuplicate)
);
parent::save($object);
$adapter->commit();
return $this;
} catch (Exception $e) {
$adapter->rollBack();
throw $e;
}
}
}
* This source code was highlighted with Source Code Highlighter.
— app/code/local/Sj/Gallery/Model/Mysql4/Translation/Collection.php
abstract class Sj_Gallery_Model_Mysql4_Translation_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract
{
protected function _initSelect()
{
$tableName = $this->getResource()->getMainTable();
$this->getSelect()
->from(array('main_table' => $tableName))
->joinLeft(array('trnslt' => $this->getResource()->getTranslationTableName()),
'trnslt.id = main_table.' . $this->getResource()->getIdFieldName(),
$this->getResource()->getTranslatableColumns()
);
return $this;
}
public function addStoreToFilter(Mage_Core_Model_Store $store)
{
$this->addFieldToFilter('trnslt.store_id', $store->getId());
return $this;
}
}
* This source code was highlighted with Source Code Highlighter.
Практикуемся
Все до безумия просто. Раньше, при создании Source модели, нужно было наследоваться от Mage_Core_Model_Mysql4_Abstract, сейчас же — от Sj_Gallery_Model_Mysql4_Translation.
И нужно создать таблицы для переводов самому в install файле вашего модуля. На одну таблицу еще одна из суффиксом "_translation" (это значение является константой класса и его можно изменить).
Единственный и очень важный момент — всегда нужно в модель устанавливать store_id перед вызовом метода load!
Пример использования коллекции:
$collection = Mage::getModel('gallery/group')->getCollection()
->addStoreToFilter(Mage::app()->getStore())
->addFieldToFilter('status', 1)
->getItems();
* This source code was highlighted with Source Code Highlighter.
Пример использования модели:
$store = Mage::app()->getStore($request->getParam('store'));
$group = Mage::getModel('gallery/group')
->setStoreId($store->getId())
->load($id);
* This source code was highlighted with Source Code Highlighter.
Исходный код install файла модуля Галереи:
$installer = $this;
$installer->startSetup();
$installer->run("
CREATE TABLE {$this->getTable('gallery/gallery')} (
`gallery_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`filename` varchar(255) NOT NULL DEFAULT '',
`status` smallint(6) NOT NULL DEFAULT '0',
`created_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`gallery_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
");
$installer->run("
CREATE TABLE IF NOT EXISTS `{$this->getTable('gallery/group')}` (
`collection_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`file` varchar(255) NOT NULL DEFAULT '',
`status` tinyint(4) NOT NULL,
`created_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`collection_id`),
KEY `gallery_group_idx` ( `collection_id` )
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
");
$installer->run("
CREATE TABLE `{$this->getTable('gallery/items_translation')}` (
`id` int(10) unsigned NOT NULL,
`title` varchar(255) NOT NULL DEFAULT '',
`description` varchar(20000) NOT NULL DEFAULT '',
`store_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`, `store_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
ALTER TABLE `{$this->getTable('gallery/items_translation')}`
ADD FOREIGN KEY (`id`) REFERENCES `{$this->getTable('gallery/gallery')}` (`gallery_id`)
ON DELETE CASCADE;
CREATE TABLE `{$this->getTable('gallery/group_translation')}` (
`id` int(10) unsigned NOT NULL,
`title` varchar(255) NOT NULL DEFAULT '',
`description` varchar(20000) NOT NULL DEFAULT '',
`store_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`, `store_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
ALTER TABLE `{$this->getTable('gallery/group_translation')}`
ADD FOREIGN KEY (`id`) REFERENCES `{$this->getTable('gallery/group')}` (`collection_id`)
ON DELETE CASCADE;
");
$installer->endSetup();
* This source code was highlighted with Source Code Highlighter.
Исходники можно скачать здесь.
P.S.: я не ставил перед собой цель реализовать полноценный i18n функционал. Я просто решил задачу и мне решение понравилось, так как оно переносимое и легкое для понимания. Этого достаточно, чтобы прозрачно работать с multi store view, но можно сделать и еще лучше. Например, организовать все в виде модуля, создать setup модель, которая сама будет создавать дополнительные таблицы, вынести имена полей, которые зависят от store, в конфигурацию.
В этой статье код был подсвечен при помощи Source Code Highlighter.