magento2: Cannot create full datetime product attribute

It appears to be impossible to add a complete datetime attribute, one that saves both date and time. I’ve created the attributes with backend_type => 'datetime' and included custom frontend_input_renderers to get a calendar picker that has both date and time. Unfortunately, the time is always stripped off due to the hardcoded force for datetime attributes to be filtered by date. This occurs here.

Please let me know if there is an intended way around this. Seems like a lot of trouble to create an entire new backend type for this, when it should be stored in the catalog_product_entity_datetime table.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 5
  • Comments: 24 (8 by maintainers)

Commits related to this issue

Most upvoted comments

@jzahedieh yea sorry for the wait. It’s been a while, but I think these are all the pieces. Keep in mind this code is from like a year ago, I think we were still on 2.0.* or something. So for the helper preference, you might want to check on that copy-pasted stuff and make sure it’s up to date.

New datetime form element

Block/Adminhtml/Form/Element/Datetime.php

<?php
/**
 * @package     BlueAcorn\ContentPublisher
 * @version     1.0.0
 * @author      Sam Tay @ Blue Acorn, Inc. <code@blueacorn.com>
 * @copyright   Copyright © 2016 Blue Acorn, Inc.
 */
namespace BlueAcorn\ContentPublisher\Block\Adminhtml\Form\Element;

use Magento\Framework\Data\Form\Element\Date;

/**
 * Class Datetime
 *
 * Created to allow full date+time picker on eav attribute. See \Magento\Backend\Block\Widget\Form methods
 * _setFieldset and _applyTypeSpecificConfig for why this is necessary.
 */
class Datetime extends Date
{
    /**
     * Override to force date and time formats before rendering html
     *
     * @return string
     * @throws \Exception
     */
    public function getElementHtml()
    {
        $this->setDateFormat($this->localeDate->getDateFormat(\IntlDateFormatter::SHORT));
        $this->setTimeFormat($this->localeDate->getTimeFormat(\IntlDateFormatter::SHORT));
        return parent::getElementHtml();
    }
}

Use it in install script

Setup/InstallData.php

<?php
/**
 * @package     BlueAcorn\ContentPublisher
 * @version     1.0.0
 * @author      Sam Tay @ Blue Acorn, Inc. <code@blueacorn.com>
 * @copyright   Copyright © 2016 Blue Acorn, Inc.
 */
namespace BlueAcorn\ContentPublisher\Setup;

use Magento\Eav\Setup\EavSetup;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;

class InstallData implements InstallDataInterface
{
    /**
     * EAV setup factory
     *
     * @var EavSetupFactory
     */
    private $eavSetupFactory;

    /**
     * Init
     *
     * @param EavSetupFactory $eavSetupFactory
     */
    public function __construct(EavSetupFactory $eavSetupFactory)
    {
        $this->eavSetupFactory = $eavSetupFactory;
    }

    /**
     * {@inheritdoc}
     */
    public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
    {
        /** @var EavSetup $eavSetup */
        $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]);
        $eavSetup->removeAttribute(\Magento\Catalog\Model\Product::ENTITY, 'publish_start');
        $eavSetup->addAttribute(
            \Magento\Catalog\Model\Product::ENTITY,
            'publish_start',
            [
                'label' => 'Enable Start Date',
                'type' => 'datetime',
                'input' => 'date',
                'input_renderer' => 'BlueAcorn\ContentPublisher\Block\Adminhtml\Form\Element\Datetime',
                'class' => 'validate-date validate-date-range date-range-publish-from',
                'backend' => 'BlueAcorn\ContentPublisher\Model\Entity\Attribute\Backend\Startdate',
                'required' => false,
                'group' => 'Product Details',
                'sort_order' => 18
            ]
        );
        $eavSetup->removeAttribute(\Magento\Catalog\Model\Product::ENTITY, 'publish_end');
        $eavSetup->addAttribute(
            \Magento\Catalog\Model\Product::ENTITY,
            'publish_end',
            [
                'label' => 'Enable End Date',
                'type' => 'datetime',
                'input' => 'date',
                'input_renderer' => 'BlueAcorn\ContentPublisher\Block\Adminhtml\Form\Element\Datetime',
                'class' => 'validate-date validate-date-range date-range-publish-to',
                'backend' => 'Magento\Eav\Model\Entity\Attribute\Backend\Datetime',
                'required' => false,
                'group' => 'Product Details',
                'sort_order' => 19,
                'note' => __('If you want to keep a product enabled indefinitely, leave the start and end dates empty.'
                    . ' If an "Enable End Date" exists, the product will remain disabled after the end date has passed.')
            ]
        );
    }
}

