Loading a Magento’s custom product form field from the database (Without creating product attributes)

Assuming that you already understand how to save a custom field value to a database, the next step in order to provide a complete saving/loading solution to any custom field, without creating product attributes, is to develop a mechanism for reading the value from the database and displaying it to our custom form field every time the product form is being loaded.

In Magento we can develop our own data provider that will help us modify the product form; for this, we need to add new arguments to the Magento’s modifier pool in the /etc/adminhtml/di.xml file as follows:

<virtualTypename=”Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Pool“>

   <arguments>

       <argument name=”modifiers” xsi:type=”array”>

           <item name=”advancedCustomOptions” xsi:type=”array”>

               <item name=”class” xsi:type=”string”>Module\Vendor\Ui\DataProvider\ProductForm</item>

               <item name=”sortOrder” xsi:type=”number”>20</item>

           </item>

       </argument>

   </arguments>

</virtualType>

And inside the AstralWeb\DirectCheckout\Ui\DataProvider file, we need to create our data provider, in this case, the data provider will be called ProductForm:

<?php

namespace Module\Vendor\Ui\DataProvider;

use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;

use Magento\Catalog\Model\Locator\LocatorInterface;

use Magento\Framework\App\RequestInterface;

use Magento\Store\Model\StoreManagerInterface;

use Magento\Framework\Exception\NoSuchEntityException;

class ProductForm extends AbstractModifier

{

    protected $locator;

    protected $request;

   public function __construct(

       LocatorInterface $locator,

       RequestInterface $request) 

{

       $this->locator = $locator;

       $this->request = $request;

}

 /**

  * {@inheritdoc}

  */

 // This function modifies the form fields before being rendered.

 public function modifyMeta(array $meta)

 {

        return $meta;

 }

   /**

    * {@inheritdoc}

    */

   // This function requires to be imported but we don’t need it.

   public function modifyData(array $data)

   {

       return $data;

   }

}

After declaring our data provider we will be able not only to inject our custom value into the form but also help in conditionally displaying the field according to the data being read from the database.

Conditional rendering

Some merchants would require us to display the custom field for only some specific products, let’s say that we only want to show the custom field when opening any configurable product form only, so it means that for any other non-configurable product form the field will not be displayed.

For defining the above behavior we can add the following lines to the modifyMeta function in order to override the arguments for the amounts container:

