Categories
Uncategorised

Vue

Templates

Vue templates end in .vue and can have three sections; template, script and style.

A lang attribute can be applied to the style tag to use other preprocessor languages;

<style lang="scss">

Styles included in a template are split out and combined into a separate CSS file. Using SCSS allows us to use import and SCSS language features within a template file;

@import "../../scss/components/light-component";
<style lang="scss">

Child Components

TODO!

Data

Initialise a Vue component with a data object. Return this from the data function when on initialisation, or include it in the script part of the .vue template file.

Data can be modified ‘live’, and will be re-rendered when the data changes. It’s a bit like KnockoutJS, but doesn’t have observer object nonsense and function calls to return the primitive value of data. Instead it uses Javascript’s getters and setters which are actually method calls on the object. Each data key defined will have a corresponding get and set method defined on the component.

Viewing a vue component in the console shows the getters and setters at the end of the object definition which all link to the same proxyGetter`` andproxyGetter“` methods. In Vue 3, a native Javascript proxy object is used instead of Vue’s own implementation.

Data values can be edited live in the browser. Installing Vue Browser Dev tools will provide an easier way to modify and debug functionality without assigning the app or component to a global window variable.

Data keys are accessible on the component as first party variables.

Props

Props are a way for a component to receive data in a read-only fashion. They don’t show in the Vue Dev tools and aren’t changeable. Think of props in a component like “arguments in a function” in PHP.

Props are accessible in templates in the same way as data keys, using curly bracket notation.

Props can be defined as an array or an object in our module using the following syntax. This allows us to specify types for props.

Array – no validation.

props: ['title'],

Objects #1 – define type

props: {
  title: String
}

Object 2 – with required

props: {
  title: {
    type: String,
    required: true
  }
}

Default Values

Use the default key when defining the prop;

props: {
  title: {
    type: String,
    required: true,
    default: "I'm the default value"
  }
}

It’s advisable to use the more verbose syntax and define types for props. We can also set a ‘required’ key here.

To pass a prop to a component, define it as an attribute on the component’s tag;

<legend-component title="Ooh matron!" />

Data Location ### 

Data should live in one place, on the deepest component which requires it, and then passed to child components via props.

When data is passed to a child component via a prop, the prop Should not be changed by the child. The value will update, but the parent will not be notified.

Communication UP with $emit (Events)

Broadcast events for parent components to listen to using

  methods: {
    toggleCollapsed() {
      this.$emit('toggle-collapsed');
    },
  },

Simple events can also be included inline in templates, rather than calling a method;

<button @click="$emit('toggle-collapsed')">Click</button>

this is automatically assumed when referencing properties in a template

We can then listen to this event when defining the component in a parent template using

<sidebar :collapsed="sidebarCollapsed" 
   @toggle-collapsed="toggleSidebarCollapsed" 
/>

@ is a shortcut for v-on:

  methods: {
    toggleSidebarCollapsed() {
      this.sidebarCollapsed = !this.sidebarCollapsed;
    },
  },

This passes :collapsed as a prop into the sidebar component. When the sidebar $emits the toggle-collapsed event, the toggleSidebarCollapsed method is called, this.sidebarCollapsed is updated, and propogated back to the sidebar component via the prop link.

Events being broadcast can be observed in the Vue Dev Tools’ Events tab in realtime.

v-bind: Dynamic Attributes

To define a dynamic value when passing in a prop, use v-bind. This uses the key from the data array as the value of the attribute, which must be prepended with v-bind:. v-bind has the shortcut of colon.

<legend-component v-bind:title="legend" />

This will take the “legend” value of the parent component’s data key and re-render the child component when it changes. The value in the attribute is actually a javascript expression, so things can be done such as;

<legend-component v-bind:title="legend + ' extra text'" />

which will concatenate the string onto the value when it changes.

v-bind: Shorthand

Use

<legend-component :title="legend + ' extra text'" />

which is an alias for v-bind.

Modular Styles

When including styles in a vue template, we can use the ‘module’ attribute to tell vue to suffix our CSS with a random string.

<style lang="scss" module>

This allows us to access modular styles in our template using the special $style variable which is made available. Use :class as a shortcut to the v-bind method.

The style object is a property of the Vue component. access

<div :class="$style.sidebar">

Class can also use an array of elements if other classes need to be used alongside the module generated class name. Use the following;

<div :class="[$style.sidebar, 'p-3', 'mb-5']">

Another trick of :class is the use of an object instead of an array. This allows us to use the classnames as keys, and set whether they should be active or not. To use dynamic keys, such as the $style.sidebar above, it needs to be wrapped in square brackets. Confusing as it looks like an array.

  <div :class="{ 
    [$style.component]: true, 
    'p-3': true, 
    'mb-5': true 
    }">

:global – don’t namespace a child class

When Vue generates module CSS classes for the template, it will also do so for child classes within our component. E.g.

<style lang="scss" module>
.component {

  ul {
    li.selected {
      background: $light-component-border;
     }
  }
}
</style>

Would produce classes such as 

```css
.sidebar_component_1vx3Z ul li.selected_2dxwF