Overwrite product initialization helper

etc/di.xml

<?xml version="1.0"?>
<!--
/**
 * @package     BlueAcorn\ContentPublisher
 * @version     1.0.0
 * @author      Sam Tay @ Blue Acorn, Inc. <code@blueacorn.com>
 * @copyright   Copyright © 2016 Blue Acorn, Inc.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper"
                type="BlueAcorn\ContentPublisher\Helper\Adminhtml\Catalog\Product\Initialization" />
</config>

Helper/Adminhtml/Catalog/Product/Initialization.php

<?php
/**
 * @package     BlueAcorn\ContentPublisher
 * @version     1.0.0
 * @author      Sam Tay @ Blue Acorn, Inc. <code@blueacorn.com>
 * @copyright   Copyright © 2016 Blue Acorn, Inc.
 */
namespace BlueAcorn\ContentPublisher\Helper\Adminhtml\Catalog\Product;

use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper as CatalogHelper;

/**
 * Class Initialization
 * Rewriting class to allow date+time on datetime attributes
 */
class Initialization extends CatalogHelper
{
    /**
     * @var \Magento\Framework\Stdlib\DateTime\Filter\DateTime
     */
    protected $dateTimeFilter;

    /**
     * @param \Magento\Framework\App\RequestInterface $request
     * @param \Magento\Store\Model\StoreManagerInterface $storeManager
     * @param \Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter $stockFilter
     * @param \Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks $productLinks
     * @param \Magento\Backend\Helper\Js $jsHelper
     * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter
     * @param \Magento\Framework\Stdlib\DateTime\Filter\DateTime $dateTimeFilter
     */
    public function __construct(
        \Magento\Framework\App\RequestInterface $request,
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter $stockFilter,
        \Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks $productLinks,
        \Magento\Backend\Helper\Js $jsHelper,
        \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter,
        \Magento\Framework\Stdlib\DateTime\Filter\DateTime $dateTimeFilter
    ) {
        $this->dateTimeFilter = $dateTimeFilter;
        parent::__construct(
            $request,
            $storeManager,
            $stockFilter,
            $productLinks,
            $jsHelper,
            $dateFilter
        );
    }