public function modifyMeta(array $meta)

   {

       $product = $this->locator->getProduct();

      if ($productType != “configurable”)

       {

           $meta[“amounts”] = [

               “arguments” => [

                   “data” => [

                       “config” => [

                           “collapsible” => false,

                           ‘opened’ => false,

                           ‘canShow’ => false,

                           ‘visible’ => false

                       ]

                   ]

               ]

           ];

       }

Please take note of the index amounts, previously declared when extending the product form:

<argument name=”data” xsi:type=”array”>

   <item name=”config” xsi:type=”array”>

      <item name=”label” xsi:type=”string”>

         Min. Amount of Products

      </item>

      <item name=”collapsible” xsi:type=”boolean”>true</item>

      <item name=”dataScope” xsi:type=”string”>data.amounts</item>

      <item name=”sortOrder” xsi:type=”number”>10</item>

   </item>

 </argument>

In the data provider, we are overriding the display characteristics for the whole container when the product’s type is not configurable. When opening a non-configurable product form, the whole field container in this case will not be displayed, just like it is shown below after overriding the container properties.

Injecting the custom value

In the same way, we override the whole container display for any non-configurable product, we can also do the same for any element inside the container when we need to inject a custom value into the product form; we just need to go down one to the children index and override the field we need to, in this case, the qty field, so, following the same example, we can add an else statement as follow:

public function modifyMeta(array $meta)

   {

       $product = $this->locator->getProduct();

      if ($productType != “configurable”)

       {

         …

       }else{

       // Any logic for loading the qty value from database goes here

      //  $qty = …

           $meta[“amounts”] = [

               “arguments” => [

                   “data” => [

                       “config” => [

                           “sortOrder” => 1,

                       ]

                   ]

               ],

               ‘children’ => [

                   ‘qty’ => [

                       ‘arguments’ => [

                           ‘data’ => [

                               ‘config’ => [

                                   ‘value’ => (string)$qty

                               ]

                           ]

                       ]

                   ]

               ]

           ];

       }

       return $meta;

   }

In the above example, we are just overriding the value sample, in this way the value we get from the database will be displayed in the product form.

This will finally finish the most basic saving/loading flow for any custom field in the product form without creating product attributes. You are welcome to experiment with any other field types rather than text input, but for most of them, the steps are very similar, just being different in the elements needed to be overridden for a particular project.

In the next article, we will describe how to customize the style of any field from the product form, allowing us to make it distinctive from others.

Saving a custom Magento’s product form field to database (Without creating product attributes)

Following our last example on how to extend the product form with a custom text input, the next step to follow is to save the custom value of it everything the form is saved; when the product form is extended by using attributes created manually, Magento will deal with all saving process, but since we are extending the product form programmatically we need to save the value ourselves.

For this let’s suppose we already created the database tables and a whole model repository/interface structure for saving and retrieving data to retrieve/save data into it (these topics will not be covered in this article).

The big mistake!!!

The most common mistake when saving a custom field in the product form is to implement an observer for catalog_product_save_before or catalog_product_save_after or a plugin to intercept the saving method of the product repository. 

Unfortunately, by doing any of these methods there are high chances of accidentally creating an infinite loop, especially if you need to save any data to the product model itself; because saving the product will trigger another save event, and that event will be continually intercepted by the observer or plugin that will save again… and will keep doing this over and over again.

For avoiding an infinite loop, it is much safer to implement an aroundPlugin to the controller Magento\Catalog\Controller\Adminhtml\Product\Save, because by intercepting the controller we can have access to the form’s data before being saved, not having to perform a “double save” action compared to other methods.

Declare the plugin

Add the plugin declaration in your CustomModule/Vendor/etc/adminhtml/di.xml

<type 

   name=”Magento\Catalog\Controller\Adminhtml\Product\Save”>

      <plugin 

         name=”check_direct_qty_plugin”

         type=”AstralWeb\Quantity\Plugin\SaveProductPlugin”

         sortOrder=”50″ />

</type>

Name your plugin with any name you want and write your plugin file inside the Plugin folder. In this particular case, the filename is SaveProductPlugin.

In the file, we will inject any class we require and declare a beforeDispatch or an aroundDispatch function for having access to the request parameters:

public function aroundDispatch(Save $subject, $proceed, $request)

{

   $params = $request->getParams();

At this point, we should go back to our previous field definition to check for the scope defined for our custom input text.

<argument name=”data” xsi:type=”array”>

   <item name=”config” xsi:type=”array”>

      <item name=”label” xsi:type=”string” translate=”true”>

         Product Qty

      </item>

      <item name=”dataScope” xsi:type=”string”>qty</item>

      <item name=”sortOrder” xsi:type=”number”>500</item>

      <item name=”componentType” xsi:type=”string”>field</item>

      <item name=”dataType” xsi:type=”string”>text</item>

      <item name=”formElement” xsi:type=”string”>input</item>

      <item name=”additionalClasses” xsi:type=”string”>qty</item>

   </item>

</argument>

Since we declare qty as our dataScope it means we must access the value as follows:

public function aroundDispatch(Save $subject, $proceed, $request)

{

   $params = $request->getParams();

   echo $params[‘qty’];

// All your logic for saving the value into the database

return $proceed($request);

}

Applying the changes

After developing the code, execute the followings commands for applying our changes to the environment:

bin/magento setup:upgrade;

bin/magento setup:di:compile;

bin/magento setup:static-content:deploy en_US es_MX;

bin/magento index:reindex;

bin/magento cache:flush;

bin/magento cache:clean;

After executing the above commands, edit any product and enter any integer in the custom text input and save it; you will see immediately that the custom value is saved into the database.

What’s next?

After making sure our custom value is being saved into our database we can start to retrieve it and display it every time the product form is being open. The next article will cover the steps for loading the data back after saving it successfully.

Extending the Magento’s product form (Without creating product attributes)

For merchants, adding very uniques properties or characteristics to their stores will allow them to stand up in front of the competition, and luckily for them, Magento allows a high customization level out of the box; but still, some projects might require very specific customizations that are not 100% customized in the admin panel, such as for example: requesting to purchase a minimum quantity of a particular product before checking, changing the checkout flow when a product doesn’t meet specific criteria or conditions, enabling/disabling the product on frontend after some custom attribute previously defined, etc; and for these customizations, we usually require to extend the product form.

In Magento, adding new fields to the product form is one of the most powerful features of its framework, the development flow is quite simple; the official documentation provides many examples; however, it lacks a proper explanation of the elements involved in the whole process; for this reason, the purpose of this document is to ease a little bit the understanding of this customization task.

Let’s assume that a merchant requires to add a new text input into the product form, allowing the store to set a minimum amount of one single product in the cart before letting customers checkout.

Usually what you would do as a developer is to programmatically create a new product attribute and assign it to one or more attribute sets, or instead, manually create a new attribute in the administrator backend and assign it to any attribute set that fits the project’s requirement.

By doing any of the methods described above the new field will be displayed, saved, and loaded out of the box anytime a product form is used, very conveniently; however, there are a couple of limitations that would require us to add the field programmatically:

  • The attribute sets will vary over time and when creating/editing a new attribute set, the administrator might forget to add the new attribute into the new set.
  • The merchant might require to display the new field in only the products that meet specific conditions, something that clearly cannot be achieved out of the box at the moment of writing this article. 

When adding a new field programmatically, in the most basic form, it will require extending the original product_form.xml and including the following components: 

  • Form: The basic form declaration.
  • Fieldset: A component-like header that is able to collapse and contains as many fields as possible.
  • Field: The actual input.

The most basic xml structure will look something like this:

<form>

<fieldset>

<field>

</field>

….

<field>

</field>

</fieldset>

</form>

Going back to our example, let’s go step by step more deeply into how to extend the product_form.xml.

First of all, we need to create a product_form.xml file in the module, inside the path   CompanyName/ModuleName/view/adminhtml/ui_component/ and keep following the basic structure with some extra configuration data

<form xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”urn:magento:module:Magento_Ui:etc/ui_configuration.xsd”>

</form>

There is nothing mysterious about the code above, we are just adding the necessary configuration for Magento to recognize our new form elements, something very similar needs to be done with the fieldset:

<fieldset name=”amounts”>

</fieldset>

The name attribute will allow us the identify the fieldset programmatically, and must be unique among all of the components in the form. Furthermore, the fieldset will also require an extra child named argument for defining specific configuration for the fieldset:

<argument name=”data” xsi:type=”array”>

   <item name=”config” xsi:type=”array”>

      <item name=”label” xsi:type=”string”>

         Min. Amount of Products

      </item>

      <item name=”collapsible” xsi:type=”boolean”>true</item>

      <item name=”dataScope” xsi:type=”string”>data.amounts</item>

      <item name=”sortOrder” xsi:type=”number”>10</item>

   </item>

 </argument>

Most of the lines can be copied and pasted, but need to pay attention to the following ones inside the config array:

  • label: This is very self-explanatory, it is what the administrator will see on the UI. 
  • dataScope: This is how the data will be accessible inside the form array when need to save and retrieve the data, and it is very important to follow the convention ‘data.index’, otherwise the data would be inaccessible; in this case, our index inside the form array will be ‘amounts’.  

Following our basic structure the field element would look like this:

<field name=”qty”>

</field>

Just as the fieldset element, the name attribute will allow us the identify the field programmatically, and must be unique among all of the components in the form. Also, the field will also require an extra child named argument for defining the specific configuration for it:

<argument name=”data” xsi:type=”array”>

   <item name=”config” xsi:type=”array”>

      <item name=”label” xsi:type=”string” translate=”true”>

         Product Qty

      </item>

      <item name=”dataScope” xsi:type=”string”>qty</item>

      <item name=”sortOrder” xsi:type=”number”>500</item>

      <item name=”componentType” xsi:type=”string”>field</item>

      <item name=”dataType” xsi:type=”string”>text</item>

      <item name=”formElement” xsi:type=”string”>input</item>

      <item name=”additionalClasses” xsi:type=”string”>qty</item>

   </item>

</argument>

Most of the lines above are critical for our text input definition, but need to pay attention to the following ones:

  • label: This is very self-explanatory, it is what the administrator will see on the UI. 
  • dataScope: This is how the data will be accessible inside the form array when need to save and retrieve the data; in this case, our index inside the form array will be ‘qty’.  
  • componentType: in our case, it needs to be a field.
  • dataType: The datatype that will be used in our text input; in this string is just fine, we can cast to string or int if needed, this will avoid conversion issues between our Magento application and the database.
  • formElement: input type.
  • adiddionatinalClasses: Any additional CSS class that will be required in order to modify the UI of the component, in this case, we are using a CSS class named ‘qty’.

By following all these steps our complete form should look like this:

<form xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”urn:magento:module:Magento_Ui:etc/ui_configuration.xsd”>

   <fieldset name=”amounts”>

       <argument name=”data” xsi:type=”array”>

           <item name=”config” xsi:type=”array”>

               <item name=”label” xsi:type=”string”>Min. Amount of Products</item>

               <item name=”collapsible” xsi:type=”boolean”>true</item>

               <item name=”dataScope” xsi:type=”string”>data.amounts</item>

               <item name=”sortOrder” xsi:type=”number”>10</item>

           </item>

       </argument>

       <field name=”qty”>

           <argument name=”data” xsi:type=”array”>

               <item name=”config” xsi:type=”array”>

                   <item name=”label” xsi:type=”string” translate=”true”>Product Qty</item>

                   <item name=”dataScope” xsi:type=”string”>qty</item>

                   <item name=”sortOrder” xsi:type=”number”>500</item>

                   <item name=”componentType” xsi:type=”string”>field</item>

                   <item name=”dataType” xsi:type=”string”>text</item>

                   <item name=”formElement” xsi:type=”string”>input</item>

                   <item name=”additionalClasses” xsi:type=”string”>qty</item>

               </item>

           </argument>

       </field>

   </fieldset>

</form>

And finally, if you open any product form in the admin backend you will find a new title “Min. Amount of Products”, including a new text input “Product Qty”.

The next article will explain the steps for loading the data back any time the administrator is saving or opening a form, but understanding the basics concepts explained in this article is the key to truly having a good perception and enhancing full control of the product form.

How to troubleshoot Magento in Chrome? Brief Review on MSP DevTools for Magento

It is a common practice among Web developers to use Chrome’s Developer tools to test, debug and troubleshoot on any website; however, none of them provide enough resources to deal with Magento, especially for backend developers, relying on more unorthodox methods, like printing messages on the UI or the Developer’s tools’ console.

Luckily, MageSpecialist DevTools is a free module, that can be found on Github at https://github.com/magespecialist/m2-MSP_DevTools , that reduces considerably those headaches by providing powerful analysis and data totally aimed to work with Magento.

Advantages of using MageSpecialist DevTools:

  • Magento’s layout debug functionality is very confusing and does not display enough information for troubleshooting; being also very intrusive.
  • It will help you to determine if there is any load observer being executed; easing the decision to replace them by writing a plugin instead.
  • You might detect what blocks or layouts are taking a long time to load.
  • The tool will also allow you to detect any heavy and unused code being unnecessary rendered.

Installation

Install the MageSpecialist Chrome Toolbar

First, install the MageSpecialist Chrome Toolbar in order to install a new Magento tab in Chrome’s Developer tool

https://chrome.google.com/webstore/detail/magespecialist-devtools-f/odbnbnenehdodpnebgldhhmicbnlmapj

After installing the above extension you will see a new Magento tab after right-clicking and selecting “Inspect” on the page.

After installing the MageSpecialist Chrome Toolbar you are ready for the next step.

Install the PhpStorm Remote Call plugin

The PhPStorm Remote Call will allow you to directly open a file in PhpStorm just by clicking on the link or the icon associated with it. Go to File → Settings →Plugins, and make sure the “Marketplace” is being selected.

Then inside the text input type “Remote Call” and proceed with the installation. The plugin will be ready to use after restarting the IDE.

Install the MSP DevTools module by composer

Inside your Magento root please execute the following command in order to install the MSP DevTools module

# composer require msp/devtools

Enable the profiler

Setting the profiler on MSP DevTools is composed of three steps:

a. Edit the bootstrap file

The pub/bootsrap.php file contains a basic configuration that is loaded on boot time by the Magento App, so we need to set the Magento profiler on boot time as by making the following changes:

Original

$profilerConfig = isset($_SERVER[‘MAGE_PROFILER’]) && strlen($_SERVER[‘MAGE_PROFILER’])

    ? $_SERVER[‘MAGE_PROFILER’]

    : trim(file_get_contents(BP . ‘/var/profiler.flag’));

if ($profilerConfig) {

    $profilerConfig = json_decode($profilerConfig, true) ?: $profilerConfig;

}

After modification:

$profilerConfig = $_SERVER[‘MAGE_PROFILER’] = [ ‘drivers’ => [[‘output’ => ‘MSP\DevTools\Profiler\Driver\Standard\Output\DevTools’]] ];

//Comment the following lines

/*$profilerConfig = isset($_SERVER[‘MAGE_PROFILER’]) && strlen($_SERVER[‘MAGE_PROFILER’])

    ? $_SERVER[‘MAGE_PROFILER’]

    : trim(file_get_contents(BP . ‘/var/profiler.flag’));

if ($profilerConfig) {

    $profilerConfig = json_decode($profilerConfig, true) ?: $profilerConfig;

}*/

b. Enable the SQL query feature.

Edit the app/etc/env.php file and add the following line to the default db connection:

  ),

  ‘db’ => 

  array (

    ‘table_prefix’ => ”,

    ‘connection’ => 

    array (

      ‘default’ => 

      array (

        ‘host’ => ‘localhost’,

        …

        ‘profiler’ => ‘1’,

      ),

    ),

  ),

  ‘resource’ => 

  array (

  1. Enable the Magento profiler

The Magento profiler can be enabled by simply executing the following command:

# dev:profiler:enable html

  1. Enabling the  MSP DevTools
  1. Flush your cache.
  2. Turn OFF Full Page Cache while you are using DevTools.
  3. Upgrade database data & schema: php bin/magento setup:upgrade
  4. Open Magento backend and go to Stores > Settings > Configuration > MageSpecialist > DevTools
  5. Enable devtools and set IP restrictions.

Features

The most powerful features include the following:

  • General tab

The General tab information will display information as the Magento version, and the configured locale.

  • Observers tab

The Observers tab will display the observers being used in the current URL and the total time spent in executing. Any high number would mean there is a performance issue in the observer and would require troubleshooting or redesign.

Clicking on any of them will display the file path of the file in your local project, and will allow you to open it directly into PhpStorm.

  • Blocks tab

The Blocks tab will display the layout blocks being rendered in the current URL and the total time spent in rendering them. Any high number would mean there is a performance issue in the block and would require troubleshooting or redesign.

Clicking on any of them will display the file path of the file in your local project, and will allow you to open it directly into PhpStorm.

It is important to note that the MSP DevTool module will add a data-mspdevtools attribute to most HTML tags, that can be used in the search box for listing the specific element.

  • UI tab

The Ui tab will display the Ui components being used in the current URL and the total time spent in executing. Also, clicking on any of them will display more specific info about any of them.

  • Magento’s profiler can be configured to be displayed inside Chrome’s Developer tools instead of being displayed at the bottom of the page.

Conclusion

The MSP DevTools module will save you a lot of time developing and troubleshooting on both the front end and back end; also will help you how to detect any performance issue that requires to be addressed, which is a “must to have” module on your development environment.

Magento – Saving Data Directly into Magento Cache

Magento uses many cache types out of the box; being very convenient for both administrators and developers, due to its decentralized design that provides specialized caching for different purposes.

When using Magento, its architecture will manage and determine the best cache type and policy to use for every particular situation; providing a more powerful solution compared with other frameworks that only provide single cache storage.

Magento is also flexible enough to allow you as a developer to implement and utilize a custom cache for any specific purpose. The process is very simple for any developer; in the following examples let’s suppose that we are working with an existent module called Astralweb_Cache. The basic procedure for using Magento cache would include the following procedures:

  1. Create a cache XML file

Create a new file in AstralWeb/Cache/etc/cache.xml with the following content:

<?xml version=”1.0″?>

<config xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”urn:magento:framework:Cache/etc/cache.xsd”>

    <type name=”astralweb_cache” translate=”label,description” instance=”AstralWeb\Cache\Model\Cache\AstralWebCache”>

        <label>AstralWeb Cache</label>

        <description>AstralWeb Custom Cache</description>

    </type>

</config>

In which:

name: This is the unique and internal cache code to be used for identifying the cache. 

instance: The PHP class will be responsible for constructing the custom cache type.

  1. Create the class AstralWen\Cache\Model\Cache\AstralWebCache

<?php

namespace AstralWeb\Cache\Model\Cache;

use Magento\Framework\App\Cache\Type\FrontendPool;

use Magento\Framework\Cache\Frontend\Decorator\TagScope;

/**

 * System / Cache Management / Cache “Label”

 */

class AstralWebCache extends TagScope

{

    /**

     * The unique and internal code to be used for identifying the cache.

     */

    const TYPE_IDENTIFIER = ‘astralweb_cache’;

    /**

     * The tag name used for identifying the cache scope

     */

    const CACHE_TAG = ‘ASTRALWEB_CACHE_TAG’;

    /**

     * @param FrontendPool $cacheFrontendPool

     */

    public function __construct(FrontendPool $cacheFrontendPool)

    {

        parent::__construct(

            $cacheFrontendPool->get(self::TYPE_IDENTIFIER),

            self::CACHE_TAG

        );

    }

}

After that you will be able to see AstralWeb Cache in System → Cache Management, meaning that the cache has been successfully created.

  1. Using the custom cache

After creating the custom cache the most common way to insert data into it is by injection into almost any class that requires inserting data.

use Magento\Framework\App\CacheInterface;

use Magento\Framework\Serialize\SerializerInterface;

use Magento\Framework\App\Cache\TypeListInterface 

use AstralWeb\Cache\Model\Cache\AstralWebCache;

….

….

….

protected $_cache;

protected $_serializer;

protected $_cacheTypeList;

/**

 * @param CacheInterface $_cache

 * @param SerializerInterface $_serializer

 */

public function __construct(

CacheInterface $cache, 

SerializerInterface $serializer,

CacheTypeList $cacheTypeList

)

{

    $this->_cache = $cache;

    $this->_serializer = $serializer;

    $this->_cacheTypeList = $cacheTypeList;

}

….

….

….

  1. Saving data into the custom cache

The following example illustrates how to insert custom data into the cache. You should notice that the data, or object, to be saved is serialized into a string.

public function saveToCache($dataToSave)

{

    $cacheKey  = AstralWebCache::TYPE_IDENTIFIER;

    $cacheTag  = AstralWebCache::CACHE_TAG;

    $storeData = $this->_cache->save(

        $this->_serializer->serialize($dataToSave), // Serialization

        $cacheKey,

        [$cacheTag],

        86400 // lifetime of the cached data (seconds)

    );

}

  1. Reading data from the custom cache

Reading data from custom cache is exactly the opposite process. You should note that the data or object will be unserialized.

public function readFromCache()

{

    $cacheKey  = AstralWebCache::TYPE_IDENTIFIER;

    $data = $this->_serializer->unserialize($this->_cache->load($cacheKey));

}

Please note that the data from the custom cache will be accessible until it reaches lifetime and before the Administrator flushes the cache. Also, if the saved data has been re-saved with different data, it will not be retrieved correctly until the cache is flushed.

  1. Invalidating the custom cache

If after re-saving the cache with different data you would like to display the cache as “invalid” to the Administrator you need to execute the following code.

public function invalidateCache()

{

    $this->_cacheTypeList->invalidate(AstralWebCache::TYPE_IDENTIFIER);

}

  1. Flushing the custom cache type

You can flush the cache manually on System → Cache Management, but you can also flush the cache programmatically as this:

public function flushCache()

{

    $this->_cacheTypeList->cleanType(AstralWebCache::TYPE_IDENTIFIER);

}

The only disadvantage of using Magento cache for your own cache is that you will not be able to configure it according to your needs, or you might not be able to reduce the abstraction layers you would like for simplifying the process even more.

However, the above approach will allow you to provide a faster response by using Magento internal cache mechanism. Instead of fetching from the databases, the requested data will be returned from the cache.