Модульные frond-end блоки — пишем свой пакет. Часть 2

от автора

В первой части я поделился своим взглядом на то, какими могут быть переиспользуемые front-end блоки, получил конструктивную критику, доработал пакет и теперь хотел бы поделиться с вами новой версией. Она позволит легко организовать использование модульных блоков для любого проекта с бекендом на php.

Для тех кто не знаком с первой частью я буду оставлять спойлеры из нее, которые введут в курс дела. Тем кому интересен конечный результат — демонстрационный пример и ссылки на репозитории в конце статьи.

Предисловие

Представлюсь — я молодой веб разработчик с опытом работы 5 лет. Последний год я работаю на фрилансе и большая часть текущих проектов связана с WordPress. Несмотря на различую критику CMS в общем и WordPress в часности, я считаю сама архитектура WordPress это довольно удачное решение, хотя конечно не без определенных недостатков. И один из них на мой взгляд это шаблоны. В крайних обновлениях сделаны большие шаги чтобы это исправить, и Gutenberg в целом становится мощным инструментом, однако к сожалению в большинстве тем продолжается каша в шаблонах, стилях и скриптах, которая делает редактирование чего-либо крайне болезненным, а переиспользование кода зачастую невозможным. Именно эта проблема и подтолкнуло меня к идее своего пакета, который бы организовывал структуру и позволял переиспользовать блоки.

Реализация будет в виде composer пакета, который можно будет использовать в совершенно различных проектах, без привязки к WordPress.

Постановка задачи

Понятие блок ниже будет по сути тем же понятием что и блок в BEM методологии, т.е. это будет группа html/js/css кода которая будет представлять одну сущность.

Генерировать html и управлять зависимостями блоков мы будем через php, что говорит о том, что наш пакет будет подходить для проектов с бекендом на php. Также условимся на берегу что, не вдаваясь в споры, не будем поддаваться влиянию новомодных вещей, таких как css-in-js или bem-json и будем придерживаться эль-классико классического подхода, т.е. предполагать что html, css и js это разные файлы.

Теперь давайте сформулируем наши основные требования к пакету:

  • Обеспечить структуру блоков

  • Предоставить поддержку наследования (расширения) блоков

  • Предоставить возможность использовать блок в блоке и соответственно поддержку зависимости ресурсов одного блока от ресурсов других блоков

Структура пакета

О ресурах блока и 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

Послесловие

Данный пакет был создан для личных целей, он облегчает мне разработку, и я буду рад если он пригодится кому-то еще. Не стесняйтесь задавать вопросы и комментировать – я буду рад ответить на ваши вопросы и услышать ваше мнение. Спасибо за внимание.

Понравилась статья? Не забудь проголосовать.

Ссылки:

репозиторий с данным пакетом

репозиторий с демонстрационным примером

репозиторий с примером использования scss и js в блоках (webpack сборщик)

репозиторий с примером использования в WordPress теме (здесь вы также можете увидеть пример расширения класса блока и использования автозагрузки, что добавляет поддержку ajax запросов для блоков)

P.S. Благодарю @alexmixaylov, @bombe и @rpsv за конструктивные комментарии к первой части.

ссылка на оригинал статьи https://habr.com/ru/post/558422/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *