Magento 2

Layout and Code Compilation for Performance

 

by Ivan Chepurnyi

What? Compilation?

Complex Simple algorithms

What makes them complex?

Repeated data processing


// ... some xml/json/yaml file initialization
foreach ($loadedData as $item) {
    $this->process($item);
}

Nested loops


foreach ($data as $item) {
    $row = [];
    foreach ($columns as $column) {
       $row[] = $column->export($item);
    }
    $csv->write($row);
}

Complex dependency tree


class ClassOne
{
     public function __construct(ClassTwo $dependency) {}
}

class ClassTwo
{
    public function __construct(ClassThree $dependency) {}
}

class ClassThree
{
    public function __construct(ClassFour $dependencyOne, ClassFive $dependencyTwo) {}
}

// ..

How can we solve it?

Repeated data processing

Translate your XML/JSON/YAML file into executable PHP code and include it when you need processed structure

Nested loops

Pre-compile second loop and execute it within the main one

Complex dependency tree

Resolve dependencies and compile resolution into executable code

But compilation looks ugly...

  • You need to create PHP code within PHP code
  • You need to write it to external file
  • You need to include that file inside of your code

I was looking for a library

Didn't find one... So I wrote it myself.

EcomDev\Compiler

  • Created to wrap writing PHP code within PHP
  • Automatically stores compiled code
  • Automatically validates source and re-compiles code when needed
  • Provides easy to use API to create parsers, builders and executors

Installation

  1. Available as a composer dependency
    composer require "ecomdev/compiler"
  2. Instantiate it directly or via DI container.

Some examples

Compile XML into PHP

XML file


<objects>
    <item id="object_one" type="object" />
    <item id="object_two" type="object" />
    <item id="object_three" type="object" />
    <type id="object" class="Some\ClassName"/>
</objects>

Parser


use EcomDev\Compiler\Statement\Builder;

class Parser implements EcomDev\Compiler\ParserInterface
{
    // .. constructor with builder as dependency
    public function parse($value)
    {
        $xml = simplexml_load_string($value);
        $info = $this->readXml($xml);
        return $this->getPhpCode($info, $this->builder);
    }
    // .. other methods
}

Parse xml data


private function readXml($xml)
{
    $info = [];
    foreach ($xml->children() as $node) {
        if ($node->getName() === 'type') {
            $info['types'][(string)$node->id] = (string)$node->class;
        } elseif ($node->getName() === 'object') {
            $info['objects'][(string)$node->id] = (string)$node->type;
        }
    }

    return $info;
}

Create PHP code


private function getPhpCode($info, $builder) {
    $compiledArray = [];

    foreach ($info['objects'] as $objectId => $type) {
        $compiledArray[$objectId] = $builder->instance($info['types'][$type]);
    }

    return $builder->container(
        $builder->returnValue($compiledArray)
    );
}
                

Compiled PHP File


return [
    'object_one' => new Some\ClassName(),
    'object_two' => new Some\ClassName(),
    'object_three' => new Some\ClassName()
];

Nested Loop Simplifying

Your constructor


public function __construct(
    EcomDev\Compiler\Builder $builder,
    EcomDev\Compiler\Compiler $compiler)
{
    $this->builder = $builder;
    $this->compiler = $compiler;
}

Export method


public function export($data, $columns)
{
    $statements = $this->compileColumns($columns, $this->builder);

    $source = new \EcomDev\Compiler\Source\StaticData(
        'your_id', 'your_checksum', $statements
    );

    $reference = $this->compiler->compile($source);
    $closure = $this->compiler->interpret($reference);

    foreach ($data as $item) {
       $row = $closure($item, $columns);
    }
}

Compilation method


public function compileColumns($columns, $builder)
{
    $item = $builder->variable('item');

    $compiledArray = [];
    foreach ($columns as $id => $column) {
        $compiledArray[] = $builder->chainVariable('columns')[$id]
                                ->export($item);
    }

    $closure = $builder->closure(
        [$item, $builder->variable('columns')],
        $builder->container([$builder->returnValue($compiledArray)])
    );

    return $builder->container([$builder->returnValue($closure)]);
}

Result


return function ($item, $columns) {
    return [
        $columns['id1']->export($item),
        $columns['id2']->export($item),
        $columns['id3']->export($item),
        // ...
    ];
};

Main Components

  • CompilerInterface - Compiler instance
  • StorageInterface - Stores compiled files
  • SourceInterface - Provider of data (File, String, StaticData)
  • ParserInterface - Parser of data
  • ObjectBuilderInterface - Bound builder for included files

And some sweet stuff...

Exportable Objects


class SomeClass implements EcomDev\Compiler\ExportableInterface
{
    public function __construct($foo, $bar) { /* */ }

    public function export() {
        return [
            'foo' => $this->foo,
            'bar' => $this->bar
        ];
    }
}
            

Will be automatically compiled into:


new SomeClass('fooValue', 'barValue');
            

Magento 2.0

Layout Compilation is a must

Why? Because of its algorithm

Layout caching

Every handle that is added to the Magento\Framework\View\Result changes the cache key for the whole generated structure.

Layout generation

Scheduled structure is generated from XML object at all times

Solution

1. Make every handle a compiled php code

2. Include compiled handles at loading phase

Good news

I am already working on it

Will be release in April 2016

GitHub

Compiler Library for M2

https://github.com/EcomDev/compiler

Layout Compiler for M1

https://github.com/EcomDev/EcomDev_LayoutCompiler

Layout Compiler for M2

Coming soon

Thank You

Q&A