Видео для продуктов в Magento.

В отличии от 2й версии довольно популярной CMS для eCommerce Magento, 1я версия не включает в себя по умолчанию возможность добавления видео для продуктов. Существует большое количество модулей в маркете, которые вы можете приобрести и установить или написать свой.

Давайте рассмотрим пример подобного функционала.

Для начала нам нужно определиться где мы будем хранить наши ведео. Во 2й версии добавлена возможность использовать https://www.youtube.com/ и https://vimeo.com . Данное решение имеет свой ряд приимуществ. Экономия на своих серверах и доп функционал апи данных сервисов.

И так, определилилсь, мы будем добавлять ссылки на видео. Для этого в админ панели catalog->manage products->select product->images мы добавим кнопку добавления полей, в которые мы будем вносить ссылку на видео и порядок вывода его на PDP.

Сделать это можно несколькими способами, я использую обсервер, который нам любезно был оставлен на этапе формирования лайяута. Для этого, мы добавим в config.xml нашего модуля

    <adminhtml>
        <events>
            <catalog_product_gallery_prepare_layout>
                <observers>
                    <company_productvideo>
                        <class>company_productvideo/observer</class>
                        <method>addVideo</method>
                        <type>singleton</type>
                    </company_productvideo>
                </observers>
            </catalog_product_gallery_prepare_layout>
        </events>
    </adminhtml>

тем самым будем дополнять формирование лаяута нашей кнопкой и полями.

Company/ProductVideo/Model/Observer.php быдет выглядеть так

<?php

/**
 * Class Company_ProductVideo_Model_Observer
 */
class Company_ProductVideo_Model_Observer
{
    /**
     * @param Varien_Event_Observer $observer
     */
    public function addVideo(Varien_Event_Observer $observer)
    {
        /* @var $galleryBlock Mage_Adminhtml_Block_Catalog_Product_Helper_Form_Gallery_Content */
        $observer->getBlock()->setChild('add_button',
            $observer->getBlock()->getLayout()->createBlock('adminhtml/widget_button', 'add_button')
                ->setData(array(
                    'label' => Mage::helper('catalog')->__('Add New Video'),
                    'class' => 'add',
                    'id' => 'add_new_video'
                ))
        );
        /**
         * @var $layout Mage_Core_Model_Layout
         */
        $layout = $observer->getBlock()->getLayout();
        $observer->getBlock()->setChild('video_url_box',
            $layout->createBlock('company_productvideo/adminhtml_catalog_product_edit_tab_images_video', 'video.urls')
        );
    }
}

в нем мы добавляем кнопку и контейнер с полями для данных. Если для кнопки мы можем использовать коровский блок, то для полей напишем свой.

Company/ProductVideo/Block/Adminhtml/Catalog/Product/Edit/Tab/Images/Video.php будет такой:

<?php

/**
 * Class Company_ProductVideo_Block_Adminhtml_Catalog_Product_Edit_Tab_Images_Video
 */
class Company_ProductVideo_Block_Adminhtml_Catalog_Product_Edit_Tab_Images_Video extends Mage_Adminhtml_Block_Widget
{
    protected $_product;

    protected $_productInstance;

    protected $_values;

    protected $_itemCount = 1;

    /**
     * Class constructor
     */
    public function __construct()
    {
        parent::__construct();
        $this->setTemplate('catalog/product/edit/images/video.phtml');
    }

    /**
     * @return int
     */
    public function getItemCount()
    {
        return $this->_itemCount;
    }

    /**
     * @param $itemCount
     * @return $this
     */
    public function setItemCount($itemCount)
    {
        $this->_itemCount = max($this->_itemCount, $itemCount);
        return $this;
    }

    /**
     * Get Product
     *
     * @return Mage_Catalog_Model_Product
     */
    public function getProduct()
    {
        if (!$this->_productInstance) {
            if ($product = Mage::registry('product')) {
                $this->_productInstance = $product;
            } else {
                $this->_productInstance = Mage::getSingleton('catalog/product');
            }
        }

        return $this->_productInstance;
    }

    /**
     * @param $product
     * @return $this
     */
    public function setProduct($product)
    {
        $this->_productInstance = $product;
        return $this;
    }

    /**
     * Retrieve options field name prefix
     *
     * @return string
     */
    public function getFieldName()
    {
        return 'product';
    }

    /**
     * Retrieve options field id prefix
     *
     * @return string
     */
    public function getFieldId()
    {
        return 'product_video';
    }

    /**
     * Check block is readonly
     *
     * @return boolean
     */
    public function isReadonly()
    {
        return $this->getProduct()->getOptionsReadonly();
    }

    /**
     * @return Mage_Core_Block_Abstract
     */
    protected function _prepareLayout()
    {
        $this->setChild('delete_button',
            $this->getLayout()->createBlock('adminhtml/widget_button')
                ->setData(array(
                    'label' => Mage::helper('catalog')->__('Delete Video'),
                    'class' => 'delete delete-product-video-url '
                ))
        );

        return parent::_prepareLayout();
    }

    /**
     * @return mixed
     */
    public function getAddButtonId()
    {
        $buttonId = $this->getLayout()
            ->getBlock('add_button')->getId();
        return $buttonId;
    }

    /**
     * @return string
     */
    public function getDeleteButtonHtml()
    {
        return $this->getChildHtml('delete_button');
    }

    /**
     * @return array
     */
    public function getVideoValues()
    {
        $videosArr = array_reverse($this->getProduct()->getVideo(), true);

        if (!$this->_values) {
            $values = array();
            foreach ($videosArr as $key => $val) {
                $value = array();
                $value['id'] = $key;
                $value['is_delete'] = $val['is_delete'];
                $value['url'] = $val['url'];
                $value['sort_order'] = $val['sort_order'];
                $value['video_id'] = $val['video_id'];
                $value['item_count'] = $key;

                $values[] = new Varien_Object($value);
            }
            $this->_values = $values;
        }
        return $this->_values;

    }

}

в конструктор мы передаем темлейт полей

<?php

?>
<?php echo $this->getTemplatesHtml() ?>

<script type="text/javascript">
    //<![CDATA[
    var firstStepTemplate = '<div class="video-url-box" id="video_{{id}}">' +
        '<table id="<?php echo $this->getFieldId() ?>_{{id}}" class="video-header" cellpadding="0" cellspacing="0">' +
        '<input type="hidden" id="<?php echo $this->getFieldId() ?>_{{id}}_is_delete" name="<?php echo $this->getFieldName() ?>[{{id}}][is_delete]" value="" />' +
        '<input type="hidden" id="<?php echo $this->getFieldId() ?>_{{id}}_video_id" name="<?php echo $this->getFieldName() ?>[{{id}}][video_id]" value="{{video_id}}" />' +
        '<thead>' +
        '<tr>' +
        '<th class="opt-url"><?php echo $this->jsQuoteEscape(Mage::helper('catalog')->__('URL')) ?></th>' +
        '<th class="opt-order"><?php echo $this->jsQuoteEscape(Mage::helper('catalog')->__('Sort Order')) ?></th>' +
        '<th class="a-right"><?php echo $this->jsQuoteEscape($this->jsQuoteEscape($this->getDeleteButtonHtml())) ?></th>' +
        '</tr>' +
        '</thead>' +
        '<tr>' +
        '<td><input type="url" class="input-text" id="<?php echo $this->getFieldId() ?>_{{id}}_url" name="<?php echo $this->getFieldName() ?>[{{id}}][url]" value="{{url}}">{{checkboxScopeUrl}}</td>' +
        '<td><input type="text" class="validate-zero-or-greater input-text" name="<?php echo $this->getFieldName() ?>[{{id}}][sort_order]" value="{{sort_order}}"></td>' +
        '<td> </td>' +
        '</tr></table></div>';
    var productVideo = {
        div: $('product_video_container_top'),
        templateSyntax: /(^|.|\r|\n)({{(\w+)}})/,
        templateText: firstStepTemplate,
        itemCount: 1,
        add: function (data) {
            this.template = new Template(this.templateText, this.templateSyntax);

            if (!data.id) {
                data = {};
                data.id = this.itemCount;
                data.type = '';
                data.video_id = 0;
            } else {
                this.itemCount = data.item_count;
            }

            Element.insert(this.div, {'after': this.template.evaluate(data)});

            this.itemCount++;
            this.bindRemoveButtons();
        },
        remove: function (event) {
            var element = $(Event.findElement(event, 'div'));
            if (element) {
                $('product_' + element.readAttribute('id') + '_' + 'is_delete').value = '1';
                element.addClassName('no-display');
                element.addClassName('ignore-validate');
                element.hide();
            }
        },
        bindRemoveButtons: function () {
            var buttons = $$('.delete.delete-product-video-url');
            for (var i = 0; i < buttons.length; i++) {
                if (!$(buttons[i]).binded) {
                    $(buttons[i]).binded = true;
                    Event.observe(buttons[i], 'click', this.remove.bind(this));
                }
            }
        }
    }

    productVideo.bindRemoveButtons();

    if ($('<?php echo $this->getAddButtonId() ?>')) {
        Event.observe('<?php echo $this->getAddButtonId() ?>', 'click', productVideo.add.bind(productVideo));
    }

    //adding data to templates
    <?php foreach ($this->getVideoValues() as $_value): ?>
    productVideo.add(<?php echo $_value->toJson() ?>);
    <?php endforeach; ?>
    //]]>
