Symfony – Bundles & Services

The purpose of bundles is to expose services to our application. It’s a bit like Dependency Injection coupled with PHP autoloading, and primarily allows for the loose coupling of parts of our code by using interfaces. A service is just a class that does some work.

Bundles are installed via composer. They add entries to the config/bundles.php via recipes. This exposes the classes within that bundle as services which can be used elsewhere.

Use the command

bin/console debug:autowiring

Services are configured using the services.yaml file. This typically configures the App namespace to be autowired (with a few exclusions such as the Entity & Test folders). This allows us to inject references to these classes into our other classes’ methods. In Magento, DI allows us to inject into class constructors, but Symfony’s system allows all Controller’s methods to receive autowired classes.

** Controllers are able to receive autowired services in any method, other classes use the DI constructor technique **

To show all registered services, including those in App\, use the --all flag.

bin/console debug:autowiring --all

This can also be used to search for services using

bin/console debug:autowiring searchTerm

When composer installs a bundle, recipes which are added are shown;

This will also change config/bundles.php. Do a git status and see the files changed by the bundle.

return [
    Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
    Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
    Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
    Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
    Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
    Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
    Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
    Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
    Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
    Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
    Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
    Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
    EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true],
    Knp\Bundle\MarkdownBundle\KnpMarkdownBundle::class => ['all' => true],

The last line is added and the modified bundles.php should be committed to the repository.

Each service registered has a unique ID. When running the bin/console debug:autowiring command, the unique ID appears alongside the service class name in blue.

Services are all PHP interfaces which define a set of methods for that utility. Including them in class methods automatically resolves them to a real object.

Displaying functions of Twig

Use the command

bin/console debug:twig

This shows filters, tests etc which can be used with Twig. E.g. the ‘raw’ filter which doesn’t escape HTML.

Bundles add services to other bundles.

Installing the knplabs/knp-markdown-bundle Will add the markdown filter to Twig, allowing for the following expression in Twig templates.

{{ my_data|markdown }}

When running the command bin/console debug:twig, * markdown(parser = null) is now in the filter list.

Cache Service & Configuring Service Behaviour

The cache interface is exposed as the following service;