Where the second hash is not required; the encapsulating hash is sufficient to namespace the class to the component. To exclude classes, add <code>:global</code> before the style declaration;

    :global li.selected { 

Which will now produce

.sidebar_component_1vx3Z ul li.selected

<h3>Using :global on the root component</h3>

Typically, using a namespaced root component is enough for our component; we don't need other classes to be hashed. To do this, so that we don't have to declare all child classes as <code>:global</code>, we can do the following;

.component :global {

### Module Classes & Dev Mode ###
Dynamically generated classnames can be awkward to debug in dev mode. Use this in <code>webpack.config.js</code> to

    .configureCssLoader((config) => {
        if (!Encore.isProduction() && config.modules) {
            config.modules.localIdentName = '[name]_[local]_[hash:base64:5]';
        }
    })
```

The [name] key is the filename of the component, E.g. products.vue would be 'products'. The [local] key is the classname, so 'sidebar' in this instance. Then we add a base64 hash with a length of ':5'.

This allows us to see what file a style exists in in dev mode.

Aliases

Aliases allow us to set base directories to use when importing files, this then negates the use of relative directory links. Adding the following to our webpack.config.js

.addAliases({
        '@': path.resolve(__dirname, 'assets', 'js'),
        styles: path.resolve(__dirname, 'assets', 'scss'),
    })

will allow us to use @ when importing javascript

import LegendComponent from '@/components/legend';

And ‘styles’ when importing styles into a javascript file;

import 'styles/app.scss';

However tilde must be prepended to the styles key when importing into a style tag;

@import "~styles/components/light-component";

Looping in Templates

Use the v-for attribute on the attribute which is to be looped over. This can be used for arrays and objects.

<li v-for="category in categories" class="nav-item">
    <a class="nav-link" :href="category.link">{{ category.name }}</a>
</li>

When using dynamic attributes like the href, remember to prepend with a colon to execute it as a javascript statement. The above template would render the following data;

    data() {
        return {
            categories: [{
                name: 'Horse Pringers',
                link: '#'
            }, {
                name: 'Amiga Hampers',
                link: '#'
            }]
        };
    },

This should however always be accompanied by a key attribute so that Vue can retain the element’s context and re-render it when required. VSCode will show an error when using the Vue extensions.

The v-for vue directive attribute should also be changed to include the key as the second parameter in the loop. The key attribute should also be prepended with a colon.

<li v-for="(category, index) in categories" :key="index" class="nav-item">

The key attribute does not render in the source code of the page as it’s a special attribute, which can be found here

Rendering an Element’s Contents – v-text

This can either be achieved using the curly brace syntax or using the v-text attribute, which is another vue directive. This can also contain standard javascript such as ternary conditionals;

<button class="btn btn-secondary btn-sm" 
   v-text="collapsed ? '>>' : '<< Collapse'">
</button>

Escaping

Vue automatically escapes HTML characters in strings when rendering in templates.

Events – v-on

Adding events to elements using the v-on directive. The shortcut for this is the @ symbol. Vue automatically prepends methods v-on on events with this., so the following actually becomes this.toggleCollapsed

<button class="btn btn-secondary btn-sm" 
   v-on:click="toggleCollapsed">
</button>

@ shortcut

<button class="btn btn-secondary btn-sm" 
      @click="toggleCollapsed">
</button>

append the regular javascript event after the colon. Target a method defined in the “`methods“ object in the component definition;

export default {
    name: 'Sidebar',
    data() {
        return {
            collapsed: false,
        };
    },
    methods : {
        toggleCollapsed() {
            this.collapsed = !this.collapsed;
        }
    }
}

Component Structure

Component properties which start with dollar are created internally by Vue, and can be manipulated if required for more advanced operations. Methods starting with underscore should be treated as private and not used.

These can however be useful when debugging and can be output in the component. E.g. every instantiated component is assigned a _uid which can be output in the template using

{{ _uid }}

It would appear that this was adopted to avoid conflicts with user created methods.

## Component instantiation – created() ##
When a component instantiates, its created method is automatically called if defined.

Conditionals – v-if and v-show Directives

A v-if on an element will completely remove it from the dom when the conditional doesn’t pass.

<div v-if="!collapsed">

A v-show conditional hides the element with display:none

<div v-show="!collapsed">

Showing should be used for simple elements which hide and appear frequently, and v-if should be used for more complex elements which reduces Vue’s rendering time when removed completely.

‘component’ root Style convention

It’s considered convention to call the root of a component’s style ‘component’. When configureCssLoader is used in development, the class name will be prepended with the name of the template file it exists in. Class names are only more cryptic in production.

Computed Properties

Computed properties allow us to take logic out of our template and into our component javascript. They don’t accept parameters, and behave just like a property on the component.

Instead of doing inline logic to switch classes for example;

  <div
    :class="{
      [$style.component]: true,
      [$style.collapsed]: collapsed,
      'p-3': true,
      'mb-5': true,
    }"
  >

We can instead do the following. Computed methods are called as though they are properties in templates javascript files.

<div :class="componentClass">

And then define our computed method in the component’s computed key;

  computed: {
    componentClass() {
      const classes = [this.$style.component, "p-3", "mb-5"];

      if(this.collapsed) {
        classes.push(this.$style.collapsed);
      }
 
      return classes;
    },

Note that classes are accessed through the $style object. This ensures we use the generated classes with transformations.

Computed & Ordering

For convention, the ‘computed’ key should be before the ‘methods’ key in a component definition.

### Performance ###
Computed methods will only run if a property referenced in the method changes on the object. So, in the above example, if we had a button which called

methods: {
    toggleCollapsed() {
      this.collapsed = !this.collapsed;
    },
},

then componentClass would update the class on the element. If we were to remove the line which modifies the collapsed property, the componentClass function would not execute!

Shared CSS

Normal shared styles should be imported and used with components where appropriate, and used as regular strings rather than using the $style object.

This will work for global styles, but for shared styles which are imported into a component, by default they may be duplicated with the module’s prefix.

To get around this, we can add a second style tag, with a lang of “css”. SCSS can be imported into this style tag, but using “scss” as the lang attribute “for some reason” duplicates the styles.

<style lang="css>
@import '~path/to/style.scss'
</style>

AJAX With Axios

yarn add axios --dev

This is an alternative to the browser's built in 'fetch' method, however this is not supported in IE 11.

Hook Methods

We can use the methods mounted and created to perform actions when we use a component. Mounted when it’s actually added to the page.

To call an ajax endpoint when a component mounts;

  mounted() {
    axios.get('/api/products').then((response) => {
        // Do things.
    });
  }

Use of created

The created hook can often be the better hook for performing ajax requests as it will start as soon as the component is created rather than added to the page.

  created() {
    axios.get('/api/products').then((response) => {
        // Do things.
    });
  }

await & async

This waits for an AJAX request to complete before continuing code execution. It’s also legal to use await on function which do not return promises, they’re just executed and returned instantly. To use this, the encapsulated method must have the async keyword before it;

async mounted() {
    const response = await axios.get('/api/products');
  }

When an async method is defined, it will always return a Promise. Only the code in the async method will pause whilst an await call is made, the rest of the application code will continue execution. The returned promise can be used to chain other operations which may require the data returned in the async method.

export default {
  name: "Catalog", 
  data: () => ({
    products: []
  }),
  async mounted() {
    const response = await axios.get('/api/products');
    this.products = response.data['hydra:member'];
  }
};

## API Platform ##
When using <a href="https://api-platform.com/">API Platform</a>, accessing the API in a browser will give the GUI. It does this by checking the ```accept``` header. To access endpoints which AJAX calls directly, add ```.jsonld``` to the end of the url.

### @id and API Platform ###
Instead of the traditional use of primary key database ids when assigning keys to objects (mainly in loops), the ```@id``` attribute can also be used, which contains the unique URI for the item in question. This can be more useful than just using the ```id``` primary key. This is known as the IRI.

## Smart Component, Dumb Component ##
The logical hierarchy of components should be defined by their usage. Creating a product list component could load the products and render them, however defining its data as a prop which is set by a "smart" component can be preferable.

A smart component can be considered like a Controller, and dumb components akin to templates.

### catalog.vue ###

<template>
<div>
<div class="row">
<div class="col-12">
<h1>Products</h1>
</div>
</div>

&lt;product-list :products=&quot;products&quot; /&gt;

</div>
</template>

<script>
import axios from 'axios';
import ProductList from '@/components/product-list';

export default {
name: "Catalog",
components: {
ProductList
},
data: () => ({
products: [],
}),
async mounted() {
const response = await axios.get('/api/products');
this.products = response.data['hydra:member'];
}
};
</script>

<br />### product-list/index.vue ###
<template>
  <div class="row">
    <div
      v-for="product in products"
      :key="product['id']"
      class="col-xs-12 col-6 mb-2 pb-2"
    >
      {{ product.name }}
    </div>
  </div>
</template>

<script>
export default {
    name: 'ProductList',
    props: {
        products: {
            type: Array,
            required: trueprodu
        }
    },
    data: {
        products: []
    }
};
</script>

In this example, the catalog component is responsible for providing the data for the product-list component. The import for the product list does not need to include the ‘index’ part of the file path. This is assumed by Vue when we import a directory.

Passing Server Data to Vue

Access the window object in computed return methods to initialise the Vue app with data on page load. Whilst this is legal, it often gets messy so it’s better to use something like JS Services to centralise the data.

Initializing Vuw with data on the page is a preferred approach to stop the initial load ‘pop’ when ajaxing in data.

Services

It’s often better to split functionality up into smaller pieces, rather than have it all lumped together in the component’s template file. We can create smaller service methods to achieve this, so that things like AJAX requests are separated from our component’s logic.

Vue Lifecycle

Taken from here

Categories
Magento 2

Category Image Paths in Magento 2

There appears to be either a bug or an oddity in the way Magento 2 stores images against categories. Confusingly, there are three types of image paths which may be returned from a category.

1. Just the Image Name

This does not include path information and typically will be from attributes added in third party modules. The full base path and image path needs to be prepended to get the full path of the image.

2. The image path from ‘/media’

This includes the relative path from the media folder and does not include ‘pub’

3. The image path from ‘/pub’

This includes the full relative path from the root directory.

Magento’s core code checks for whether its path starts with a slash to determine what path modifications need to be made to get the full image path. NICE.

$isRelative = substr($imageName, 0, 1) === '/';

Helper to account for Magento 2’s behaviour

use Magento\Framework\App\Filesystem\DirectoryList;

class CategoryImageHelper extends \Magento\Framework\App\Helper\AbstractHelper {
    // Magento seemingly don't store this as a const anywhere so everyone usually just throws this
    // string around willy-nilly. I'm sure that's not creating problems down the road!
    const CATEGORY_DIR = 'catalog/category/';
    
    protected $_filesystem;
    protected $_storeManager;

    public function __construct(
        \Magento\Framework\App\Helper\Context $context,
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Magento\Framework\Filesystem $filesystem
    ) {
        parent::__construct($context);
        $this->_filesystem = $filesystem;
        $this->_storeManager = $storeManager;
    }


    /**
     * 
     * @param string $imageName
     * @return string|bool
     */
     protected function getImagePath($imageName) 
     {
        $realPath = '';
        // Yeah. this is how Magento do it in Catalog/Model/Category.
        $isRelative = substr($imageName, 0, 1) === '/';
        
        if($isRelative) {
            // Magento might include pub in its image name. YEAH. *MIGHT*.
            $hasPub = strpos($imageName, '/' . DirectoryList::PUB . '/') === 0;
            $realPath = $this->_filesystem->getDirectoryRead($hasPub ? DirectoryList::ROOT : DirectoryList::PUB)->getAbsolutePath($imageName);
        } else {
            $realPath = $this->_filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath(self::CATEGORY_DIR . $imageName);
        }
            
        if (!$this->_filesystem->isFile($realPath) || !$this->_filesystem->isExist($realPath)) {
            return false;
        }

        return $realPath;
    }
}

Categories
Magento 2

Fixing Quote Item Extension Attributes (Magento 2.3.5)

Do you know that feeling as a developer when you do things the Right Way(tm) according to a framework’s best practices only to get tripped up by a half-baked implementation? Welcome to Magento 2’s extension attributes!

Magento 2.3.5 doesn’t appear to pass cart item extension attributes to the javascript checkoutConfig object on the cart and checkout pages. This issue appears to be similar to this bug which stopped extension attributes from permeating to the checkoutConfig.quoteData object. Unfortunately, the bug has only been fixed for quote objects, not quote items.

The Fix

Add a plugin to the Checkout module’s DefaultConfigProvider in di.xml;

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Checkout\Model\DefaultConfigProvider">
        <plugin name="vendor_module_checkout_model_defaultconfigprovider" type="Vendor\Module\Plugin\Checkout\Model\DefaultConfigProvider" />
    </type>
</config>

Now we can manipulate the data in the plugin to make it do the thing it was intended to do when Magento 2 launched in 2015. Snark.

<?php
namespace Vendor\Namespace\Plugin\Checkout\Model;

class DefaultConfigProvider
{ 
    /** @var \Vendor\Namespace\Helper\Quote */
    protected $quoteHelper;

    /**
     *
     * @param \Magento\Checkout\Model\DefaultConfigProvider $subject
     * @param array $result
     * @return void
     */
    public function afterGetConfig(
        \Magento\Checkout\Model\DefaultConfigProvider $subject,
        $result
    ) {
        if(isset($result['quoteItemData'])) {
            foreach($result['quoteItemData'] as $itemKey => $quoteItem) {
                if(isset($quoteItem['extension_attributes']) && !is_array($quoteItem['extension_attributes'])) {
                    $extensionAttributes = $quoteItem['extension_attributes'];
                    $data = [];
                    foreach ($extensionAttributes->__toArray() as $key => $value) {
                        if (!is_object($value)) {
                            $data[$key] = $value;
                        }
                    }
                    $result['quoteItemData'][$itemKey]['extension_attributes'] = $data;
                }
            }
        }
        return $result;
    }
}

Next half baked implementation bug, please!

Categories
Magento 2

Magento 2: Disable All Modules By Vendor (Regex)

Use this find/replace regex in config.php to set all modules by a particular vendor to disabled.

Find Tab

(Amasty_[a-z0-9_]+'\s?=>\s?)(1)

Replace Tab

$10
Categories
Magento 2

Stop Magento 2 from Redirecting on Ajax Add to Cart

Useful for debugging the ajax request. Comment out the location.reload() in the ajaxSubmit function in module-catalog/view/frontend/web/js/catalog-add-to-cart.js

/** @inheritdoc */
complete: function (res) {
    if (res.state() === 'rejected') {
        // location.reload();
    }
}
Categories
Magento 2

Multiselects as Type Text; Magento 2.3.2 indexing woes

Creating a multiselect in Magento when an attribute has many, many values gets truncated when it’s indexed into the catalog_product_flat table. This is because the catalog_product_flat table’s column is made specifically as a type varchar as length 255 for multiselects.

A fix is to create a plugin for the \Magento\Eav\Model\Entity\Attribute\Source\Table class;

di.xml:

<type name="Magento\Eav\Model\Entity\Attribute\Source\Table">
    <plugin name="module_eav_entity_attribute_source_table" type="Namespace\Module\Plugin\Eav\Model\Entity\Attribute\Source\Table" sortOrder="10"/>
</type>

And the PHP class…

namespace Namespace\Module\Plugin\Eav\Model\Entity\Attribute\Source;

class Table
{
    /**
     * For some reason, Magento indexes text types which are multiselects and type text as varchar 255.
     * This keeps the creation of text index columns so they stay as text type
     *
     * @param \Magento\Eav\Model\Entity\Attribute\Source\Table $subject
     * @param array $result
     * @return array
     */
    public function afterGetFlatColumns(
        $subject,
        $result
    ) {
        $attributeCode = $subject->getAttribute()->getAttributeCode();
        $isMulti = $subject->getAttribute()->getFrontend()->getInputType() == 'multiselect';

        if($isMulti && $subject->getAttribute()->getBackendType() == \Magento\Framework\DB\Ddl\Table::TYPE_TEXT) {
            $result[$attributeCode]['length'] = null;
        }

        return $result;
    }
}

This doesn’t appear to be intended, maybe Magento don’t recommend creating multiselects as type text anymore, but that’s no good for all use cases. Ho-hum.

Categories
Magento 2

Magento 2’s Customer Attribute (And Address) Bizarreness

Adding customer attributes to be editable only for admin accounts, you would think would be a straightforward process. Well let me tell you that Magento 2 has other ideas!

Firstly, adding to forms. You’ll notice that just adding attributes to either adminhtml_customer, or adminhtml_customer_address does nothing in the admin area. For some ungodly reason, to get them to show, attributes must also be added to the customer_account_edit and customer_address_edit forms.

Great! attributes now appear in the admin area, but woe unto thee that thinks we’re done here!

Saving from the Admin Area

Hahaha, expecting them to save… just like that? Fool. Make sure you’ve also set the system key of your attribute to false, otherwise Magento will just ignore them.

They’ll need adding to an attribute set and group for this to work correctly; typically this is just the default of each for the entity type.

So now we should be done… right? RIGHT?! Well, young urchin, try updating a customer account information or address from the frontend of your store. What’s that? Hahaha, yes, Magento just obliterated your custom customer attribute values. Serves you right for being so optimistic.

Now, you’ll notice, that even though we’ve added the attributes to the customer_account_edit and customer_address_edit forms to get them to show in the admin area, which foolishly in my mind should have no effect there, that do not show in the frontend.

No, if you want them to appear for customers, you’ll have to do more than add the attribute to a form (guffaws). But, for this example, we don’t want them to show in the frontend and certainly don’t want the data to be sent into the flaming abyss by a customer’s saving of them.

The fix, in true Magento style, is to set the visible key to false when creating the attribute, which persists to the customer_eav_attribute‘s is_visible column. Yes… visible prevents Magento from trying to save the data from the frontend. Cue manic laughter from the Magento Devs as they revel in our pain.

Recap

customer_account_edit and customer_address_edit forms – shows the field in the admin area.
is_system – Set to false for Magento to save the value from the admin area.
Add to attribute set and group – Also required to save from the admin area.
visible – Prevents Magento from trying to save the values from the frontend’s customer account section.

I’m not 100% sure what the adminhtml_customer and adminhtml_customer_address forms are actually used for anymore, but I’ll keep them for a laugh, just in case the Magento devs change their minds.

Note: adminhtml_customer now appears to be in use again now, and does have a part to play in the saving of data from the admin area. Keep those hurdles coming!

Also of note; the following installer adds the attribute to the correct group and set. If attributes are still not saving, this may have not been set in eav_entity_attribute table, and make sure the group and set exist in that table.

A code snippet:

protected function createCustomerAttributes(
    \Magento\Eav\Setup\EavSetup $eavSetup,
    ModuleDataSetupInterface $setup
) {
    $attributes = [];

    $customerSetup = $this->customerSetupFactory->create(['setup' => $setup]);
    $customerEntity = $customerSetup->getEavConfig()->getEntityType(\Magento\Customer\Model\Customer::ENTITY);
    $attributeSetId = $customerEntity->getDefaultAttributeSetId();
    $attributeSet = $this->attributeSetFactory->create();
    $attributeGroupId = $attributeSet->getDefaultGroupId($attributeSetId);

    // ... Add other attributes here 

    $attributes['attribute_code'] = [
        'type'     => 'varchar',
        'label'    => 'Attribute label',
        'input'    => 'text',
        'visible'  => false,
        'system' => false,
        'required' => false,
        'user_defined' => true,
        'position' => 290
    ];

    foreach ($attributes as $code => $attribute) {

        $eavSetup->addAttribute(
            \Magento\Customer\Model\Customer::ENTITY,
            $code,
            $attribute
        );

        $createdAttribute = $this->eavConfig->getAttribute(
            \Magento\Customer\Model\Customer::ENTITY,
            $code
        );

        $createdAttribute->setData('used_in_forms', ['customer_account_edit', 'adminhtml_customer'])
            ->setData('attribute_set_id', $attributeSetId)
            ->setData('attribute_group_id', $attributeGroupId)
            ->save();
    }
}

Come on Magento 2, we need more hoops to jump through than that! It’s like it’s not even trying to be frustrating some days.

Categories
Magento 2

Create Attributes In Magento 2

Creating attributes in Magento 2 is very similar to Magento 1. Magento 2, for the most part is smart enough to ignore the creation of attributes which already exist, and will instead update them. This is all well and good, unless your attributes have options in them. In which case, you will need to check whether the attribute exists already or Magento will duplicate option values on subsequent runs of your installer or updater.

The fix for this is to check whether the attribute already exists before attempting to create it, however this causes another issue. Magento’s EAVConfig class caches attributes after a the getAttribute method is called – so this cache needs cleaning if we need to set other data on the attribute after creation, such as the forms it needs to exist in.

The following is an example to create a customer address attribute, taken from an installer class where $this->eavConfig is an instance of \Magento\Eav\Model\Config:

protected function createCustomerAddressAttributes(
    \Magento\Eav\Setup\EavSetup $eavSetup,
    ModuleDataSetupInterface $setup
) {

    $customerAddressSetup = $this->customerSetupFactory->create(['setup' => $setup]);
    $customerAddressEntity = $customerAddressSetup->getEavConfig()->getEntityType(
        \Magento\Customer\Api\AddressMetadataInterface::ENTITY_TYPE_ADDRESS
    );
    $attributeSetId = $customerAddressEntity->getDefaultAttributeSetId();
    $attributeSet = $this->attributeSetFactory->create();
    $attributeGroupId = $attributeSet->getDefaultGroupId($attributeSetId);

    $attributes = [];

    $attributes['attribute_code'] = [
        'type'     => 'text',
        'label'    => 'Attribute Label',
        'input'    => 'textarea',
        'visible'  => true,
        'required' => false,
        'user_defined' => true,
        'position' => 220
    ];

    foreach ($attributes as $code => $attribute) {
        $this->eavConfig->clear();
        // Don't create the attribute if it already exists
        $attributeCheck = $this->eavConfig->getAttribute(
            \Magento\Customer\Api\AddressMetadataInterface::ENTITY_TYPE_ADDRESS,
            $code
        );
        
        if ($attributeCheck->getAttributeId()) {
            continue;
        }

        // Stop Magento from loading the cached attribute with no Id.
        $this->eavConfig->clear();

        $attribute['system'] = 0;

        $eavSetup->addAttribute(
            \Magento\Customer\Api\AddressMetadataInterface::ENTITY_TYPE_ADDRESS,
            $code,
            $attribute
        );

        $createdAttribute = $this->eavConfig->getAttribute(
            \Magento\Customer\Api\AddressMetadataInterface::ENTITY_TYPE_ADDRESS,
            $code
        );

        $createdAttribute->setData('used_in_forms', ['adminhtml_customer_address', 'customer_address_edit'])
            ->setData('attribute_set_id', $attributeSetId)
            ->setData('attribute_group_id', $attributeGroupId)
            ->save();
    }

    return $this;        
}

If we didn’t have the $this->eavConfig->clear(); clear in there, the installer would use the cached version of the attribute when performing our initial $attributeCheck, and the final save would try and create the attribute again.

Categories
Magento 2

Fixing slow reindexes in Magento 2 & MariaDb

We recently noticed some discrepancies when indexing catalog_category_product on Mariadb when compared to Mysql 5.7. The indexer would take around 13 minutes on MariaDb, compared to around 3 seconds on MySql. To add more confusion to the mix, MariaDb was running on a powerful staging server, and MySQL running on a lowly MacBook Pro development machine.

After much head-scratching and career path questioning, the issue seemed to be related to the following statement in Magento\Catalog\Model\Indexer\Category\Product\Action\Full.php

$this->connection->query(
    $this->connection->insertFromSelect(
        $resultSelect,
        $this->tableMaintainer->getMainTmpTable((int)$store->getId()),
        $columns,
        AdapterInterface::INSERT_ON_DUPLICATE
    )
);

This is executed in the reindexCategoriesBySelect method, which creates a temporary table to work with when regenerating the index for a particular store. It turns out that MariaDb’s temporary table usage woefully bad when large amounts of data are being inserted. This appears to be related when aria_used_for_temp_tables is set to ON, a value which can only be changed by recompiling MariaDb. See here and here.

The fix, without switching database engines, or recompiling MariaDb, is to adjust Magento’s batchRowsCount to a lower number so that the database isn’t dealing with as many temporary table inserts at a time. Magento provide config settings for this value for all of their indexers which use temporary tables, so adjust this value for whichever indexer is giving slow performance. For our case, changing the default value from 100000 (the default) to 500 brought the indexer time down from 13 minutes to 6 seconds. The following, added to di.xml was the panacea for our case

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Catalog\Model\Indexer\Category\Product\Action\Full">
        <arguments>
            <argument name="batchRowsCount" xsi:type="number">500</argument>
        </arguments>
    </type>
</config>

Magento’s documentation on use of this value can be found here, which states

“You can experiment to determine the ideal batch size. In general, halving the batch size can decrease the indexer execution time”.

Yup, experiment indeed with this value, YMMV! Happy indexing!

Categories
Magento 2

Magento 2’s uiRegistry Debugging

On the frontend of Magento 2 components are constructed hierarchically. Their names are derived from a concatenation of their parent names. Looking at the checkout_index_index.xml, we can see that the checkout component contains a horrendous amount of config.

To make debugging of this gargantuan hellspawned rat’s nest easier, we can use the uiRegistry object in our browser’s console.

If we would like to get a particular object and we know the full concatenated name of the item, we can simply use something akin to the following;

requirejs('uiRegistry').get("checkout.steps.shipping-step.shippingAddress");

If however, we would like to get a uiComponent using a property name, we can instead use the get method as a query. In the example above, if we only knew the script location of the uiComponent in question, we could instead perform

requirejs('uiRegistry').get("component = Magento_Checkout/js/view/shipping");

We can also pass in a callback method as the second parameter, where the item(s) returned are passed in as parameters to that function.

Get a uiComponent By XML Name

The un-concatenated index property of a uiComponent contains the name attribute on item XML node

requirejs('uiRegistry').get('index=<XML_NAME_ATTRIBUTE>');

Getting all registered uiComponents

The get method also allows us to pass in a callback function as the first parameter, instead of a query. This will pass all items sequentially through our callback, allowing us to see exactly what is registered;

requirejs('uiRegistry').get(function(item){
    console.log(item.name, item);
});

Do this on the checkout and prepare to have an exorcist level of data vomited into your poor, unsuspecting console.

Getting a uiComponent’s children

This property is an observable knockout object, so to get the actual array, execute it as a function.

componentInstance.elems();

Traversing a uiComponent’s Children

This will output the full concatenated name of all children of the uiComponent instance

_.each(uiComponentInstance.elems(), function(child) {
    console.log(child.name);
});

Using the uiRegistry to search for uiComponents

The get method of the uiRegistry allows the searching for properties. E.g. on the checkout, the sidebar item has a sortOrder of 50. The get command always returns the first element encountered, regardless of how many match.

var component = registry.get('sortOrder = 50');

Related Magento DevDoc

Using MageSpecialist’s Debug Module

MageSpecialist’s module allows the discovery of uiComponents through their Magento browser plugin and Magento Module. This allows two way debugging of uiComponents. They can be selected and shown on the page using the extension’s console.

MageSpecialist’s module adds the attribute data-mspdevtools-ui to the root node of the uiComponent. Using the hash value of this attribute, we can use MSP’s console to search for the hash, revealing the uiComponent details.

Structure of uiComponents

Imports

Imports allow data to be imported at the time the object is created from another uiComponent. E.g.

defaults: {
    imports: {
        foo: 'customer_listing.customer_listing:name'
    }
}

This will assign foo as the value of the variable in the customer_listing name property.

Listens

The listens object on a uiComponent can be set up to fire when imports are received. E.g. using the ‘Imports’ ‘foo’ example above;

listens: {
    foo: 'functionToCall'
}

Regions

Regions allow a group of uiComponents to be rendered in one area. E.g. on the checkout page, retrieving the summary component, we can get items for a region.

requirejs('uiRegistry').get('checkout.sidebar.summary.cart_items.details').getRegion('after_details')();

Will output child items subscribed to a region; the regions array is an observable hence the function call after retreival. Region is stored in the displayArea property of child items.

uiComponent.get('displayArea');

Disabling uiComponents with XML

Use the item’s config array and set item componentDisabled to true. Follow the same XML Path of the module structure to be disabled and it will be merged.

<item name="fee" xsi:type="array">
    <item name="config" xsi:type="array">
        <item name="componentDisabled" xsi:type="boolean">true</item>
    </item>
</item>

Attaching a Component

In the component’s argument data/config/component node to set the component JS

<uicomponent_name>
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="component xsi:type="string">VENDOR_MODULE/js/js-file-path</item>
        </item>
    </argument>
</uicomponent_name>

The file itself is translated to Vendor/ModuleName/view/<area>/web/js/js-file-path.js

The actual name of the component in the uiRegistry is namespaced against of the component, essentially duplicated.

This example would be uicomponent_name.uicomponent_name

Binding using scope

Note: This is done with XHTML in adminhtml.

<div data-bind="scope:uicomponent_name.uicomponent_name">
    <!-- ko template:getTemplate() --><!-- /ko>
</div>

This will use the view model’s js for every child node of the div

getTemplate() is defined in the uiElement base class which returns this.template