Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В первой части я поделился своим взглядом на то, какими могут быть переиспользуемые 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, отмечу важный для меня пункт, то что он дисциплинирует, т.е. заставляет отделять обработку от вывода и подготавливать переменные заранее, и в данном случае мы будем использовать его.
Блок
Каждый блок будет состоять из:
Статических ресурсов (css/js/twig)
Класса блока, который будет предоставлять данные для twig шаблона и управлять зависимостями.
Вспомогательные классы: Settings (пути к блокам и их пространства имен), TwigWrapper (обертка для Twig пакета), BlocksLoader (автозагрузка всех блоков, опционально), Helper (набор статических доп. функций)
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 за конструктивные комментарии к первой части.