    /**
     * Initialize product before saving
     *
     * @param \Magento\Catalog\Model\Product $product
     * @return \Magento\Catalog\Model\Product
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     * @SuppressWarnings(PHPMD.NPathComplexity)
     */
    public function initialize(\Magento\Catalog\Model\Product $product)
    {
        $productData = $this->request->getPost('product');
        unset($productData['custom_attributes']);
        unset($productData['extension_attributes']);

        if ($productData) {
            $stockData = isset($productData['stock_data']) ? $productData['stock_data'] : [];
            $productData['stock_data'] = $this->stockFilter->filter($stockData);
        }

        foreach (['category_ids', 'website_ids'] as $field) {
            if (!isset($productData[$field])) {
                $productData[$field] = [];
            }
        }

        $wasLockedMedia = false;
        if ($product->isLockedAttribute('media')) {
            $product->unlockAttribute('media');
            $wasLockedMedia = true;
        }

        $dateFieldFilters = [];
        $attributes = $product->getAttributes();
        foreach ($attributes as $attrKey => $attribute) {
            if ($attribute->getBackend()->getType() == 'datetime') {
                if (array_key_exists($attrKey, $productData) && $productData[$attrKey] != '') {
                    // BEGIN BA CHANGES
                    $dateFieldFilters[$attrKey] =
                        ($attribute->getFrontendInputRenderer() == 'BlueAcorn\ContentPublisher\Block\Adminhtml\Form\Element\Datetime')
                            ? $this->dateTimeFilter
                            : $this->dateFilter;
                    // END BA CHANGES
                }
            }
        }

        $inputFilter = new \Zend_Filter_Input($dateFieldFilters, [], $productData);
        $productData = $inputFilter->getUnescaped();

        $product->addData($productData);

        if ($wasLockedMedia) {
            $product->lockAttribute('media');
        }

        if ($this->storeManager->hasSingleStore()) {
            $product->setWebsiteIds([$this->storeManager->getStore(true)->getWebsite()->getId()]);
        }

        /**
         * Check "Use Default Value" checkboxes values
         */
        $useDefaults = $this->request->getPost('use_default');
        if ($useDefaults) {
            foreach ($useDefaults as $attributeCode) {
                $product->setData($attributeCode, false);
            }
        }

        $links = $this->request->getPost('links');
        $links = is_array($links) ? $links : [];
        $linkTypes = ['related', 'upsell', 'crosssell'];
        foreach ($linkTypes as $type) {
            if (isset($links[$type])) {
                $links[$type] = $this->jsHelper->decodeGridSerializedInput($links[$type]);
            }
        }
        $product = $this->productLinks->initializeLinks($product, $links);

        /**
         * Initialize product options
         */
        if (isset($productData['options']) && !$product->getOptionsReadonly()) {
            // mark custom options that should to fall back to default value
            $options = $this->mergeProductOptions(
                $productData['options'],
                $this->request->getPost('options_use_default')
            );
            $product->setProductOptions($options);
        }

        $product->setCanSaveCustomOptions(
            (bool)$this->request->getPost('affect_product_custom_options') && !$product->getOptionsReadonly()
        );

        return $product;
    }
}

If you are implementing start/end, set max value stuff

etc/adminhtml/events.xml

<?xml version="1.0"?>
<!--
/**
 * @package     BlueAcorn\ContentPublisher
 * @version     1.0.0
 * @author      Sam Tay @ Blue Acorn, Inc. <code@blueacorn.com>
 * @copyright   Copyright © 2016 Blue Acorn, Inc.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="catalog_product_validate_before">
        <observer name="setStartDateMaxValue" instance="BlueAcorn\ContentPublisher\Observer\Adminhtml\Product\SetStartDateMaxValue"/>
    </event>
</config>

Observer/Adminhtml/Product/SetStartDateMaxValue.php

<?php
/**
 * @package     BlueAcorn\ContentPublisher
 * @version     1.0.0
 * @author      Sam Tay @ Blue Acorn, Inc. <code@blueacorn.com>
 * @copyright   Copyright © 2016 Blue Acorn, Inc.
 */
namespace BlueAcorn\ContentPublisher\Observer\Adminhtml\Product;

use Magento\Framework\Event\Observer as EventObserver;
use Magento\Framework\Event\ObserverInterface;

/**
 * Class SetStartDateMaxValue
 * Observes catalog_product_validate_before
 * Purpose: Set max value on start date equal to end date value
 */
class SetStartDateMaxValue implements ObserverInterface
{
    /**
     * Execute observer
     * @param EventObserver $observer
     */
    public function execute(EventObserver $observer)
    {
        /** @var \Magento\Catalog\Model\Product $product */
        $product = $observer->getEvent()->getProduct();
        $product->getResource()->getAttribute('publish_start')
            ->setMaxValue($product->getPublishEnd());
    }
}

I opened this issue in 2016… haven’t worked on Magento in years. Congrats though!