Modular frond-end blocks - writing your own package. Part 2

In the first part, I shared my view of what reusable front-end blocks can be, received constructive criticism, finalized the package and now I would like to share with you a new version. It will allow you to easily organize the use of modular blocks for any project with a php backend.





For those who are not familiar with the first part, I will leave spoilers from it, which will bring you up to date. For those who are interested in the end result - a demo example and links to repositories at the end of the article .





Foreword

I will introduce myself - I am a young web developer with 5 years of experience. For the last year I have been working as a freelancer and most of the current projects are related to WordPress. Despite various criticisms of CMS in general and WordPress in particular, I think the architecture of WordPress itself is a pretty good solution, although of course not without certain drawbacks. And one of them, in my opinion, is templates. In recent updates, big steps have been taken to fix this, and Gutenberg as a whole is becoming a powerful tool, but unfortunately most themes continue to mess with templates, styles and scripts, which makes editing something extremely painful, and reuse of code is often impossible. It was this problem that pushed me to the idea of ​​my own package, which would organize the structure and allow the reuse of blocks.





The implementation will be in the form of a composer package that can be used in completely different projects, without being tied to WordPress.





Formulation of the problem

The concept of a block below will be essentially the same concept as a block in the BEM methodology , i.e. this will be a group of html / js / css code that will represent one entity.





We will generate html and manage block dependencies through php, which means that our package will be suitable for projects with a php backend. We will also agree on the shore that, without getting into disputes, we will not succumb to the influence of newfangled things, such as css-in-js or bem-json, and we will adhere to the el-classico classical approach, i.e. assume html, css and js are different files.





Now let's state our basic package requirements:









  • ()









twig

, css js , .. .js .css .min.css .min.js ( webpack ). html Twig ( ). - , php , , Twig, , , .. , .









  1. :





    1. (css/js/twig)





    2. , twig .





  2. : Settings ( ), TwigWrapper ( Twig ), BlocksLoader ( , ), Helper ( . )





  3. Renderer - , , , (css, js)





, :





  • php 7.4





  • PSR-4 (PSR-4 , composer, .. autoload/psr4 composer.json )





  • ( Button.php Button.css Button.twig)





() : , .





Block





, twig ( protected ) , (, ). ‘get_class_vars’ ‘ReflectionProperty’ , , (protected/public) . protected .





, , , .





Block.php
<?php

declare(strict_types=1);

namespace LightSource\FrontBlocks;

use Exception;
use ReflectionProperty;

abstract class Block
{

    public const TEMPLATE_KEY_NAMESPACE = '_namespace';
    public const TEMPLATE_KEY_TEMPLATE = '_template';
    public const TEMPLATE_KEY_IS_LOADED = '_isLoaded';
    public const RESOURCE_KEY_NAMESPACE = 'namespace';
    public const RESOURCE_KEY_FOLDER = 'folder';
    public const RESOURCE_KEY_RELATIVE_RESOURCE_PATH = 'relativeResourcePath';
    public const RESOURCE_KEY_RELATIVE_BLOCK_PATH = 'relativeBlockPath';
    public const RESOURCE_KEY_RESOURCE_NAME = 'resourceName';

    private array $fieldsInfo;
    private bool $isLoaded;

    public function __construct()
    {
        $this->fieldsInfo = [];
        $this->isLoaded   = false;

        $this->readFieldsInfo();
        $this->autoInitFields();
    }

    public static function onLoad()
    {
    }

    public static function getResourceInfo(Settings $settings, string $blockClass = ''): ?array
    {
        // using static for child support
        $blockClass = ! $blockClass ?
            static::class :
            $blockClass;

        // e.g. $blockClass = Namespace/Example/Theme/Main/ExampleThemeMain
        $resourceInfo = [
            self::RESOURCE_KEY_NAMESPACE              => '',
            self::RESOURCE_KEY_FOLDER                 => '',
            self::RESOURCE_KEY_RELATIVE_RESOURCE_PATH => '',// e.g. Example/Theme/Main/ExampleThemeMain
            self::RESOURCE_KEY_RELATIVE_BLOCK_PATH    => '',// e.g. Example/Theme/Main
            self::RESOURCE_KEY_RESOURCE_NAME          => '',// e.g. ExampleThemeMain
        ];

        $blockFolderInfo = $settings->getBlockFolderInfoByBlockClass($blockClass);

        if (! $blockFolderInfo) {
            $settings->callErrorCallback(
                [
                    'error'      => 'Block has the non registered namespace',
                    'blockClass' => $blockClass,
                ]
            );

            return null;
        }

        $resourceInfo[self::RESOURCE_KEY_NAMESPACE] = $blockFolderInfo['namespace'];
        $resourceInfo[self::RESOURCE_KEY_FOLDER]    = $blockFolderInfo['folder'];

        //  e.g. Example/Theme/Main/ExampleThemeMain
        $relativeBlockNamespace = str_replace($resourceInfo[self::RESOURCE_KEY_NAMESPACE] . '\\', '', $blockClass);

        // e.g. ExampleThemeMain
        $blockName = explode('\\', $relativeBlockNamespace);
        $blockName = $blockName[count($blockName) - 1];

        // e.g. Example/Theme/Main
        $relativePath = explode('\\', $relativeBlockNamespace);
        $relativePath = array_slice($relativePath, 0, count($relativePath) - 1);
        $relativePath = implode(DIRECTORY_SEPARATOR, $relativePath);

        $resourceInfo[self::RESOURCE_KEY_RELATIVE_RESOURCE_PATH] = $relativePath . DIRECTORY_SEPARATOR . $blockName;
        $resourceInfo[self::RESOURCE_KEY_RELATIVE_BLOCK_PATH]    = $relativePath;
        $resourceInfo[self::RESOURCE_KEY_RESOURCE_NAME]          = $blockName;

        return $resourceInfo;
    }

    private static function getResourceInfoForTwigTemplate(Settings $settings, string $blockClass): ?array
    {
        $resourceInfo = self::getResourceInfo($settings, $blockClass);

        if (! $resourceInfo) {
            return null;
        }

        $absTwigPath = implode(
            '',
            [
                $resourceInfo['folder'],
                DIRECTORY_SEPARATOR,
                $resourceInfo['relativeResourcePath'],
                $settings->getTwigExtension(),
            ]
        );

        if (! is_file($absTwigPath)) {
            $parentClass = get_parent_class($blockClass);

            if ($parentClass &&
                is_subclass_of($parentClass, self::class) &&
                self::class !== $parentClass) {
                return self::getResourceInfoForTwigTemplate($settings, $parentClass);
            } else {
                return null;
            }
        }

        return $resourceInfo;
    }

    final public function getFieldsInfo(): array
    {
        return $this->fieldsInfo;
    }

    final public function isLoaded(): bool
    {
        return $this->isLoaded;
    }

    private function getBlockField(string $fieldName): ?Block
    {
        $block      = null;
        $fieldsInfo = $this->fieldsInfo;

        if (key_exists($fieldName, $fieldsInfo)) {
            $block = $this->{$fieldName};

            // prevent possible recursion by a mistake (if someone will create a field with self)
            // using static for children support
            $block = ($block &&
                      $block instanceof Block &&
                      get_class($block) !== static::class) ?
                $block :
                null;
        }

        return $block;
    }

    public function getDependencies(string $sourceClass = ''): array
    {
        $dependencyClasses = [];
        $fieldsInfo        = $this->fieldsInfo;

        foreach ($fieldsInfo as $fieldName => $fieldType) {
            $dependencyBlock = $this->getBlockField($fieldName);

            if (! $dependencyBlock) {
                continue;
            }

            $dependencyClass = get_class($dependencyBlock);

            // 1. prevent the possible permanent recursion
            // 2. add only unique elements, because several fields can have the same type
            if (
                ($sourceClass && $dependencyClass === $sourceClass) ||
                in_array($dependencyClass, $dependencyClasses, true)
            ) {
                continue;
            }

            // used static for child support
            $subDependencies = $dependencyBlock->getDependencies(static::class);
            // only unique elements
            $subDependencies = array_diff($subDependencies, $dependencyClasses);

            // sub dependencies are before the main dependency
            $dependencyClasses = array_merge($dependencyClasses, $subDependencies, [$dependencyClass,]);
        }

        return $dependencyClasses;
    }

    // can be overridden for add external arguments
    public function getTemplateArgs(Settings $settings): array
    {
        // using static for child support
        $resourceInfo = self::getResourceInfoForTwigTemplate($settings, static::class);

        $pathToTemplate = $resourceInfo ?
            $resourceInfo[self::RESOURCE_KEY_RELATIVE_RESOURCE_PATH] . $settings->getTwigExtension() :
            '';
        $namespace      = $resourceInfo[self::RESOURCE_KEY_NAMESPACE] ?? '';

        $templateArgs = [
            self::TEMPLATE_KEY_NAMESPACE => $namespace,
            self::TEMPLATE_KEY_TEMPLATE  => $pathToTemplate,
            self::TEMPLATE_KEY_IS_LOADED => $this->isLoaded,
        ];

        if (! $pathToTemplate) {
            $settings->callErrorCallback(
                [
                    'error' => 'Twig template is missing for the block',
                    // using static for child support
                    'class' => static::class,
                ]
            );
        }

        foreach ($this->fieldsInfo as $fieldName => $fieldType) {
            $value = $this->{$fieldName};

            if ($value instanceof self) {
                $value = $value->getTemplateArgs($settings);
            }

            $templateArgs[$fieldName] = $value;
        }

        return $templateArgs;
    }

    protected function getFieldType(string $fieldName): ?string
    {
        $fieldType = null;

        try {
            // used static for child support
            $property = new ReflectionProperty(static::class, $fieldName);
        } catch (Exception $ex) {
            return $fieldType;
        }

        if (! $property->isProtected()) {
            return $fieldType;
        }

        return $property->getType() ?
            $property->getType()->getName() :
            '';
    }

    private function readFieldsInfo(): void
    {
        $fieldNames = array_keys(get_class_vars(static::class));

        foreach ($fieldNames as $fieldName) {
            $fieldType = $this->getFieldType($fieldName);

            // only protected fields
            if (is_null($fieldType)) {
                continue;
            }

            $this->fieldsInfo[$fieldName] = $fieldType;
        }
    }

    private function autoInitFields(): void
    {
        foreach ($this->fieldsInfo as $fieldName => $fieldType) {
            // ignore fields without a type
            if (! $fieldType) {
                continue;
            }

            $defaultValue = null;

            switch ($fieldType) {
                case 'int':
                case 'float':
                    $defaultValue = 0;
                    break;
                case 'bool':
                    $defaultValue = false;
                    break;
                case 'string':
                    $defaultValue = '';
                    break;
                case 'array':
                    $defaultValue = [];
                    break;
            }

            try {
                if (is_subclass_of($fieldType, Block::class)) {
                    $defaultValue = new $fieldType();
                }
            } catch (Exception $ex) {
                $defaultValue = null;
            }

            // ignore fields with a custom type (null by default)
            if (is_null($defaultValue)) {
                continue;
            }

            $this->{$fieldName} = $defaultValue;
        }
    }

    final protected function load(): void
    {
        $this->isLoaded = true;
    }

}

      
      



BlockTest.php
<?php

declare(strict_types=1);

namespace LightSource\FrontBlocks\Tests\unit;

use Codeception\Test\Unit;
use LightSource\FrontBlocks\Block;
use LightSource\FrontBlocks\Settings;
use org\bovigo\vfs\vfsStream;
use UnitTester;

class BlockTest extends Unit
{

    protected UnitTester $tester;

    public function testReadProtectedFields()
    {
        $block = new class extends Block {
            protected $loadedField;
        };

        $this->assertEquals(
            ['loadedField',],
            array_keys($block->getFieldsInfo())
        );
    }

    public function testIgnoreReadPublicFields()
    {
        $block = new class extends Block {
            public $ignoredField;
        };

        $this->assertEquals(
            [],
            array_keys($block->getFieldsInfo())
        );
    }

    public function testReadFieldWithType()
    {
        $block = new class extends Block {
            protected string $loadedField;
        };

        $this->assertEquals(
            [
                'loadedField' => 'string',
            ],
            $block->getFieldsInfo()
        );
    }

    public function testReadFieldWithoutType()
    {
        $block = new class extends Block {
            protected $loadedField;
        };

        $this->assertEquals(
            [
                'loadedField' => '',
            ],
            $block->getFieldsInfo()
        );
    }

    public function testAutoInitIntField()
    {
        $block = new class extends Block {

            protected int $int;

            public function getInt()
            {
                return $this->int;
            }
        };

        $this->assertTrue(0 === $block->getInt());
    }

    public function testAutoInitFloatField()
    {
        $block = new class extends Block {

            protected float $float;

            public function getFloat()
            {
                return $this->float;
            }
        };

        $this->assertTrue(0.0 === $block->getFloat());
    }

    public function testAutoInitStringField()
    {
        $block = new class extends Block {

            protected string $string;

            public function getString()
            {
                return $this->string;
            }
        };

        $this->assertTrue('' === $block->getString());
    }

    public function testAutoInitBoolField()
    {
        $block = new class extends Block {

            protected bool $bool;

            public function getBool()
            {
                return $this->bool;
            }
        };

        $this->assertTrue(false === $block->getBool());
    }

    public function testAutoInitArrayField()
    {
        $block = new class extends Block {

            protected array $array;

            public function getArray()
            {
                return $this->array;
            }
        };

        $this->assertTrue([] === $block->getArray());
    }

    public function testAutoInitBlockField()
    {
        $testBlock        = new class extends Block {
        };
        $testBlockClass   = get_class($testBlock);
        $block            = new class ($testBlockClass) extends Block {

            protected $block;
            private $testClass;

            public function __construct($testClass)
            {
                $this->testClass = $testClass;
                parent::__construct();
            }

            public function getFieldType(string $fieldName): ?string
            {
                return ('block' === $fieldName ?
                    $this->testClass :
                    parent::getFieldType($fieldName));
            }

            public function getBlock()
            {
                return $this->block;
            }
        };
        $actualBlockClass = $block->getBlock() ?
            get_class($block->getBlock()) :
            '';

        $this->assertEquals($actualBlockClass, $testBlockClass);
    }

    public function testIgnoreAutoInitFieldWithoutType()
    {
        $block = new class extends Block {

            protected $default;

            public function getDefault()
            {
                return $this->default;
            }
        };

        $this->assertTrue(null === $block->getDefault());
    }

    public function testGetResourceInfo()
    {
        $settings = new Settings();
        $settings->addBlocksFolder('TestNamespace', 'test-folder');
        $this->assertEquals(
            [
                Block::RESOURCE_KEY_NAMESPACE              => 'TestNamespace',
                Block::RESOURCE_KEY_FOLDER                 => 'test-folder',
                Block::RESOURCE_KEY_RELATIVE_RESOURCE_PATH => 'Button/Theme/Red/ButtonThemeRed',
                Block::RESOURCE_KEY_RELATIVE_BLOCK_PATH    => 'Button/Theme/Red',
                Block::RESOURCE_KEY_RESOURCE_NAME          => 'ButtonThemeRed',
            ],
            Block::getResourceInfo($settings, 'TestNamespace\\Button\\Theme\\Red\\ButtonThemeRed')
        );
    }

    public function testGetDependenciesWithSubDependenciesRecursively()
    {
        $spanBlock   = new class extends Block {
        };
        $buttonBlock = new class ($spanBlock) extends Block {

            protected $spanBlock;

            public function __construct($spanBlock)
            {
                parent::__construct();

                $this->spanBlock = $spanBlock;
            }
        };
        $formBlock   = new class ($buttonBlock) extends Block {

            protected $buttonBlock;

            public function __construct($buttonBlock)
            {
                parent::__construct();

                $this->buttonBlock = $buttonBlock;
            }
        };

        $this->assertEquals(
            [
                get_class($spanBlock),
                get_class($buttonBlock),
            ],
            $formBlock->getDependencies()
        );
    }

    public function testGetDependenciesInRightOrder()
    {
        $spanBlock   = new class extends Block {
        };
        $buttonBlock = new class ($spanBlock) extends Block {

            protected $spanBlock;

            public function __construct($spanBlock)
            {
                parent::__construct();

                $this->spanBlock = $spanBlock;
            }
        };
        $formBlock   = new class ($buttonBlock) extends Block {

            protected $buttonBlock;

            public function __construct($buttonBlock)
            {
                parent::__construct();

                $this->buttonBlock = $buttonBlock;
            }
        };

        $this->assertEquals(
            [
                get_class($spanBlock),
                get_class($buttonBlock),
            ],
            $formBlock->getDependencies()
        );
    }

    public function testGetDependenciesWhenBlocksAreDependentFromEachOther()
    {
        $buttonBlock = new class extends Block {

            protected $formBlock;

            public function __construct()
            {
                parent::__construct();
            }

            public function setFormBlock($formBlock)
            {
                $this->formBlock = $formBlock;
            }

        };
        $formBlock   = new class ($buttonBlock) extends Block {

            protected $buttonBlock;

            public function __construct($buttonBlock)
            {
                parent::__construct();

                $this->buttonBlock = $buttonBlock;
            }
        };
        $buttonBlock->setFormBlock($formBlock);

        $this->assertEquals(
            [
                get_class($buttonBlock),
            ],
            $formBlock->getDependencies()
        );
    }

    public function testGetDependenciesWithoutDuplicatesWhenSeveralWithOneType()
    {
        function getButtonBlock()
        {
            return new class extends Block {
            };
        }

        $inputBlock = new class (getButtonBlock()) extends Block {

            protected $buttonBlock;

            public function __construct($buttonBlock)
            {
                parent::__construct();
                $this->buttonBlock = $buttonBlock;
            }
        };

        $formBlock = new class ($inputBlock) extends Block {

            protected $inputBlock;
            protected $firstButtonBlock;
            protected $secondButtonBlock;

            public function __construct($inputBlock)
            {
                parent::__construct();

                $this->inputBlock        = $inputBlock;
                $this->firstButtonBlock  = getButtonBlock();
                $this->secondButtonBlock = getButtonBlock();
            }
        };

        $this->assertEquals(
            [
                get_class(getButtonBlock()),
                get_class($inputBlock),
            ],
            $formBlock->getDependencies()
        );
    }

    public function testGetTemplateArgsWhenBlockContainsBuiltInTypes()
    {
        $settings    = new Settings();
        $buttonBlock = new class extends Block {

            protected string $name;

            public function __construct()
            {
                parent::__construct();
                $this->name = 'button';
            }
        };

        $this->assertEquals(
            [
                Block::TEMPLATE_KEY_NAMESPACE => '',
                Block::TEMPLATE_KEY_TEMPLATE  => '',
                Block::TEMPLATE_KEY_IS_LOADED => false,
                'name'                        => 'button',
            ],
            $buttonBlock->getTemplateArgs($settings)
        );
    }

    public function testGetTemplateArgsWhenBlockContainsAnotherBlockRecursively()
    {
        $settings    = new Settings();
        $spanBlock   = new class extends Block {

            protected string $name;

            public function __construct()
            {
                parent::__construct();
                $this->name = 'span';
            }
        };
        $buttonBlock = new class ($spanBlock) extends Block {

            protected $spanBlock;

            public function __construct($spanBlock)
            {
                parent::__construct();
                $this->spanBlock = $spanBlock;
            }
        };
        $formBlock   = new class ($buttonBlock) extends Block {

            protected $buttonBlock;

            public function __construct($buttonBlock)
            {
                parent::__construct();
                $this->buttonBlock = $buttonBlock;
            }

        };

        $this->assertEquals(
            [
                Block::TEMPLATE_KEY_NAMESPACE => '',
                Block::TEMPLATE_KEY_TEMPLATE  => '',
                Block::TEMPLATE_KEY_IS_LOADED => false,
                'buttonBlock'                 => [
                    Block::TEMPLATE_KEY_NAMESPACE => '',
                    Block::TEMPLATE_KEY_TEMPLATE  => '',
                    Block::TEMPLATE_KEY_IS_LOADED => false,
                    'spanBlock'                   => [
                        Block::TEMPLATE_KEY_NAMESPACE => '',
                        Block::TEMPLATE_KEY_TEMPLATE  => '',
                        Block::TEMPLATE_KEY_IS_LOADED => false,
                        'name'                        => 'span',
                    ],
                ],
            ],
            $formBlock->getTemplateArgs($settings)
        );
    }

    public function testGetTemplateArgsWhenTemplateIsInParent()
    {
        $rootDirectory = $this->tester->getUniqueDirectory(__METHOD__);
        $namespace     = $this->tester->getUniqueControllerNamespaceWithAutoloader(__METHOD__, $rootDirectory->url());
        $blocksFolder  = vfsStream::create(
            [
                'ButtonBase'  => [
                    'ButtonBase.php'  => $this->tester->getBlockClassFile(
                        $namespace . '\ButtonBase',
                        'ButtonBase',
                        '\\' . Block::class
                    ),
                    'ButtonBase.twig' => '',
                ],
                'ButtonChild' => [
                    'ButtonChild.php' => $this->tester->getBlockClassFile(
                        $namespace . '\ButtonChild',
                        'ButtonChild',
                        '\\' . $namespace . '\ButtonBase\ButtonBase'
                    ),
                ],
            ],
            $rootDirectory
        );


        $settings = new Settings();
        $settings->addBlocksFolder($namespace, $blocksFolder->url());

        $buttonChildClass = $namespace . '\ButtonChild\ButtonChild';
        $buttonChild      = new $buttonChildClass();

        if (! $buttonChild instanceof Block) {
            $this->fail("Class doesn't child to Block");
        }

        $this->assertEquals(
            [
                Block::TEMPLATE_KEY_NAMESPACE => $namespace,
                Block::TEMPLATE_KEY_TEMPLATE  => 'ButtonBase/ButtonBase.twig',
                Block::TEMPLATE_KEY_IS_LOADED => false,
            ],
            $buttonChild->getTemplateArgs($settings)
        );
    }
}

      
      



BlocksLoader





, ::onLoad, , ajax ..





BlocksLoader.php
<?php

declare(strict_types=1);

namespace LightSource\FrontBlocks;

class BlocksLoader
{

    private array $loadedBlockClasses;
    private Settings $settings;

    public function __construct(Settings $settings)
    {
        $this->loadedBlockClasses = [];
        $this->settings           = $settings;
    }

    final public function getLoadedBlockClasses(): array
    {
        return $this->loadedBlockClasses;
    }

    private function tryToLoadBlock(string $phpClass): bool
    {
        $isLoaded = false;

        if (
            ! class_exists($phpClass, true) ||
            ! is_subclass_of($phpClass, Block::class)
        ) {
            // without any error, because php files can contain other things
            return $isLoaded;
        }

        call_user_func([$phpClass, 'onLoad']);

        return true;
    }

    private function loadBlocks(string $namespace, array $phpFileNames): void
    {
        foreach ($phpFileNames as $phpFileName) {
            $phpClass = implode('\\', [$namespace, str_replace('.php', '', $phpFileName),]);

            if (! $this->tryToLoadBlock($phpClass)) {
                continue;
            }

            $this->loadedBlockClasses[] = $phpClass;
        }
    }

    private function loadDirectory(string $directory, string $namespace): void
    {
        // exclude ., ..
        $fs = array_diff(scandir($directory), ['.', '..']);

        $phpFilePreg = '/.php$/';

        $phpFileNames      = Helper::arrayFilter(
            $fs,
            function ($f) use ($phpFilePreg) {
                return (1 === preg_match($phpFilePreg, $f));
            },
            false
        );
        $subDirectoryNames = Helper::arrayFilter(
            $fs,
            function ($f) {
                return false === strpos($f, '.');
            },
            false
        );

        foreach ($subDirectoryNames as $subDirectoryName) {
            $subDirectory = implode(DIRECTORY_SEPARATOR, [$directory, $subDirectoryName]);
            $subNamespace = implode('\\', [$namespace, $subDirectoryName]);

            $this->loadDirectory($subDirectory, $subNamespace);
        }

        $this->loadBlocks($namespace, $phpFileNames);
    }

    final public function loadAllBlocks(): void
    {
        $blockFoldersInfo = $this->settings->getBlockFoldersInfo();

        foreach ($blockFoldersInfo as $namespace => $folder) {
            $this->loadDirectory($folder, $namespace);
        }
    }

}
      
      



BlocksLoaderTest.php
<?php

declare(strict_types=1);

namespace LightSource\FrontBlocks\Tests\unit;

use Codeception\Test\Unit;
use LightSource\FrontBlocks\Block;
use LightSource\FrontBlocks\BlocksLoader;
use LightSource\FrontBlocks\Settings;
use org\bovigo\vfs\vfsStream;
use UnitTester;

class BlocksLoaderTest extends Unit
{

    protected UnitTester $tester;

    public function testLoadAllBlocksWhichChildToBlock()
    {
        $rootDirectory = $this->tester->getUniqueDirectory(__METHOD__);
        $namespace     = $this->tester->getUniqueControllerNamespaceWithAutoloader(__METHOD__, $rootDirectory->url());
        $blocksFolder  = vfsStream::create(
            [
                'ButtonBase'  => [
                    'ButtonBase.php' => $this->tester->getBlockClassFile(
                        $namespace . '\ButtonBase',
                        'ButtonBase',
                        '\\' . Block::class
                    ),
                ],
                'ButtonChild' => [
                    'ButtonChild.php' => $this->tester->getBlockClassFile(
                        $namespace . '\ButtonChild',
                        'ButtonChild',
                        '\\' . $namespace . '\ButtonBase\ButtonBase'
                    ),
                ],
            ],
            $rootDirectory
        );

        $settings = new Settings();
        $settings->addBlocksFolder($namespace, $blocksFolder->url());

        $blocksLoader = new BlocksLoader($settings);
        $blocksLoader->loadAllBlocks();

        $this->assertEquals(
            [
                $namespace . '\ButtonBase\ButtonBase',
                $namespace . '\ButtonChild\ButtonChild',
            ],
            $blocksLoader->getLoadedBlockClasses()
        );
    }

    public function testLoadAllBlocksIgnoreNonChild()
    {
        $rootDirectory = $this->tester->getUniqueDirectory(__METHOD__);
        $namespace     = $this->tester->getUniqueControllerNamespaceWithAutoloader(__METHOD__, $rootDirectory->url());
        $blocksFolder  = vfsStream::create(
            [
                'ButtonBase' => [
                    'ButtonBase.php' => '<?php use ' . $namespace . '; class ButtonBase{}',
                ],
            ],
            $rootDirectory
        );

        $settings = new Settings();
        $settings->addBlocksFolder($namespace, $blocksFolder->url());

        $blocksLoader = new BlocksLoader($settings);
        $blocksLoader->loadAllBlocks();

        $this->assertEmpty($blocksLoader->getLoadedBlockClasses());
    }

    public function testLoadAllBlocksInSeveralFolders()
    {
        $rootDirectory   = $this->tester->getUniqueDirectory(__METHOD__);
        $firstFolderUrl  = $rootDirectory->url() . '/First';
        $secondFolderUrl = $rootDirectory->url() . '/Second';
        $firstNamespace  = $this->tester->getUniqueControllerNamespaceWithAutoloader(
            __METHOD__ . '_first',
            $firstFolderUrl,
        );
        $secondNamespace = $this->tester->getUniqueControllerNamespaceWithAutoloader(
            __METHOD__ . '_second',
            $secondFolderUrl,
        );
        vfsStream::create(
            [
                'First'  => [
                    'ButtonBase' => [
                        'ButtonBase.php' => $this->tester->getBlockClassFile(
                            $firstNamespace . '\ButtonBase',
                            'ButtonBase',
                            '\\' . Block::class
                        ),
                    ],
                ],
                'Second' => [
                    'ButtonBase' => [
                        'ButtonBase.php' => $this->tester->getBlockClassFile(
                            $secondNamespace . '\ButtonBase',
                            'ButtonBase',
                            '\\' . Block::class
                        ),
                    ],
                ],
            ],
            $rootDirectory
        );

        $settings = new Settings();
        $settings->addBlocksFolder($firstNamespace, $firstFolderUrl);
        $settings->addBlocksFolder($secondNamespace, $secondFolderUrl);

        $blocksLoader = new BlocksLoader($settings);
        $blocksLoader->loadAllBlocks();

        $this->assertEquals(
            [
                $firstNamespace . '\ButtonBase\ButtonBase',
                $secondNamespace . '\ButtonBase\ButtonBase',
            ],
            $blocksLoader->getLoadedBlockClasses()
        );
    }
}
      
      



Renderer





, , , (css, js)





Renderer.php
<?php

declare(strict_types=1);

namespace LightSource\FrontBlocks;

class Renderer
{

    private Settings $settings;
    private TwigWrapper $twigWrapper;
    private BlocksLoader $blocksLoader;
    private array $usedBlockClasses;

    public function __construct(Settings $settings)
    {
        $this->settings         = $settings;
        $this->twigWrapper             = new TwigWrapper($settings);
        $this->blocksLoader     = new BlocksLoader($settings);
        $this->usedBlockClasses = [];
    }

    final public function getSettings(): Settings
    {
        return $this->settings;
    }

    final public function getTwigWrapper(): TwigWrapper
    {
        return $this->twigWrapper;
    }

    final public function getBlocksLoader(): BlocksLoader
    {
        return $this->blocksLoader;
    }

    final public function getUsedBlockClasses(): array
    {
        return $this->usedBlockClasses;
    }

    final public function getUsedResources(string $extension, bool $isIncludeSource = false): string
    {
        $resourcesContent = '';

        foreach ($this->usedBlockClasses as $usedBlockClass) {
            $getResourcesInfoCallback = [$usedBlockClass, 'getResourceInfo'];

            if (! is_callable($getResourcesInfoCallback)) {
                $this->settings->callErrorCallback(
                    [
                        'message' => "Block class doesn't exist",
                        'class'   => $usedBlockClass,
                    ]
                );

                continue;
            }

            $resourceInfo = call_user_func_array(
                $getResourcesInfoCallback,
                [
                    $this->settings,
                ]
            );

            $pathToResourceFile = $resourceInfo['folder'] .
                                  DIRECTORY_SEPARATOR . $resourceInfo['relativeResourcePath'] . $extension;

            if (! is_file($pathToResourceFile)) {
                continue;
            }

            $resourcesContent .= $isIncludeSource ?
                "\n/* " . $resourceInfo['resourceName'] . " */\n" :
                '';

            $resourcesContent .= file_get_contents($pathToResourceFile);
        }

        return $resourcesContent;
    }

    final public function render(Block $block, array $args = [], bool $isPrint = false): string
    {
        $dependencies           = array_merge($block->getDependencies(), [get_class($block),]);
        $newDependencies        = array_diff($dependencies, $this->usedBlockClasses);
        $this->usedBlockClasses = array_merge($this->usedBlockClasses, $newDependencies);

        $templateArgs           = $block->getTemplateArgs($this->settings);
        $templateArgs           = Helper::arrayMergeRecursive($templateArgs, $args);

        $namespace              = $templateArgs[Block::TEMPLATE_KEY_NAMESPACE];
        $relativePathToTemplate = $templateArgs[Block::TEMPLATE_KEY_TEMPLATE];

        // log already exists
        if (! $relativePathToTemplate) {
            return '';
        }

        return $this->twigWrapper->render($namespace, $relativePathToTemplate, $templateArgs, $isPrint);
    }

}

      
      



RendererTest.php
<?php

declare(strict_types=1);

namespace LightSource\FrontBlocks\Tests\unit;

use Codeception\Test\Unit;
use LightSource\FrontBlocks\Block;
use LightSource\FrontBlocks\Renderer;
use LightSource\FrontBlocks\Settings;
use org\bovigo\vfs\vfsStream;
use UnitTester;

class RendererTest extends Unit
{

    protected UnitTester $tester;

    public function testRenderAddsBlockToUsedList()
    {
        $settings = new Settings();
        $renderer = new Renderer($settings);

        $button = new class extends Block {
        };

        $renderer->render($button);

        $this->assertEquals(
            [
                get_class($button),
            ],
            $renderer->getUsedBlockClasses()
        );
    }

    public function testRenderAddsBlockDependenciesToUsedList()
    {
        $settings = new Settings();
        $renderer = new Renderer($settings);

        $button = new class extends Block {
        };
        $form   = new class ($button) extends Block {

            protected $button;

            public function __construct($button)
            {
                parent::__construct();
                $this->button = $button;
            }
        };

        $renderer->render($form);

        $this->assertEquals(
            [
                get_class($button),
                get_class($form),
            ],
            $renderer->getUsedBlockClasses()
        );
    }

    public function testRenderAddsDependenciesBeforeBlockToUsedList()
    {
        $settings = new Settings();
        $renderer = new Renderer($settings);

        $button = new class extends Block {
        };
        $form   = new class ($button) extends Block {

            protected $button;

            public function __construct($button)
            {
                parent::__construct();
                $this->button = $button;
            }
        };

        $renderer->render($form);

        $this->assertEquals(
            [
                get_class($button),
                get_class($form),
            ],
            $renderer->getUsedBlockClasses()
        );
    }

    public function testRenderAddsBlockToUsedListOnce()
    {
        $settings = new Settings();
        $renderer = new Renderer($settings);

        $button = new class extends Block {
        };

        $renderer->render($button);
        $renderer->render($button);

        $this->assertEquals(
            [
                get_class($button),
            ],
            $renderer->getUsedBlockClasses()
        );
    }

    public function testRenderAddsBlockDependenciesToUsedListOnce()
    {
        $settings = new Settings();
        $renderer = new Renderer($settings);

        $button = new class extends Block {
        };
        $form   = new class ($button) extends Block {

            protected $button;

            public function __construct($button)
            {
                parent::__construct();
                $this->button = $button;
            }
        };
        $footer = new class ($button) extends Block {

            protected $button;

            public function __construct($button)
            {
                parent::__construct();
                $this->button = $button;
            }
        };

        $renderer->render($form);
        $renderer->render($footer);

        $this->assertEquals(
            [
                get_class($button),
                get_class($form),
                get_class($footer),
            ],
            $renderer->getUsedBlockClasses()
        );
    }

    public function testGetUsedResources()
    {
        $rootDirectory = $this->tester->getUniqueDirectory(__METHOD__);
        $namespace     = $this->tester->getUniqueControllerNamespaceWithAutoloader(__METHOD__, $rootDirectory->url());
        $blocksFolder  = vfsStream::create(
            [
                'Button' => [
                    'Button.php' => $this->tester->getBlockClassFile(
                        $namespace . '\Button',
                        'Button',
                        '\\' . Block::class
                    ),
                    'Button.css' => '.button{}',
                ],
                'Form'   => [
                    'Form.php' => $this->tester->getBlockClassFile(
                        $namespace . '\Form',
                        'Form',
                        '\\' . Block::class
                    ),
                    'Form.css' => '.form{}',
                ],
            ],
            $rootDirectory
        );

        $formClass   = $namespace . '\Form\Form';
        $form        = new $formClass();
        $buttonClass = $namespace . '\Button\Button';
        $button      = new $buttonClass();

        $settings = new Settings();
        $settings->addBlocksFolder($namespace, $blocksFolder->url());
        $renderer = new Renderer($settings);

        $renderer->render($button);
        $renderer->render($form);

        $this->assertEquals('.button{}.form{}', $renderer->getUsedResources('.css'));
    }

    public function testGetUsedResourcesWithIncludedSource()
    {
        $rootDirectory = $this->tester->getUniqueDirectory(__METHOD__);
        $namespace     = $this->tester->getUniqueControllerNamespaceWithAutoloader(__METHOD__, $rootDirectory->url());
        $blocksFolder  = vfsStream::create(
            [
                'Button' => [
                    'Button.php' => $this->tester->getBlockClassFile(
                        $namespace . '\Button',
                        'Button',
                        '\\' . Block::class
                    ),
                    'Button.css' => '.button{}',
                ],
                'Form'   => [
                    'Form.php' => $this->tester->getBlockClassFile(
                        $namespace . '\Form',
                        'Form',
                        '\\' . Block::class
                    ),
                    'Form.css' => '.form{}',
                ],
            ],
            $rootDirectory
        );

        $formClass   = $namespace . '\Form\Form';
        $form        = new $formClass();
        $buttonClass = $namespace . '\Button\Button';
        $button      = new $buttonClass();

        $settings = new Settings();
        $settings->addBlocksFolder($namespace, $blocksFolder->url());
        $renderer = new Renderer($settings);

        $renderer->render($button);
        $renderer->render($form);

        $this->assertEquals(
            "\n/* Button */\n.button{}\n/* Form */\n.form{}",
            $renderer->getUsedResources('.css', true)
        );
    }
}

      
      



Settings





,





Settings.php
<?php

declare(strict_types=1);

namespace LightSource\FrontBlocks;

class Settings
{

    private array $blockFoldersInfo;
    private array $twigArgs;
    private string $twigExtension;
    private $errorCallback;

    public function __construct()
    {
        $this->blockFoldersInfo = [];
        $this->twigArgs         = [
            // will generate exception if a var doesn't exist instead of replace to NULL
            'strict_variables' => true,
            // disable autoescape to prevent break data
            'autoescape'       => false,
        ];
        $this->twigExtension    = '.twig';
        $this->errorCallback    = null;
    }

    public function addBlocksFolder(string $namespace, string $folder): void
    {
        $this->blockFoldersInfo[$namespace] = $folder;
    }

    public function setTwigArgs(array $twigArgs): void
    {
        $this->twigArgs = array_merge($this->twigArgs, $twigArgs);
    }

    public function setErrorCallback(?callable $errorCallback): void
    {
        $this->errorCallback = $errorCallback;
    }

    public function setTwigExtension(string $twigExtension): void
    {
        $this->twigExtension = $twigExtension;
    }

    public function getBlockFoldersInfo(): array
    {
        return $this->blockFoldersInfo;
    }

    public function getBlockFolderInfoByBlockClass(string $blockClass): ?array
    {
        foreach ($this->blockFoldersInfo as $blockNamespace => $blockFolder) {
            if (0 !== strpos($blockClass, $blockNamespace)) {
                continue;
            }

            return [
                'namespace' => $blockNamespace,
                'folder'    => $blockFolder,
            ];
        }

        return null;
    }

    public function getTwigArgs(): array
    {
        return $this->twigArgs;
    }

    public function getTwigExtension(): string
    {
        return $this->twigExtension;
    }

    public function callErrorCallback(array $errors): void
    {
        if (! is_callable($this->errorCallback)) {
            return;
        }

        call_user_func_array($this->errorCallback, [$errors,]);
    }
}

      
      



SettingsTest.php
<?php

declare(strict_types=1);

namespace LightSource\FrontBlocks\Tests\unit;

use Codeception\Test\Unit;
use LightSource\FrontBlocks\Settings;

class SettingsTest extends Unit
{
    public function testGetBlockFolderInfoByBlockClass()
    {
        $settings = new Settings();
        $settings->addBlocksFolder('TestNamespace', 'test-folder');
        $this->assertEquals(
            [
                'namespace' => 'TestNamespace',
                'folder'    => 'test-folder',
            ],
            $settings->getBlockFolderInfoByBlockClass('TestNamespace\Class')
        );
    }

    public function testGetBlockFolderInfoByBlockClassWhenSeveral()
    {
        $settings = new Settings();
        $settings->addBlocksFolder('FirstNamespace', 'first-namespace');
        $settings->addBlocksFolder('SecondNamespace', 'second-namespace');
        $this->assertEquals(
            [
                'namespace' => 'FirstNamespace',
                'folder'    => 'first-namespace',
            ],
            $settings->getBlockFolderInfoByBlockClass('FirstNamespace\Class')
        );
    }

    public function testGetBlockFolderInfoByBlockClassIgnoreWrong()
    {
        $settings = new Settings();
        $settings->addBlocksFolder('TestNamespace', 'test-folder');
        $this->assertEquals(
            null,
            $settings->getBlockFolderInfoByBlockClass('WrongNamespace\Class')
        );
    }
}

      
      



TwigWrapper





Twig , . twig _include ( include _isLoaded _template Block->getTemplateArgs ) _merge ( , ).





TwigWrapper.php
<?php

declare(strict_types=1);

namespace LightSource\FrontBlocks;

use Exception;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
use Twig\Loader\LoaderInterface;
use Twig\TwigFilter;
use Twig\TwigFunction;

class TwigWrapper
{

    private ?LoaderInterface $twigLoader;
    private ?Environment $twigEnvironment;
    private Settings $settings;

    public function __construct(Settings $settings, ?LoaderInterface $twigLoader = null)
    {
        $this->twigEnvironment = null;
        $this->settings        = $settings;
        $this->twigLoader      = $twigLoader;

        $this->init();
    }

    private static function GetTwigNamespace(string $namespace)
    {
        return str_replace('\\', '_', $namespace);
    }

    // e.g for extend a twig with adding a new filter
    public function getEnvironment(): ?Environment
    {
        return $this->twigEnvironment;
    }

    private function extendTwig(): void
    {
        $this->twigEnvironment->addFilter(
            new TwigFilter(
                '_merge',
                function ($source, $additional) {
                    return Helper::arrayMergeRecursive($source, $additional);
                }
            )
        );
        $this->twigEnvironment->addFunction(
            new TwigFunction(
                '_include',
                function ($block, $args = []) {
                    $block = Helper::arrayMergeRecursive($block, $args);

                    return $block[Block::TEMPLATE_KEY_IS_LOADED] ?
                        $this->render(
                            $block[Block::TEMPLATE_KEY_NAMESPACE],
                            $block[Block::TEMPLATE_KEY_TEMPLATE],
                            $block
                        ) :
                        '';
                }
            )
        );
    }

    private function init(): void
    {
        $blockFoldersInfo = $this->settings->getBlockFoldersInfo();

        try {
            // can be already init (in tests)
            if (! $this->twigLoader) {
                $this->twigLoader = new FilesystemLoader();
                foreach ($blockFoldersInfo as $namespace => $folder) {
                    $this->twigLoader->addPath($folder, self::GetTwigNamespace($namespace));
                }
            }

            $this->twigEnvironment = new Environment($this->twigLoader, $this->settings->getTwigArgs());
        } catch (Exception $ex) {
            $this->twigEnvironment = null;

            $this->settings->callErrorCallback(
                [
                    'message' => $ex->getMessage(),
                    'file'    => $ex->getFile(),
                    'line'    => $ex->getLine(),
                    'trace'   => $ex->getTraceAsString(),
                ]
            );

            return;
        }

        $this->extendTwig();
    }

    public function render(string $namespace, string $template, array $args = [], bool $isPrint = false): string
    {
        $html = '';

        // twig isn't loaded
        if (is_null($this->twigEnvironment)) {
            return $html;
        }

        // can be empty, e.g. for tests
        $twigNamespace = $namespace ?
            '@' . self::GetTwigNamespace($namespace) . '/' :
            '';

        try {
            // will generate ean exception if a template doesn't exist OR broken
            // also if a var doesn't exist (if using a 'strict_variables' flag, see Twig_Environment->__construct)
            $html .= $this->twigEnvironment->render($twigNamespace . $template, $args);
        } catch (Exception $ex) {
            $html = '';

            $this->settings->callErrorCallback(
                [
                    'message'  => $ex->getMessage(),
                    'file'     => $ex->getFile(),
                    'line'     => $ex->getLine(),
                    'trace'    => $ex->getTraceAsString(),
                    'template' => $template,
                ]
            );
        }

        if ($isPrint) {
            echo $html;
        }

        return $html;
    }
}

      
      



TwigWrapperTest.php
<?php

declare(strict_types=1);

namespace LightSource\FrontBlocks\Tests\unit;

use Codeception\Test\Unit;
use LightSource\FrontBlocks\Block;
use LightSource\FrontBlocks\Settings;
use LightSource\FrontBlocks\TwigWrapper;
use Twig\Loader\ArrayLoader;

class TwigWrapperTest extends Unit
{

    private function renderBlock(array $blocks, string $template, array $renderArgs = []): string
    {
        $twigLoader = new ArrayLoader($blocks);
        $settings   = new Settings();
        $twig       = new TwigWrapper($settings, $twigLoader);

        return $twig->render('', $template, $renderArgs);
    }

    public function testExtendTwigIncludeFunctionWhenBlockIsLoaded()
    {
        $blocks     = [
            'form.twig'   => '{{ _include(button) }}',
            'button.twig' => 'button content',
        ];
        $template   = 'form.twig';
        $renderArgs = [
            'button' => [
                Block::TEMPLATE_KEY_NAMESPACE => '',
                Block::TEMPLATE_KEY_TEMPLATE  => 'button.twig',
                Block::TEMPLATE_KEY_IS_LOADED => true,
            ],
        ];

        $this->assertEquals('button content', $this->renderBlock($blocks, $template, $renderArgs));
    }

    public function testExtendTwigIncludeFunctionWhenBlockNotLoaded()
    {
        $blocks     = [
            'form.twig'   => '{{ _include(button) }}',
            'button.twig' => 'button content',
        ];
        $template   = 'form.twig';
        $renderArgs = [
            'button' => [
                Block::TEMPLATE_KEY_NAMESPACE => '',
                Block::TEMPLATE_KEY_TEMPLATE  => 'button.twig',
                Block::TEMPLATE_KEY_IS_LOADED => false,
            ],
        ];

        $this->assertEquals('', $this->renderBlock($blocks, $template, $renderArgs));
    }

    public function testExtendTwigIncludeFunctionWhenArgsPassed()
    {
        $blocks     = [
            'form.twig'   => '{{ _include(button,{classes:["test-class",],}) }}',
            'button.twig' => '{{ classes|join(" ") }}',
        ];
        $template   = 'form.twig';
        $renderArgs = [
            'button' => [
                Block::TEMPLATE_KEY_NAMESPACE => '',
                Block::TEMPLATE_KEY_TEMPLATE  => 'button.twig',
                Block::TEMPLATE_KEY_IS_LOADED => true,
                'classes'                     => ['own-class',],
            ],
        ];

        $this->assertEquals('own-class test-class', $this->renderBlock($blocks, $template, $renderArgs));
    }

    public function testExtendTwigMergeFilter()
    {
        $blocks     = [
            'button.twig' => '{{ {"array":["first",],}|_merge({"array":["second",],}).array|join(" ") }}',
        ];
        $template   = 'button.twig';
        $renderArgs = [];

        $this->assertEquals('first second', $this->renderBlock($blocks, $template, $renderArgs));
    }
}

      
      



Helper





, , .





Helper.php
<?php

declare(strict_types=1);

namespace LightSource\FrontBlocks;

abstract class Helper
{

    final public static function arrayFilter(array $array, callable $callback, bool $isSaveKeys): array
    {
        $arrayResult = array_filter($array, $callback);

        return $isSaveKeys ?
            $arrayResult :
            array_values($arrayResult);
    }

    final public static function arrayMergeRecursive(array $args1, array $args2): array
    {
        foreach ($args2 as $key => $value) {
            if (intval($key) === $key) {
                $args1[] = $value;

                continue;
            }

            // recursive sub-merge for internal arrays
            if (
                is_array($value) &&
                key_exists($key, $args1) &&
                is_array($args1[$key])
            ) {
                $value = self::arrayMergeRecursive($args1[$key], $value);
            }

            $args1[$key] = $value;
        }

        return $args1;
    }
}

      
      



HelperTest.php
<?php

declare(strict_types=1);

namespace LightSource\FrontBlocks\Tests\unit;

use Codeception\Test\Unit;
use LightSource\FrontBlocks\Helper;

class HelperTest extends Unit
{

    public function testArrayFilterWithoutSaveKeys()
    {
        $this->assertEquals(
            [
                0 => '2',
            ],
            Helper::ArrayFilter(
                ['1', '2'],
                function ($value) {
                    return '1' !== $value;
                },
                false
            )
        );
    }

    public function testArrayFilterWithSaveKeys()
    {
        $this->assertEquals(
            [
                1 => '2',
            ],
            Helper::ArrayFilter(
                ['1', '2'],
                function ($value) {
                    return '1' !== $value;
                },
                true
            )
        );
    }

    public function testArrayMergeRecursive()
    {
        $this->assertEquals(
            [
                'classes' => [
                    'first',
                    'second',
                ],
                'value'   => 2,
            ],
            Helper::arrayMergeRecursive(
                [
                    'classes' => [
                        'first',
                    ],
                    'value'   => 1,
                ],
                [
                    'classes' => [
                        'second',
                    ],
                    'value'   => 2,
                ]
            )
        );
    }
}

      
      



, .





, css , - scss/webpack .





, Header, Article Button. Header Button , Article Button.





Header





Header.php
<?php

namespace LightSource\FrontBlocksSample\Header;

use LightSource\FrontBlocks\Block;

class Header extends Block
{

    protected string $name;

    public function loadByTest()
    {
        parent::load();
        $this->name = 'I\'m Header';
    }
}

      
      



Header.twig
<div class="header">
    {{ name }}
</div>
      
      



Header.css
.header {
    color: green;
    border:1px solid green;
    padding: 10px;
}

      
      



Button





Button.php
<?php

namespace LightSource\FrontBlocksSample\Button;

use LightSource\FrontBlocks\Block;

class Button extends Block
{

    protected string $name;

    public function loadByTest()
    {
        parent::load();
        $this->name = 'I\'m Button';
    }
}

      
      



Button.twig
<div class="button">
    {{ name }}
</div>
      
      



Button.css
.button {
    color: black;
    border: 1px solid black;
    padding: 10px;
}

      
      



Article





Article.php
<?php

namespace LightSource\FrontBlocksSample\Article;

use LightSource\FrontBlocks\Block;
use LightSource\FrontBlocksSample\Button\Button;

class Article extends Block
{

    protected string $name;
    protected Button $button;

    public function loadByTest()
    {
        parent::load();
        $this->name = 'I\'m Article, I contain another block';
        $this->button->loadByTest();
    }
}

      
      



Article.twig
<div class="article">

    <p class="article__name">{{ name }}</p>

    {{ _include(button) }}

</div>
      
      



Article.css
.article {
    color: orange;
    border: 1px solid orange;
    padding: 10px;
}

.article__name {
    margin: 0 0 10px;
    line-height: 1.5;
}

      
      



, html , css





example.php
<?php

use LightSource\FrontBlocks\{
    Renderer,
    Settings
};
use LightSource\FrontBlocksSample\{
    Article\Article,
    Header\Header
};

require_once __DIR__ . '/vendors/vendor/autoload.php';

//// settings

ini_set('display_errors', 1);

$settings = new Settings();
$settings->addBlocksFolder('LightSource\FrontBlocksSample', __DIR__ . '/Blocks');
$settings->setErrorCallback(
    function (array $errors) {
        // todo log or any other actions
        echo '<pre>' . print_r($errors, true) . '</pre>';
    }
);
$renderer = new Renderer($settings);

//// usage

$header = new Header();
$header->loadByTest();

$article = new Article();
$article->loadByTest();

$content = $renderer->render($header);
$content .= $renderer->render($article);
$css     = $renderer->getUsedResources('.css', true);

//// html

?>
<html>

<head>

    <title>Example</title>
    <style>
        <?= $css ?>
    </style>
    <style>
        .article {
            margin-top: 10px;
        }
    </style>

</head>

<body>

<?= $content ?>

</body>

</html>

      
      







example.png

, , - . – . .





? .





:













repository with an example of using scss and js in blocks (webpack collector)





repository with an example of use in a WordPress theme (here you can also see an example of extending the block class and using autoloading, which adds support for ajax requests for blocks)





PS Thanks to @alexmixaylov , @bombe and @rpsv for constructive comments on the first part.








All Articles