Categories
Webpack

Symfony Encore

Install encore via composer, which will create a package.json file with the node dependencies for webpack. Then run npm install which will acquire those packages;

composer require encore
npm i

Refreshing the site now will give an error about missing manifest files. We need to run the encore dev command to generate these and compile our assets. Commands have been added to package.json. Run npm run dev to build our assets.

    "scripts": {
        "dev-server": "encore dev-server",
        "dev": "encore dev",
        "watch": "encore dev --watch",
        "build": "encore production --progress"
    }

Including generated assets

In the html base twig template, use these in the stylesheets and javascripts block

{% block stylesheets %}
    {{ encore_entry_link_tags('app') }}
{% endblock %}


{% block javascripts %}
    {{ encore_entry_script_tags('app') }}
{% endblock %}

This will take the generated entry points in public/build/entrypoints.json and include the files in the template.

Importing third party libraries

Install a third party library

npm i jquery --dev

Because this is a third party library, we don’t need path directories. The main export of a library exists in the library’s “package.json“` in its node_modules directory in the ‘main’ key.

Variables do not permeate across files and are not global. Using the following in our javascript file only makes dollar available within that file.

import $ from 'jquery';

Mainifest.json & Twig Images

Mainifest.json is created when webpack runs. It contains a map of key values of all source and build files. When referencing images in twig templates, use the key in manifest.json to correctly use the image. This will include the build path.

Use

    .copyFiles({
        from: './assets/images',
        to: 'images/[path][name].[hash:8].[ext]'
    });

in webpack.config.js to populate manifest.json with the copied image files.

Fixing jQuery Plugins

use .autoProvidejQuery() which can fix jQuery plugnis which don’t play nice with imports.

Configuring for Production

Encore is set up to do this automatically with hashing of filenames when in production;

 .enableVersioning(Encore.isProduction())

Make sure the webserver is set up to serve no-expire headers for a long time in the future for js, and css, and image files.

Demo Files

Categories
Uncategorised

ElasticSearch

Use Kibana For Elastic Search GUI

Elastic search will need to be installed via the download on the webpage and started manually on Mac, rather than installing via homebrew as an x-pack error will occur when running Kibana.

Download Elasticsearch, and execute bin/elasticsearch to start.
Download Kibana, and execute bin/kibana-plugin

Navigate to localhost:5601 to access the GUI

Troubleshooting (so early? REALLY?! Yes.)

Summary of Elasticsearch

  • Full-text search engine
  • NoSQL database
  • Analytics engine
  • Written in Java
  • Lucine Based (~ Solr)
  • Inverted infices
  • Easy to scale (~Elastic)
  • RESTFul interfece (HTTP/JSON)
  • Schemaless (kinda)
  • Real-time
  • ELK Stack

HTTP Codes

Elasticsearch uses HTTP codes correctly. So a 201 will be returned when a document is created using calls such as

POST /blog

Returns
{"acknowledged": true}

Schemaless (Kinda) – Mappings

Elasticsearch is mostly schemaless, in that it will guess the datatype of fields. Sometimes, it gets this wrong. To tell Elasticsearch the correct type for a data key, use mappings.

Mappings are explicitly set at index creation time

E.g.

[json]
{
“mappings” : {
“post : {
“properties : {
title: {
“type” : “string”
},
“date” : {
“type” : “date”,
“format” : “E, dd MMM YYYY HH:mm:ss Z”
},
“guid” : {
“type” : “integer”
}
}
}
}
}
[/json]
[json]

Analzers

{
mappings : {

{
“title” : {
“type” : “string”,
“fields” {
“en” : {
“type” : “string”
“analyzer”
}
}
}
}

}
}
[/json]

Show Indexes

https://www.elastic.co/guide/en/elasticsearch/reference/6.8/cat-indices.html

curl -X GET 'localhost:9200/_cat/indices?pretty=true'

Create Index

curl -X PUT "localhost:9200/test_index?pretty"

Delete All Indexes

curl -X DELETE localhost:9200/_all
https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-delete-index.html

Delete Index

curl -X DELETE localhost:9200/index_name

API Conventions

Most APIs suppost execution across multiple indices. Different notations are used to perform operations across multiple indexes. E.g. comma separated, wildcard notation, _all keyword,

Date & Time

Elasticsearch allows you to search indices according to date and time. You need to specify date and time in a specific format like

<static_name{date_math_expr{date_format|time_zone}}>

  • static_name is the staric text part of the name
  • date_math_expr computes the date dynamically
  • date_format is an optional date format
  • time_zone is an optional time zone

Common Options For all REST APIs

  • Pretty Result
  • Human Readable Output
  • Date Math
  • Response Filtering
  • Flat Settings
  • Parameter
  • No Values
  • Time Units
  • Byte Size Units
  • Unit-less Quantities
  • Distance Units
  • Fuzziness
  • Enabling Stack Traces
  • Request Body In Query String

URL Access Control

Users can also use a proxy with URL-based access control to the Elastic Search indices.

User has an option of specitying an index in teh URL and on each individual request within the request body for some requests like:

  • multi-search
  • multi-get
  • bulk

Elastic Search in Magento

Elastic search replaces the old and doddery MySQL search in Magento for faster and better matching algorithms. And it’s also nice that customers can checkout when another customer uses the store’s search; something hit and miss in the good ol’ MySQL days. The MySQL search indexer (before deprecation) used to be in the Magento\CatalogSearch module, however the elastic search now takes over the saving of the data, but the CatalogSearch module is still responsible for the preparation of the data. The FullText name is a hangover from (MySQL’s FullText index)[https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html], and is a bit of a misnomer now that Elastic Search is used.

The indexer command is defined in the CatalogSearch module’s indexer.xml file as catalogsearch_fulltext, so to regenerate the elastic search index we can run

bin/magento indexer:reindex catalogsearch_fulltext

This runs the executeFull() method of the Model\Indexer\Fulltext class.

It’s the executeByDimensions method which actually kicks things off, and either runs a batched index or performs the ful index by calling the $saveHandler->saveIndex() method. This is where the Elastic Search module steps in – the $saveHandler here is the Magento\Elasticsearch\Model\Indexer\IndexerHandler class, and this is is the lad who actually saves the index to the Elastic Search server.

The data for the save handler is generated by the rebuildStoreIndex of the Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full class’ rebuildStoreIndex method, which uses the Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider‘s getSearchableProducts method.

The product attributes which are to be indexed start off life in the rebuildStoreIndex‘s $dynamicFields variable. This stores attributes by their type, in a rather ironic static array of keys (‘int’, ‘varchar’…). I’m actually surprised there’s not some kind of dynamic fields type provider or something here. Magento loves providers.

So the getSearchableAttributes method is the urchin responsible for getting the product attributes which should actually go into the index. It’s the productAttributeCollectionFactory which is used to create the productAttribute collection, which limits attributes to those which have one of the the following conditions in the catalog_eav_attribute table; is_searchable = 1, is_visible_in_advanced_search = 1, is_filterable > 0, is_filterable_in_search = 1, used_for_sort_by=1,. These conditions are added to the collection using the addToIndexFilter method, which usefully hardcodes the status and visibility to required codes. A plugin could be added to this method to add additional attributes whose settings don’t require any of the searchable flags set.

As an aside, I think it’s worth giving an honourable mention to the misspelled and deprecated catelogsearch_searchable_attributes_load_after event which gets fired here. I’ll bet Amasty rely on this somewhere in one of their thousand modules. The correctly named event is fired straight after this one :D.

A note on Dimensions

I’m not 100% on what dimensions are, but it appears to be a way to create parallel indexes with different parameters. So a different index could exist based on customer group, or website. That’s my best guess at this point ­čÖé

To identify which index relates to which website on an Elastic Search server, a prefix should be used. This is set in Magento’s Store Configuration.

Elastic Search & Failing to Restart

How to prevent systemd service start operation from timing out

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.