Pull to refresh

Идея Doctrine I18n в Magento

Reading time 8 min
Views 1.1K
Все началось с того, что я взял на доработку очередной фриланс проект. Заданием было доделатьреализовать мультиязычный модуль Галереи. Проблема возникла, как всегда внезапно…

Двигатель прогресса


Галерея состоит из альбомов и айтемов. И альбом и айтем имеют такие основные поля:
  • 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.
Tags:
Hubs:
-3
Comments 2
Comments Comments 2

Articles