Covers most simple to advanced caching needs.
 Symfony\Contracts\Cache\CacheInterface (

Use this interface in methods to use this service. The cache interface is automatically instantiated from the Typehint. E.g.

use Symfony\Contracts\Cache\CacheInterface;

class Example {
    public function show(CacheInterface $cache) {

Using the Cache Interface

$textContent = 'text item content'
$cache->get('text_content_cache_'.md5($textContent), function() use ($textContent) {
       // Transform the text in some way to be cached
        return "ITEM TO BE CACHED " . $textContent;

Use the get method to check if a cached item exists. If it doesn’t, the callback method is executed to create the content to be cached.

Cached items are stored on the filesystem in var/cache/dev/pools/ and can be inspected in the Symfony Toolbar’s cache section, which shows cache hits and misses.

There’s also a Symfony\Contracts\Cache\TagAwareCacheInterface ( service which allows finer grained cache control with tagging of items, allowing groups to be set & cleared.

The dd (dump and die) method

Use this to inspect objects, rather than var_dump.

Configuring Bundles

Bundles provide configuration which can be passed to a service on instantiation. To show the available configuration for a service, find the bundle name in bundles.php. Use the bundle name in the following CLI command;

bin/console config:dump KnpMarkdownBundle

To change the service for the parser in the above image, we would create a new config file in config/packages. The name of the file doesn’t matter, but for convention it generally matches the first key of the YAML. In this instance, it would be;

        service: markdown.parser.light

“markdown.parser.light” is the id of a service in the container.

The Service Container

The service container is essentially an associative array of services registered in the application. Each service’s key is its unique id. For a complete list of all services, run

bin/console debug:container

A lot of these services aren’t autowirable. For the shorter list of autowirable services (which can be used with typehints) stick to the shorter list offered by debug:autowiring

To find which class is assigned to a service’s id, use the console command to search for the string;

bin/console debug:container markdown.parser.light

Which shows the class behind the service is Knp\Bundle\MarkdownBundle\Parser\Preset\Light.

This is primarily how the autowiring system works. Bundles add services to the container and typically they use this snake-case naming scheme, which means the services can’t be autowired. Then, to add autowiring support for the most important services, they register an alias from the class or interface to that service.

Showing all configuration

The FrameworkBundle provides the main config for the symfony application. Show all of the config using

bin/console config:dump FrameworkBundle

More useful, we can dump specific config within the FramwworkBundle. To view the default cache services config, we can run

bin/console config:dump FrameworkBundle cache

This will output the config we’re able to change. Even though the cache service is offered by the FrameworkBundle, the config file for this service is in packages/cache.yaml. Generally, config will be in the bundle config file, but it’s fine to create separate config files for big parts of the same bundle.

To view the current config for cache, including our changes in YAML files, use

bin/console debug:config FrameworkBundle cache
Current configuration for "framework.cache"

app: cache.adapter.array
prefix_seed: _%kernel.project_dir%.%kernel.container_class%
system: cache.adapter.system
directory: '%kernel.cache_dir%/pools'
default_redis_provider: 'redis://localhost'
default_memcached_provider: 'memcached://localhost'
default_pdo_provider: database_connection
pools: {  }


  • Environments are sets of configuration
  • There are two environments, dev and prod.
  • Dev shows a big exception page, prod hides errors.

Environment variables defined in .env are different to Symfony environments. To change the Symfony environment using the .env file, change APP_ENV=dev to APP_ENV=prod.

This explains how Symfony initialises via the src/Kernel.php file. This loads the config for bundles, and creates them. This file is part of the local application and is editable (although probably won’t be required).

  • registerBundles loads bundles from bundles.php (this is defined in MicroKernalTrait.php)
  • configureContainer loads all config from the config directory. This shows why config names don’t matter – they’re all globbed from the packages directory.
  • All config files are merged together in one giant array.
  • override specifig config in the environment’s directory. E.g. prod/.
  • config files are now YAML, but routes can be php too. This is defined in the configureRoutes method.
  • routes can also be defined for specific environments. The configureRoutes method loads routes from 'config/{routes}/. $this->environment . '.*.yaml'

A route which is added just to the dev environment is the web_profiler (config/routes/dev/web_profiler.yaml).

We can enable and disable bundles for particular Symfony environments. This can be achieved via the bundles.php file by defining the environment in the bundle’s config array. E.g. by default the DebugBundle is only enabled for test and dev

    Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],

Debugging Routes

Run the command

bin/console debug:router

This will show all defined routes. Several useful routes are configured by default for the dev environment, such as getting phpinfo() with _profiler/phpinfo.


Use to configure services via yaml files. Typically, it’s convention to keep parameters in services.yaml, however any yaml config file can contain parameters. To show available parameters for all services, use the command

php bin/console debug:container --parameters

Parameters can be used as scalar values using the path of the config value, surrounded by percentage signs. To bind parameters to services, the arguments or bind key can be used in services.yaml. Bind is preferred and is apparently more powerful than arguments.

            $isDebug: '%kernel.debug%

To override an existing parameter, match the key to the defined parameter’s path;

        app: cache.adapter.filesystem

Global Config Binding

Global bindings can be defined and then used by any service using the defined key. In services.yaml use the bind key of the _defaults key;

            bool $isDebug: '%kernel.debug%'

This will now be available to any service constructor in App or any controller method. The type is optional, but it must be explicitly typed the same when passed into a method.

Defining a custom bind service

To create a service of a particular type, but with a different actual service class, use the following syntax;

    # default configuration for services in *this* file


            Psr\Log\LoggerInterface $mdLogger: '@monolog.logger.markdown'

Use @ to refer to a named service within a string. Classes can now be autowired with the service Psr\Log\LoggerInterface $mdLogger, which must be passed in exactly as specified with the typehint for default binding to work.

Alternatively to default binding, a service can also be aliased.

Aliasing Services

To create an alias of a service, define it in the services root key of services.yaml

    Psr\Log\LoggerInterface $mdLogger: '@monolog.logger.markdown'

This will create a service with the alias Psr\Log\LoggerInterface $mdLogger which will be available when using that exact type & parameter name.

Named aliases

In the same way that bundle providers provide named aliases, we can also do the same. This allows us to create aliases of another service, E.g.

    php.logger: '@monolog.logger.php'

            $mdLogger: '@php.logger'

\Psr\Log\LoggerInterface $mdLogger will now be the defined PHP logger.


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.


        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;


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

Elastic Search Magento 2

Elastic Search

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

{"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


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


mappings : {

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


Show Indexes

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

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 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)[], 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




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



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 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() {

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" 

@ 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


<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;

Would produce classes such as 

.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 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

        '@': 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="">{{ }}</a>

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'">


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" 

@ shortcut

<button class="btn btn-secondary btn-sm" 

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;

      [$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) {
      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'

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 =['hydra:member'];

## API Platform ##
When using <a href="">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 ###

<div class="row">
<div class="col-12">

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


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

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

<br />### product-list/index.vue ###
  <div class="row">
      v-for="product in products"
      class="col-xs-12 col-6 mb-2 pb-2"
      {{ }}

export default {
    name: 'ProductList',
    props: {
        products: {
            type: Array,
            required: trueprodu
    data: {
        products: []

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.


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

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
    ) {
        $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;

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="" 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" />

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.

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,
    ) {
        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!

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


Replace Tab

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();
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;


<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"/>

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(
    ) {
        $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.

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.


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) {


        $createdAttribute = $this->eavConfig->getAttribute(

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

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.