Идея Doctrine I18n в Magento

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

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


    Галерея состоит из альбомов и айтемов. И альбом и айтем имеют такие основные поля:
    • 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.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 2

      0
      Сразу прошу прощения если я не так понял задачу. Но почему бы не создать табличку items_description в которой хранятся нужные title на разных языках связанные через ключ с таблицей language, и при запросе просто джойнить items с items_description по language_id. Как то так.
        0
        Во-первых, таблицы language в Magento нету, там все это делается через store view. А во-вторых, это решение более универсальное, так как не нужно придумывать никаких таблиц и связей, а просто создать по шаблону.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое