Bloques modulares de extremo frontal: redacción de su propio paquete. Parte 2

En la primera parte, compartí mi punto de vista sobre lo que pueden ser los bloques de front-end reutilizables, recibí críticas constructivas, finalicé el paquete y ahora me gustaría compartir con ustedes una nueva versión. Le permitirá organizar fácilmente el uso de bloques modulares para cualquier proyecto con un backend php.





Para aquellos que no estén familiarizados con la primera parte, dejaré spoilers de ella, que los pondrán al día. Para aquellos que estén interesados ​​en el resultado final: un ejemplo de demostración y enlaces a repositorios al final del artículo .





Prefacio

Me presentaré: soy un desarrollador web joven con 5 años de experiencia. Durante el último año he estado trabajando como autónomo y la mayoría de los proyectos actuales están relacionados con WordPress. A pesar de varias críticas al CMS en general y WordPress en particular, creo que la arquitectura de WordPress en sí es una solución bastante buena, aunque por supuesto no sin ciertos inconvenientes. Y uno de ellos, en mi opinión, son las plantillas. En actualizaciones recientes, se han tomado grandes pasos para solucionar este problema, y ​​Gutenberg en su conjunto se está convirtiendo en una herramienta poderosa, pero desafortunadamente la mayoría de los temas continúan alterando plantillas, estilos y scripts, lo que hace que la edición sea algo extremadamente doloroso y la reutilización del código es a menudo imposible. Fue este problema el que me empujó a la idea de mi propio paquete, que organizaría la estructura y permitiría la reutilización de bloques.





La implementación tendrá la forma de un paquete de composición que se puede utilizar en proyectos completamente diferentes, sin estar vinculado a WordPress.





Formulación del problema

El concepto de un bloque a continuación será esencialmente el mismo concepto que un bloque en la metodología BEM , es decir este será un grupo de código html / js / css que representará una entidad.





Generaremos html y administraremos las dependencias de bloques a través de php, lo que significa que nuestro paquete será adecuado para proyectos con backend php. También estaremos de acuerdo en la orilla en que, sin entrar en disputas, no sucumbiremos a la influencia de cosas novedosas, como css-in-js o bem-json, y nos adheriremos al enfoque clásico el-classico , es decir. suponga que html, css y js son archivos diferentes.





Ahora establezcamos los requisitos básicos de nuestro paquete:









  • ()









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

, , - . – . .





? .





:













repositorio con un ejemplo del uso de scss y js en bloques (recopilador de paquetes web)





repositorio con un ejemplo de uso en un tema de WordPress (aquí también puede ver un ejemplo de extensión de la clase de bloque y uso de carga automática, que agrega soporte para solicitudes de bloques ajax)





PD Gracias a @alexmixaylov , @bombe y @rpsv por sus constructivos comentarios sobre la primera parte.








All Articles