</script>

<div><?php if (!$this->isReadonly()): ?><input type="hidden" name="affect_product_custom_videos"
                                               value="1"/><?php endif; ?></div>

При написании блока и темплейта за основу брались с коровского функционала options для продукта.

Отлично, кнопку с полями добавили, теперь нам нужно подумать о сохранении данных.

Данные для продукта хранятся в EAV формате. То есть из-за большого колличества информации, данные разносятся в несколько таблиц, которые связаны между собой. Мы конечно же можем сделать новую таблицу и сохранять в нее. Но мы будем стараться придерживаться техник используемых в мадженто и создадим аттрибут продукта.

Company/ProductVideo/sql/company_productvideo_setup/install-0.1.0.php будет таким:

<?php

$installer = new Mage_Eav_Model_Entity_Setup('core_setup');
$installer->startSetup();

$installer->addAttribute('catalog_product', 'video', array(
    'type' => 'text',
    'backend' => 'company_productvideo/product_attribute_backend_video',
    'frontend' => '',
    'group' => 'Images',
    'label' => 'Product Video',
    'input' => '',
    'class' => '',
    'source' => '',
    'global' => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_GLOBAL,
    'visible' => false,
    'required' => false,
    'user_defined' => true,
    'default' => '',
    'searchable' => false,
    'filterable' => false,
    'comparable' => false,
    'visible_on_front' => false,
    'unique' => false,
    'is_configurable' => false
));

$installer->endSetup();

тут важно указывать тип, группу и бек энд модель. С остальным можно поиграться при желании.

И так если по 1му и 2му параметрам +/- понятно, то бек энд модель рассмотрим подробнее.

В виде данных у нас может быть много текста (ссылки и позиции), их колличество может варироваться. Поэтому предлагаю использовать сериализацию данных и сохранение в одной записи. Таким образом мы не будем плодить записи.

JS в теплейте с полями устроен таким образом, что при удалении полей, поле не удаляется, а в нем передается параметр “is_delete”, по которому данная запись будет исключена при записи. Сделано это для того что бы у нас была возможность удалять последнюю запись.

То есть если мы JS в темплейте при нажатии кнопки delete будем просто удалять элемент, то это будет работать до тех пор пока мы не станем удалять последний элемент. При удалении последнего, данные в параметре будут отсутствовать и соответственно последнюю запись не удалит. Решений данного момента можно рассматривать несколько, мы будем использовать принцип, который применялся в options для продукта. И так модель

Company/ProductVideo/Model/Product/Attribute/Backend/Video.php будет такая:

<?php

/**
 * Class Company_ProductVideo_Model_Product_Attribute_Backend_Video
 */
class Company_ProductVideo_Model_Product_Attribute_Backend_Video extends Mage_Eav_Model_Entity_Attribute_Backend_Serialized
{
    /**
     * Remove deleted rows and serialize before saving
     *
     * @param Varien_Object $object
     * @return Mage_Eav_Model_Entity_Attribute_Backend_Serialized
     */
    public function beforeSave($object)
    {
        // parent::beforeSave() is not called intentionally
        $attrCode = $this->getAttribute()->getAttributeCode();
        if ($object->hasData($attrCode)) {
            $videos = $object->getData($attrCode);
            foreach ($videos as $key => $val) {
                if ($val['is_delete'] == '1') {
                    unset($videos[$key]);
                }

            }
            $object->setData($attrCode, serialize($videos));
        }
        return $this;
    }
}

мы могли бы использовать коровскую модель, но так как нам перед сохранением нужно обрабатывать данные, то мы ее наследуем.

И так, мы сделали сохранение, остается передать данные на фронт. Для этого мы напишем хелпер

Company/ProductVideo/Helper/Data.php и добавим в него метод:

<?php

/**
 * Class Company_ProductVideo_Helper_Data
 */
class Company_ProductVideo_Helper_Data extends Mage_Core_Helper_Abstract
{
    /**
     * @param $product
     * @return array
     */
    public function getProductVideos($product)
    {
        $data = $product->getVideo();
        $videos = array();
        if ($data) {
            foreach ($data as $video) {
                $sort_order = $video['sort_order'];
                $url = $video['url'];

                array_push($videos, array(
                    'url' => $url,
                    'sort_order' => $sort_order,
                ));
            }
        }
        return $videos;
    }
}

вариантов передачи может быть несколько, зависит от того как мы хотим это обрабатывать в темлейте. Теперь мы можем создать переменную в темплейте PDP и передавая в

$productVideos = Mage::helper('company_productvideo')->getProductVideos($_product);

объект продукта, получая информацию о видео.

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

Leave a Reply