В отличии от 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);
объект продукта, получая информацию о видео.
Данный пример можно дорабатывать (валидации полей, использование апи для получения детальных данных о видео и тд). Надеюсь данная информация комуто будет полезна)