こんにちは、Habrの読者の皆様。毎年、Web開発には、モジュラーアプローチを使用し、コードの開発と編集を簡素化するさまざまなソリューションがますます増えています。この記事では、再利用可能なフロントエンドブロック(phpバックエンドを使用するプロジェクトの場合)についての私の見解を示し、アイデアから実装までのすべてのステップを私と一緒に実行することをお勧めします。面白そう?それなら猫へようこそ。
序文
自己紹介をします。私は5年の経験を持つ若いWeb開発者です。昨年、私はフリーランサーとして働いており、現在のプロジェクトのほとんどはWordPressに関連しています。 CMS全般、特にWordPressに対するさまざまな批判にもかかわらず、WordPress自体のアーキテクチャは、もちろん特定の欠点がないわけではありませんが、かなり良い解決策だと思います。そして、私の意見では、そのうちの1つはテンプレートです。最近のアップデートでは、これを修正するために大きな措置が講じられており、グーテンベルクは全体として強力なツールになりつつありますが、残念ながらほとんどのテーマはテンプレート、スタイル、スクリプトをいじり続けているため、編集が非常に面倒であり、コードの再利用は多くの場合不可能です。私自身のミニフレームワークのアイデアに私を駆り立てたのはこの問題でした(パッケージを読んでください、しかしそれは構造に要件を課すので、私たちはそれをミニフレームワークと誇らしげに呼びます)、これにより、構造が整理され、ブロックを再利用できるようになります。
実装は、WordPressに縛られることなく、まったく異なるプロジェクトで使用できるコンポーザーパッケージの形式になります。
この記事を書く動機は、モジュラーブロックを整理するためのソリューションを共有したいという願望と、Habrの読者が自分の記事を書きたいという願望でした。これは、初心者向けに時々発生する独自のパッケージを作成したいという願望に似ています。既製のcomposerまたはnpmパッケージを使用します。
上記の文章から結論できるように、これはハブレに関する私の最初の記事です。したがって、トマトを投げたり、厳密に判断したりしないでください。
問題の定式化
以下のブロックの概念は、基本的にBEM方法論のブロックと同じ概念になります。これは、1つのエンティティを表すhtml / js / cssコードのグループになります。
html php, , php. , , , css-in-js bem-json - , .. html, css js .
-:
()
, css js , .. .js .css .min.css .min.js ( webpack ). html Twig ( ). - , Php , , Twig, , , .. , .
.
:
(css/js/twig)
( twig )
( , twig )
: Settings ( , ..), Twig
Blocks
, :
(Settings, Twig)
, (css/js)
( , , )
, – :
php 7.4+
PSR-4 (PSR-4 , composer, .. autoload/psr4 composer.json )
:
‘_C’
( )
, :
(CamelCase = camel-case)
(just_block = just-block)
‘Block_Theme_Main_C’ ‘block—theme--main’
, .. .
() : , . , , , - , .
FIELDS_READER
‘get_class_vars’ ‘ReflectionProperty’ , , (protected/public) . protected .
, , , .
FIELDS_READER.php
<?php
declare( strict_types=1 );
namespace LightSource\FrontBlocksFramework;
use Exception;
use ReflectionProperty;
abstract class FIELDS_READER {
private array $_fieldsInfo;
public function __construct() {
$this->_fieldsInfo = [];
$this->_readFieldsInfo();
$this->_autoInitFields();
}
final protected function _getFieldsInfo(): array {
return $this->_fieldsInfo;
}
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 {
// get protected fields without the '__' prefix
$fieldNames = array_keys( get_class_vars( static::class ) );
$fieldNames = array_filter( $fieldNames, function ( $fieldName ) {
$prefix = substr( $fieldName, 0, 2 );
return '__' !== $prefix;
} );
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, MODEL::class ) ||
is_subclass_of( $fieldType, CONTROLLER::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;
}
}
}
FIELDS_READERTest.php
<?php
declare( strict_types=1 );
namespace LightSource\FrontBlocksFramework\Tests\unit;
use Codeception\Test\Unit;
use LightSource\FrontBlocksFramework\CONTROLLER;
use LightSource\FrontBlocksFramework\FIELDS_READER;
use LightSource\FrontBlocksFramework\MODEL;
class FIELDS_READERTest extends Unit {
public function testReadProtectedField() {
$fieldsReader = new class extends FIELDS_READER {
protected $_loadedField;
public function __construct() {
parent::__construct();
}
public function getFields() {
return $this->_getFieldsInfo();
}
};
$this->assertEquals( [
'_loadedField' => '',
], $fieldsReader->getFields() );
}
public function testIgnoreReadProtectedPrefixedField() {
$fieldsReader = new class extends FIELDS_READER {
protected $__unloadedField;
public function __construct() {
parent::__construct();
}
public function getFields() {
return $this->_getFieldsInfo();
}
};
$this->assertEquals( [], $fieldsReader->getFields() );
}
public function testIgnoreReadPublicField() {
$fieldsReader = new class extends FIELDS_READER {
public $unloadedField;
public function __construct() {
parent::__construct();
}
public function getFields() {
return $this->_getFieldsInfo();
}
};
$this->assertEquals( [
], $fieldsReader->getFields() );
}
public function testIgnoreReadPrivateField() {
$fieldsReader = new class extends FIELDS_READER {
private $unloadedField;
public function __construct() {
parent::__construct();
}
public function getFields() {
return $this->_getFieldsInfo();
}
};
$this->assertEquals( [
], $fieldsReader->getFields() );
}
public function testReadFieldWithType() {
$fieldsReader = new class extends FIELDS_READER {
protected string $_loadedField;
public function __construct() {
parent::__construct();
}
public function getFields() {
return $this->_getFieldsInfo();
}
};
$this->assertEquals( [
'_loadedField' => 'string',
], $fieldsReader->getFields() );
}
public function testReadFieldWithoutType() {
$fieldsReader = new class extends FIELDS_READER {
protected $_loadedField;
public function __construct() {
parent::__construct();
}
public function getFields() {
return $this->_getFieldsInfo();
}
};
$this->assertEquals( [
'_loadedField' => '',
], $fieldsReader->getFields() );
}
////
public function testAutoInitIntField() {
$fieldsReader = new class extends FIELDS_READER {
protected int $_int;
public function __construct() {
parent::__construct();
}
public function getInt() {
return $this->_int;
}
};
$this->assertTrue( 0 === $fieldsReader->getInt() );
}
public function testAutoInitFloatField() {
$fieldsReader = new class extends FIELDS_READER {
protected float $_float;
public function __construct() {
parent::__construct();
}
public function getFloat() {
return $this->_float;
}
};
$this->assertTrue( 0.0 === $fieldsReader->getFloat() );
}
public function testAutoInitStringField() {
$fieldsReader = new class extends FIELDS_READER {
protected string $_string;
public function __construct() {
parent::__construct();
}
public function getString() {
return $this->_string;
}
};
$this->assertTrue( '' === $fieldsReader->getString() );
}
public function testAutoInitBoolField() {
$fieldsReader = new class extends FIELDS_READER {
protected bool $_bool;
public function __construct() {
parent::__construct();
}
public function getBool() {
return $this->_bool;
}
};
$this->assertTrue( false === $fieldsReader->getBool() );
}
public function testAutoInitArrayField() {
$fieldsReader = new class extends FIELDS_READER {
protected array $_array;
public function __construct() {
parent::__construct();
}
public function getArray() {
return $this->_array;
}
};
$this->assertTrue( [] === $fieldsReader->getArray() );
}
public function testAutoInitModelField() {
$testModel = new class extends MODEL {
};
$testModelClass = get_class( $testModel );
$fieldsReader = new class ( $testModelClass ) extends FIELDS_READER {
protected $_model;
private $_testClass;
public function __construct( $testClass ) {
$this->_testClass = $testClass;
parent::__construct();
}
public function _getFieldType( string $fieldName ): ?string {
return ( '_model' === $fieldName ?
$this->_testClass :
parent::_getFieldType( $fieldName ) );
}
public function getModel() {
return $this->_model;
}
};
$actualModelClass = $fieldsReader->getModel() ?
get_class( $fieldsReader->getModel() ) :
'';
$this->assertEquals( $actualModelClass, $testModelClass );
}
public function testAutoInitControllerField() {
$testController = new class extends CONTROLLER {
};
$testControllerClass = get_class( $testController );
$fieldsReader = new class ( $testControllerClass ) extends FIELDS_READER {
protected $_controller;
private $_testClass;
public function __construct( $testControllerClass ) {
$this->_testClass = $testControllerClass;
parent::__construct();
}
public function _getFieldType( string $fieldName ): ?string {
return ( '_controller' === $fieldName ?
$this->_testClass :
parent::_getFieldType( $fieldName ) );
}
public function getController() {
return $this->_controller;
}
};
$actualModelClass = $fieldsReader->getController() ?
get_class( $fieldsReader->getController() ) :
'';
$this->assertEquals( $actualModelClass, $testControllerClass );
}
public function testIgnoreInitFieldWithoutType() {
$fieldsReader = new class extends FIELDS_READER {
protected $_default;
public function __construct() {
parent::__construct();
}
public function getDefault() {
return $this->_default;
}
};
$this->assertTrue( null === $fieldsReader->getDefault() );
}
}
MODEL
FIELDS_READER, ‘_isLoaded’, , twig, ‘getFields’, protected , .
MODEL.php
<?php
declare( strict_types=1 );
namespace LightSource\FrontBlocksFramework;
abstract class MODEL extends FIELDS_READER {
private bool $_isLoaded;
public function __construct() {
parent::__construct();
$this->_isLoaded = false;
}
final public function isLoaded(): bool {
return $this->_isLoaded;
}
public function getFields(): array {
$args = [];
$fieldsInfo = $this->_getFieldsInfo();
foreach ( $fieldsInfo as $fieldName => $fieldType ) {
$args[ $fieldName ] = $this->{$fieldName};
}
return $args;
}
final protected function _load(): void {
$this->_isLoaded = true;
}
}
MODELTest.php
<?php
declare( strict_types=1 );
namespace LightSource\FrontBlocksFramework\Tests\unit;
use Codeception\Test\Unit;
use LightSource\FrontBlocksFramework\MODEL;
class MODELTest extends Unit {
public function testGetFields() {
$model = new class extends MODEL {
protected string $_field1;
public function __construct() {
parent::__construct();
}
public function update() {
$this->_field1 = 'just string';
}
};
$model->update();
$this->assertEquals( [
'_field1' => 'just string',
], $model->getFields() );
}
}
CONTROLLER
MODEL FIELDS_READER, . – ‘__external’, twig .
GetResourceInfo (twig,css,js) , ( ).
getTemplateArgs twig , protected ( ‘_’ ) , _template _isLoaded, , . (.. Model Model ) - : .. ( ), getTemplateArgs , .
getDependencies ( ) (.. ) -, .
, , .. , ( ), . (.. ) ( ).
CONTROLLER.php
<?php
declare( strict_types=1 );
namespace LightSource\FrontBlocksFramework;
use Exception;
abstract class CONTROLLER extends FIELDS_READER {
const TEMPLATE_KEY__TEMPLATE = '_template';
const TEMPLATE_KEY__IS_LOADED = '_isLoaded';
private ?MODEL $_model;
// using the prefix to prevent load this field
protected array $__external;
public function __construct( ?MODEL $model = null ) {
parent::__construct();
$this->_model = $model;
$this->__external = [];
$this->_autoInitModel();
}
final public static function GetResourceInfo( Settings $settings, string $controllerClass = '' ): array {
// using static for children support
$controllerClass = ! $controllerClass ?
static::class :
$controllerClass;
// e.g. $controllerClass = Example/Theme/Main/Example_Theme_Main_C
$resourceInfo = [
'resourceName' => '',// e.g. example--theme--main
'relativePath' => '',// e.g. Example/Theme/Main
'relativeResourcePath' => '', // e.g. Example/Theme/Main/example--theme--main
];
$controllerSuffix = Settings::$ControllerSuffix;
// e.g. Example/Theme/Main/Example_Theme_Main
$relativeControllerNamespace = $settings->getBlocksDirNamespace() ?
str_replace( $settings->getBlocksDirNamespace() . '\\', '', $controllerClass ) :
$controllerClass;
$relativeControllerNamespace = substr( $relativeControllerNamespace, 0, mb_strlen( $relativeControllerNamespace ) - mb_strlen( $controllerSuffix ) );
// e.g. Example_Theme_Main
$phpBlockName = explode( '\\', $relativeControllerNamespace );
$phpBlockName = $phpBlockName[ count( $phpBlockName ) - 1 ];
// e.g. example--theme--main (from Example_Theme_Main)
$blockNameParts = preg_split( '/(?=[A-Z])/', $phpBlockName, - 1, PREG_SPLIT_NO_EMPTY );
$blockResourceName = [];
foreach ( $blockNameParts as $blockNamePart ) {
$blockResourceName[] = strtolower( $blockNamePart );
}
$blockResourceName = implode( '-', $blockResourceName );
$blockResourceName = str_replace( '_', '-', $blockResourceName );
// e.g. Example/Theme/Main
$relativePath = explode( '\\', $relativeControllerNamespace );
$relativePath = array_slice( $relativePath, 0, count( $relativePath ) - 1 );
$relativePath = implode( DIRECTORY_SEPARATOR, $relativePath );
$resourceInfo['resourceName'] = $blockResourceName;
$resourceInfo['relativePath'] = $relativePath;
$resourceInfo['relativeResourcePath'] = $relativePath . DIRECTORY_SEPARATOR . $blockResourceName;
return $resourceInfo;
}
// can be overridden if Controller doesn't have own twig (uses parents)
public static function GetPathToTwigTemplate( Settings $settings, string $controllerClass = '' ): string {
return self::GetResourceInfo( $settings, $controllerClass )['relativeResourcePath'] . $settings->getTwigExtension();
}
// can be overridden if Controller doesn't have own model (uses parents)
public static function GetModelClass(): string {
$controllerClass = static::class;
$modelClass = rtrim( $controllerClass, Settings::$ControllerSuffix );
return ( $modelClass !== $controllerClass &&
class_exists( $modelClass, true ) &&
is_subclass_of( $modelClass, MODEL::class ) ?
$modelClass :
'' );
}
public static function OnLoad() {
}
final public function setModel( MODEL $model ): void {
$this->_model = $model;
}
private function _getControllerField( string $fieldName ): ?CONTROLLER {
$controller = null;
$fieldsInfo = $this->_getFieldsInfo();
if ( key_exists( $fieldName, $fieldsInfo ) ) {
$controller = $this->{$fieldName};
// prevent possible recursion by a mistake (if someone will create a field with self)
// using static for children support
$controller = ( $controller &&
$controller instanceof CONTROLLER ||
get_class( $controller ) !== static::class ) ?
$controller :
null;
}
return $controller;
}
public function getTemplateArgs( Settings $settings ): array {
$modelFields = $this->_model ?
$this->_model->getFields() :
[];
$templateArgs = [];
foreach ( $modelFields as $modelFieldName => $modelFieldValue ) {
$templateFieldName = ltrim( $modelFieldName, '_' );
if ( ! $modelFieldValue instanceof MODEL ) {
$templateArgs[ $templateFieldName ] = $modelFieldValue;
continue;
}
$modelFieldController = $this->_getControllerField( $modelFieldName );
$modelFieldArgs = [];
$externalFieldArgs = $this->__external[ $modelFieldName ] ?? [];
if ( $modelFieldController ) {
$modelFieldController->setModel( $modelFieldValue );
$modelFieldArgs = $modelFieldController->getTemplateArgs( $settings );
}
$templateArgs[ $templateFieldName ] = HELPER::ArrayMergeRecursive( $modelFieldArgs, $externalFieldArgs );
}
// using static for children support
return array_merge( $templateArgs, [
self::TEMPLATE_KEY__TEMPLATE => static::GetPathToTwigTemplate( $settings ),
self::TEMPLATE_KEY__IS_LOADED => ( $this->_model && $this->_model->isLoaded() ),
] );
}
public function getDependencies( string $sourceClass = '' ): array {
$dependencyClasses = [];
$controllerFields = $this->_getFieldsInfo();
foreach ( $controllerFields as $fieldName => $fieldType ) {
$dependencyController = $this->_getControllerField( $fieldName );
if ( ! $dependencyController ) {
continue;
}
$dependencyClass = get_class( $dependencyController );
// 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 = $dependencyController->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 declare a target model class and provide an IDE support
public function getModel(): ?MODEL {
return $this->_model;
}
private function _autoInitModel() {
if ( $this->_model ) {
return;
}
$modelClass = static::GetModelClass();
try {
$this->_model = $modelClass ?
new $modelClass() :
$this->_model;
} catch ( Exception $ex ) {
$this->_model = null;
}
}
}
CONTROLLERTest.php
<?php
declare( strict_types=1 );
namespace LightSource\FrontBlocksFramework\Tests\unit;
use Codeception\Test\Unit;
use LightSource\FrontBlocksFramework\{
CONTROLLER,
MODEL,
Settings
};
class CONTROLLERTest extends Unit {
private function _getModel( array $fields, bool $isLoaded = false ): MODEL {
return new class ( $fields, $isLoaded ) extends MODEL {
private array $_fields;
public function __construct( array $fields, bool $isLoaded ) {
parent::__construct();
$this->_fields = $fields;
if ( $isLoaded ) {
$this->_load();
}
}
public function getFields(): array {
return $this->_fields;
}
};
}
private function _getController( ?MODEL $model ): CONTROLLER {
return new class ( $model ) extends CONTROLLER {
public function __construct( ?MODEL $model = null ) {
parent::__construct( $model );
}
};
}
private function _getTemplateArgsWithoutAdditional( array $templateArgs ) {
$templateArgs = array_diff_key( $templateArgs, [
CONTROLLER::TEMPLATE_KEY__TEMPLATE => '',
CONTROLLER::TEMPLATE_KEY__IS_LOADED => '',
] );
foreach ( $templateArgs as $templateKey => $templateValue ) {
if ( ! is_array( $templateValue ) ) {
continue;
}
$templateArgs[ $templateKey ] = $this->_getTemplateArgsWithoutAdditional( $templateValue );
}
return $templateArgs;
}
////
public function testGetResourceInfoWithoutCamelCaseInBlockName() {
$settings = new Settings();
$settings->setControllerSuffix( '_C' );
$settings->setBlocksDirNamespace( 'Namespace' );
$this->assertEquals( [
'resourceName' => 'block',
'relativePath' => 'Block',
'relativeResourcePath' => 'Block/block',
], CONTROLLER::GetResourceInfo( $settings, 'Namespace\\Block\\Block_C' ) );
}
public function testGetResourceInfoWithCamelCaseInBlockName() {
$settings = new Settings();
$settings->setControllerSuffix( '_C' );
$settings->setBlocksDirNamespace( 'Namespace' );
$this->assertEquals( [
'resourceName' => 'block-name',
'relativePath' => 'BlockName',
'relativeResourcePath' => 'BlockName/block-name',
], CONTROLLER::GetResourceInfo( $settings, 'Namespace\\BlockName\\BlockName_C' ) );
}
public function testGetResourceInfoWithoutCamelCaseInTheme() {
$settings = new Settings();
$settings->setControllerSuffix( '_C' );
$settings->setBlocksDirNamespace( 'Namespace' );
$this->assertEquals( [
'resourceName' => 'block--theme--main',
'relativePath' => 'Block/Theme/Main',
'relativeResourcePath' => 'Block/Theme/Main/block--theme--main',
], CONTROLLER::GetResourceInfo( $settings, 'Namespace\\Block\\Theme\\Main\\Block_Theme_Main_C' ) );
}
public function testGetResourceInfoWithCamelCaseInTheme() {
$settings = new Settings();
$settings->setControllerSuffix( '_C' );
$settings->setBlocksDirNamespace( 'Namespace' );
$this->assertEquals( [
'resourceName' => 'block--theme--just-main',
'relativePath' => 'Block/Theme/JustMain',
'relativeResourcePath' => 'Block/Theme/JustMain/block--theme--just-main',
], CONTROLLER::GetResourceInfo( $settings, 'Namespace\\Block\\Theme\\JustMain\\Block_Theme_JustMain_C' ) );
}
////
public function testGetTemplateArgsWhenModelContainsBuiltInTypes() {
$settings = new Settings();
$model = $this->_getModel( [
'stringVariable' => 'just string',
] );
$controller = $this->_getController( $model );
$this->assertEquals( [
'stringVariable' => 'just string',
], $this->_getTemplateArgsWithoutAdditional( $controller->getTemplateArgs( $settings ) ) );
}
public function testGetTemplateArgsWhenModelContainsAnotherModel() {
$settings = new Settings();
$modelA = $this->_getModel( [
'_modelA' => 'just string from model a',
] );
$modelB = $this->_getModel( [
'_modelA' => $modelA,
'_modelB' => 'just string from model b',
] );
$controllerForModelA = $this->_getController( null );
$controllerForModelB = new class ( $modelB, $controllerForModelA ) extends CONTROLLER {
protected $_modelA;
public function __construct( ?MODEL $model = null, $controllerForModelA ) {
parent::__construct( $model );
$this->_modelA = $controllerForModelA;
}
};
$this->assertEquals( [
'modelA' => [
'modelA' => 'just string from model a',
],
'modelB' => 'just string from model b',
], $this->_getTemplateArgsWithoutAdditional( $controllerForModelB->getTemplateArgs( $settings ) ) );
}
public function testGetTemplateArgsWhenControllerContainsExternalArgs() {
$settings = new Settings();
$modelA = $this->_getModel( [
'_additionalField' => '',
'_modelA' => 'just string from model a',
] );
$modelB = $this->_getModel( [
'_modelA' => $modelA,
'_modelB' => 'just string from model b',
] );
$controllerForModelA = $this->_getController( null );
$controllerForModelB = new class ( $modelB, $controllerForModelA ) extends CONTROLLER {
protected $_modelA;
public function __construct( ?MODEL $model = null, $controllerForModelA ) {
parent::__construct( $model );
$this->_modelA = $controllerForModelA;
$this->__external['_modelA'] = [
'additionalField' => 'additionalValue',
];
}
};
$this->assertEquals( [
'modelA' => [
'additionalField' => 'additionalValue',
'modelA' => 'just string from model a',
],
'modelB' => 'just string from model b',
], $this->_getTemplateArgsWithoutAdditional( $controllerForModelB->getTemplateArgs( $settings ) ) );
}
public function testGetTemplateArgsContainsAdditionalFields() {
$settings = new Settings();
$model = $this->_getModel( [] );
$controller = $this->_getController( $model );
$this->assertEquals( [
CONTROLLER::TEMPLATE_KEY__TEMPLATE,
CONTROLLER::TEMPLATE_KEY__IS_LOADED,
], array_keys( $controller->getTemplateArgs( $settings ) ) );
}
public function testGetTemplateArgsWhenAdditionalIsLoadedIsFalse() {
$settings = new Settings();
$model = $this->_getModel( [] );
$controller = $this->_getController( $model );
$actual = array_intersect_key( $controller->getTemplateArgs( $settings ), [ CONTROLLER::TEMPLATE_KEY__IS_LOADED => '', ] );
$this->assertEquals( [ CONTROLLER::TEMPLATE_KEY__IS_LOADED => false, ], $actual );
}
public function testGetTemplateArgsWhenAdditionalIsLoadedIsTrue() {
$settings = new Settings();
$model = $this->_getModel( [], true );
$controller = $this->_getController( $model );
$actual = array_intersect_key( $controller->getTemplateArgs( $settings ), [ CONTROLLER::TEMPLATE_KEY__IS_LOADED => '', ] );
$this->assertEquals( [ CONTROLLER::TEMPLATE_KEY__IS_LOADED => true, ], $actual );
}
public function testGetTemplateArgsAdditionalTemplateIsRight() {
$settings = new Settings();
$model = $this->_getModel( [] );
$controller = $this->_getController( $model );
$actual = array_intersect_key( $controller->getTemplateArgs( $settings ), [ CONTROLLER::TEMPLATE_KEY__TEMPLATE => '', ] );
$this->assertEquals( [
CONTROLLER::TEMPLATE_KEY__TEMPLATE => $controller::GetPathToTwigTemplate( $settings ),
], $actual );
}
////
public function testGetDependencies() {
$controllerA = $this->_getController( null );
$controllerB = new class ( null, $controllerA ) extends CONTROLLER {
protected $_controllerA;
public function __construct( ?MODEL $model = null, $controllerA ) {
parent::__construct( $model );
$this->_controllerA = $controllerA;
}
};
$this->assertEquals( [
get_class( $controllerA ),
], $controllerB->getDependencies() );
}
public function testGetDependenciesWithSubDependencies() {
$controllerA = new class extends CONTROLLER {
public function getDependencies( string $sourceClass = '' ): array {
return [
'A',
];
}
};
$controllerB = new class ( null, $controllerA ) extends CONTROLLER {
protected $_controllerA;
public function __construct( ?MODEL $model = null, $controllerA ) {
parent::__construct( $model );
$this->_controllerA = $controllerA;
}
};
$this->assertEquals( [
'A',
get_class( $controllerA ),
], $controllerB->getDependencies() );
}
public function testGetDependenciesWithSubDependenciesRecursively() {
$controllerA = new class extends CONTROLLER {
public function getDependencies( string $sourceClass = '' ): array {
return [
'A',
];
}
};
$controllerB = new class ( null, $controllerA ) extends CONTROLLER {
protected $_controllerA;
public function __construct( ?MODEL $model = null, $controllerA ) {
parent::__construct( $model );
$this->_controllerA = $controllerA;
}
};
$controllerC = new class ( null, $controllerB ) extends CONTROLLER {
protected $_controllerB;
public function __construct( ?MODEL $model = null, $controllerB ) {
parent::__construct( $model );
$this->_controllerB = $controllerB;
}
};
$this->assertEquals( [
'A',
get_class( $controllerA ),
get_class( $controllerB ),
], $controllerC->getDependencies() );
}
public function testGetDependenciesWithSubDependenciesInOrderWhenSubBeforeMainDependency() {
$controllerA = new class extends CONTROLLER {
public function getDependencies( string $sourceClass = '' ): array {
return [
'A',
];
}
};
$controllerB = new class ( null, $controllerA ) extends CONTROLLER {
protected $_controllerA;
public function __construct( ?MODEL $model = null, $controllerA ) {
parent::__construct( $model );
$this->_controllerA = $controllerA;
}
};
$this->assertEquals( [
'A',
get_class( $controllerA ),
], $controllerB->getDependencies() );
}
public function testGetDependenciesWithSubDependenciesWhenBlocksAreDependentFromEachOther() {
$controllerA = new class extends CONTROLLER {
protected $_controllerB;
public function setControllerB( $controllerB ) {
$this->_controllerB = $controllerB;
}
};
$controllerB = new class ( null, $controllerA ) extends CONTROLLER {
protected $_controllerA;
public function __construct( ?MODEL $model = null, $controllerA ) {
parent::__construct( $model );
$this->_controllerA = $controllerA;
}
};
$controllerA->setControllerB( $controllerB );
$this->assertEquals( [
get_class( $controllerA ),
], $controllerB->getDependencies() );
}
public function testGetDependenciesWithoutDuplicatesWhenSeveralWithOneType() {
$controllerA = $this->_getController( null );
$controllerB = new class ( null, $controllerA ) extends CONTROLLER {
protected $_controllerA;
protected $_controllerAA;
protected $_controllerAAA;
public function __construct( ?MODEL $model = null, $controllerA ) {
parent::__construct( $model );
$this->_controllerA = $controllerA;
$this->_controllerAA = $controllerA;
$this->_controllerAAA = $controllerA;
}
};
$this->assertEquals( [
get_class( $controllerA ),
], $controllerB->getDependencies() );
}
////
public function testAutoInitModel() {
$modelClass = str_replace( [ '::', '\\' ], '_', __METHOD__ );
$controllerClass = $modelClass . Settings::$ControllerSuffix;
eval( 'class ' . $modelClass . ' extends ' . MODEL::class . ' {}' );
eval( 'class ' . $controllerClass . ' extends ' . CONTROLLER::class . ' {}' );
$controller = new $controllerClass();
$actualModelClass = $controller->getModel() ?
get_class( $controller->getModel() ) :
'';
$this->assertEquals( $modelClass, $actualModelClass );
}
public function testAutoInitModelWhenModelHasWrongClass() {
$modelClass = str_replace( [ '::', '\\' ], '_', __METHOD__ );
$controllerClass = $modelClass . Settings::$ControllerSuffix;
eval( 'class ' . $modelClass . ' {}' );
eval( 'class ' . $controllerClass . ' extends ' . CONTROLLER::class . ' {}' );
$controller = new $controllerClass();
$this->assertEquals( null, $controller->getModel() );
}
}
Settings
,
Settings.php
<?php
declare( strict_types=1 );
namespace LightSource\FrontBlocksFramework;
class Settings {
public static string $ControllerSuffix = '_C';
private string $_blocksDirPath;
private string $_blocksDirNamespace;
private array $_twigArgs;
private string $_twigExtension;
private $_errorCallback;
public function __construct() {
$this->_blocksDirPath = '';
$this->_blocksDirNamespace = '';
$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 setBlocksDirPath( string $blocksDirPath ): void {
$this->_blocksDirPath = $blocksDirPath;
}
public function setBlocksDirNamespace( string $blocksDirNamespace ): void {
$this->_blocksDirNamespace = $blocksDirNamespace;
}
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 setControllerSuffix( string $controllerSuffix ): void {
$this->_controllerSuffix = $controllerSuffix;
}
public function getBlocksDirPath(): string {
return $this->_blocksDirPath;
}
public function getBlocksDirNamespace(): string {
return $this->_blocksDirNamespace;
}
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, ] );
}
}
Twig
, twig _include ( _isLoaded _template CONROLLER->getTemplateArgs ) _merge ( , ).
Twig.php
<?php
declare( strict_types=1 );
namespace LightSource\FrontBlocksFramework;
use Exception;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
use Twig\Loader\LoaderInterface;
use Twig\TwigFilter;
use Twig\TwigFunction;
class Twig {
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();
}
// 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[ CONTROLLER::TEMPLATE_KEY__IS_LOADED ] ?
$this->render( $block[ CONTROLLER::TEMPLATE_KEY__TEMPLATE ], $block ) :
'';
} ) );
}
private function _init(): void {
try {
$this->_twigLoader = ! $this->_twigLoader ?
new FilesystemLoader( $this->_settings->getBlocksDirPath() ) :
$this->_twigLoader;
$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 $template, array $args = [], bool $isPrint = false ): string {
$html = '';
// twig isn't loaded
if ( is_null( $this->_twigEnvironment ) ) {
return $html;
}
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( $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;
}
}
TwigTest.php
<?php
declare( strict_types=1 );
namespace LightSource\FrontBlocksFramework\Tests\unit;
use Codeception\Test\Unit;
use Exception;
use LightSource\FrontBlocksFramework\CONTROLLER;
use LightSource\FrontBlocksFramework\Settings;
use LightSource\FrontBlocksFramework\Twig;
use Twig\Loader\ArrayLoader;
class TwigTest extends Unit {
private function _renderBlock( array $blocks, string $renderBlock, array $renderArgs = [] ): string {
$twigLoader = new ArrayLoader( $blocks );
$settings = new Settings();
$twig = new Twig( $settings, $twigLoader );
$content = '';
try {
$content = $twig->render( $renderBlock, $renderArgs );
} catch ( Exception $ex ) {
$this->fail( 'Twig render exception, ' . $ex->getMessage() );
}
return $content;
}
public function testExtendTwigIncludeFunctionWhenBlockIsLoaded() {
$blocks = [
'block-a.twig' => '{{ _include(blockB) }}',
'block-b.twig' => 'block-b content',
];
$renderBlock = 'block-a.twig';
$renderArgs = [
'blockB' => [
CONTROLLER::TEMPLATE_KEY__TEMPLATE => 'block-b.twig',
CONTROLLER::TEMPLATE_KEY__IS_LOADED => true,
],
];
$this->assertEquals( 'block-b content', $this->_renderBlock( $blocks, $renderBlock, $renderArgs ) );
}
public function testExtendTwigIncludeFunctionWhenBlockNotLoaded() {
$blocks = [
'block-a.twig' => '{{ _include(blockB) }}',
'block-b.twig' => 'block-b content',
];
$renderBlock = 'block-a.twig';
$renderArgs = [
'blockB' => [
CONTROLLER::TEMPLATE_KEY__TEMPLATE => 'block-b.twig',
CONTROLLER::TEMPLATE_KEY__IS_LOADED => false,
],
];
$this->assertEquals( '', $this->_renderBlock( $blocks, $renderBlock, $renderArgs ) );
}
public function testExtendTwigIncludeFunctionWhenArgsPassed() {
$blocks = [
'block-a.twig' => '{{ _include(blockB, {classes:["test-class",],}) }}',
'block-b.twig' => '{{ classes|join(" ") }}',
];
$renderBlock = 'block-a.twig';
$renderArgs = [
'blockB' => [
CONTROLLER::TEMPLATE_KEY__TEMPLATE => 'block-b.twig',
CONTROLLER::TEMPLATE_KEY__IS_LOADED => true,
'classes' => [ 'own-class', ],
],
];
$this->assertEquals( 'own-class test-class', $this->_renderBlock( $blocks, $renderBlock, $renderArgs ) );
}
public function testExtendTwigMergeFilter() {
$blocks = [
'block-a.twig' => '{{ {"array":["a",],}|_merge({"array":["b",],}).array|join(" ") }}',
];
$renderBlock = 'block-a.twig';
$renderArgs = [];
$this->assertEquals( 'a b', $this->_renderBlock( $blocks, $renderBlock, $renderArgs ) );
}
}
Blocks
.
LoadAll OnLoad ( , ).
renderBlock , twig CONROLLER->getTemplateArgs . , css js.
getUsedResources CONTROLLER::GetResourceInfo css js , , .. ./
Blocks.php
<?php
declare( strict_types=1 );
namespace LightSource\FrontBlocksFramework;
class Blocks {
private array $_loadedControllerClasses;
private array $_usedControllerClasses;
private Settings $_settings;
private Twig $_twig;
public function __construct( Settings $settings ) {
$this->_loadedControllerClasses = [];
$this->_usedControllerClasses = [];
$this->_settings = $settings;
$this->_twig = new Twig( $settings );
}
final public function getLoadedControllerClasses(): array {
return $this->_loadedControllerClasses;
}
final public function getUsedControllerClasses(): array {
return $this->_usedControllerClasses;
}
final public function getSettings(): Settings {
return $this->_settings;
}
final public function getTwig(): Twig {
return $this->_twig;
}
final public function getUsedResources( string $extension, bool $isIncludeSource = false ): string {
$resourcesContent = '';
foreach ( $this->_usedControllerClasses as $usedControllerClass ) {
$getResourcesInfoCallback = [ $usedControllerClass, 'GetResourceInfo' ];
if ( ! is_callable( $getResourcesInfoCallback ) ) {
$this->_settings->callErrorCallback( [
'message' => "Controller class doesn't exist",
'class' => $usedControllerClass,
] );
continue;
}
$resourceInfo = call_user_func_array( $getResourcesInfoCallback, [
$this->_settings,
] );
$pathToResourceFile = $this->_settings->getBlocksDirPath() . DIRECTORY_SEPARATOR . $resourceInfo['relativeResourcePath'] . $extension;
if ( ! is_file( $pathToResourceFile ) ) {
continue;
}
$resourcesContent .= $isIncludeSource ?
"\n/* " . $resourceInfo['resourceName'] . " */\n" :
'';
$resourcesContent .= file_get_contents( $pathToResourceFile );
}
return $resourcesContent;
}
private function _loadController( string $phpClass, array $debugArgs ): bool {
$isLoaded = false;
if ( ! class_exists( $phpClass, true ) ||
! is_subclass_of( $phpClass, CONTROLLER::class ) ) {
$this->_settings->callErrorCallback( [
'message' => "Class doesn't exist or doesn't child",
'args' => $debugArgs,
] );
return $isLoaded;
}
call_user_func( [ $phpClass, 'OnLoad' ] );
return true;
}
private function _loadControllers( string $directory, string $namespace, array $controllerFileNames ): void {
foreach ( $controllerFileNames as $controllerFileName ) {
$phpFile = implode( DIRECTORY_SEPARATOR, [ $directory, $controllerFileName ] );
$phpClass = implode( '\\', [ $namespace, str_replace( '.php', '', $controllerFileName ), ] );
$debugArgs = [
'directory' => $directory,
'namespace' => $namespace,
'phpFile' => $phpFile,
'phpClass' => $phpClass,
];
if ( ! $this->_loadController( $phpClass, $debugArgs ) ) {
continue;
}
$this->_loadedControllerClasses[] = $phpClass;
}
}
private function _loadDirectory( string $directory, string $namespace ): void {
// exclude ., ..
$fs = array_diff( scandir( $directory ), [ '.', '..' ] );
$controllerFilePreg = '/' . Settings::$ControllerSuffix . '.php$/';
$controllerFileNames = HELPER::ArrayFilter( $fs, function ( $f ) use ( $controllerFilePreg ) {
return ( 1 === preg_match( $controllerFilePreg, $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->_loadControllers( $directory, $namespace, $controllerFileNames );
}
final public function loadAll(): void {
$directory = $this->_settings->getBlocksDirPath();
$namespace = $this->_settings->getBlocksDirNamespace();
$this->_loadDirectory( $directory, $namespace );
}
final public function renderBlock( CONTROLLER $controller, array $args = [], bool $isPrint = false ): string {
$dependencies = array_merge( $controller->getDependencies(), [ get_class( $controller ), ] );
$newDependencies = array_diff( $dependencies, $this->_usedControllerClasses );
$this->_usedControllerClasses = array_merge( $this->_usedControllerClasses, $newDependencies );
$templateArgs = $controller->getTemplateArgs( $this->_settings );
$templateArgs = HELPER::ArrayMergeRecursive( $templateArgs, $args );
return $this->_twig->render( $templateArgs[ CONTROLLER::TEMPLATE_KEY__TEMPLATE ], $templateArgs, $isPrint );
}
}
BlocksTest.php
<?php
declare( strict_types=1 );
namespace LightSource\FrontBlocksFramework\Tests\unit;
use Codeception\Test\Unit;
use Exception;
use LightSource\FrontBlocksFramework\Blocks;
use LightSource\FrontBlocksFramework\CONTROLLER;
use LightSource\FrontBlocksFramework\MODEL;
use LightSource\FrontBlocksFramework\Settings;
use LightSource\FrontBlocksFramework\Twig;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamDirectory;
class BlocksTest extends Unit {
private function _getBlocks( string $namespace, vfsStreamDirectory $rootDirectory, array $structure, array $usedControllerClasses = [] ): ?Blocks {
vfsStream::create( $structure, $rootDirectory );
$settings = new Settings();
$settings->setBlocksDirNamespace( $namespace );
$settings->setBlocksDirPath( $rootDirectory->url() );
$twig = $this->make( Twig::class, [
'render' => function ( string $template, array $args = [], bool $isPrint = false ): string {
return '';
},
] );
try {
$blocks = $this->make( Blocks::class, [
'_loadedControllerClasses' => [],
'_usedControllerClasses' => $usedControllerClasses,
'_twig' => $twig,
'_settings' => $settings,
] );
} catch ( Exception $ex ) {
$this->fail( "Can't make Blocks stub, " . $ex->getMessage() );
}
$blocks->loadAll();
return $blocks;
}
// get a unique namespace depending on a test method to prevent affect other tests
private function _getUniqueControllerNamespaceWithAutoloader( string $methodConstant, vfsStreamDirectory $rootDirectory ): string {
$namespace = str_replace( '::', '_', $methodConstant );
spl_autoload_register( function ( $class ) use ( $rootDirectory, $namespace ) {
$targetNamespace = $namespace . '\\';
if ( 0 !== strpos( $class, $targetNamespace ) ) {
return;
}
$relativePathToFile = str_replace( $targetNamespace, '', $class );
$relativePathToFile = str_replace( '\\', '/', $relativePathToFile );
$absPathToFile = $rootDirectory->url() . DIRECTORY_SEPARATOR . $relativePathToFile . '.php';
include_once $absPathToFile;
} );
return $namespace;
}
// get a unique directory name depending on a test method to prevent affect other tests
private function _getUniqueDirectory( string $methodConstant ): vfsStreamDirectory {
$dirName = str_replace( [ ':', '\\' ], '_', $methodConstant );
return vfsStream::setup( $dirName );
}
private function _getControllerClassFile( string $namespace, string $class ): string {
$vendorControllerClass = '\LightSource\FrontBlocksFramework\CONTROLLER';
return '<?php namespace ' . $namespace . '; class ' . $class . ' extends ' . $vendorControllerClass . ' {}';
}
private function _getController( array $dependencies = [] ) {
return new class ( null, $dependencies ) extends CONTROLLER {
private array $_dependencies;
public function __construct( ?MODEL $model = null, array $dependencies ) {
parent::__construct( $model );
$this->_dependencies = $dependencies;
}
function getDependencies( string $sourceClass = '' ): array {
return $this->_dependencies;
}
function getTemplateArgs( Settings $settings ): array {
return [
CONTROLLER::TEMPLATE_KEY__TEMPLATE => '',
];
}
};
}
////
public function testLoadAllControllersWithPrefix() {
// fixme
$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );
$namespace = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );
$blocks = $this->_getBlocks( $namespace, $rootDirectory, [
'Block' => [
'Block_C.php' => $this->_getControllerClassFile( "{$namespace}\Block", 'Block_C' ),
],
] );
$this->assertEquals( [
"{$namespace}\Block\Block_C",
], $blocks->getLoadedControllerClasses() );
}
public function testLoadAllIgnoreControllersWithoutPrefix() {
$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );
$namespace = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );
$blocks = $this->_getBlocks( $namespace, $rootDirectory, [
'Block' => [
'Block.php' => $this->_getControllerClassFile( "{$namespace}\Block", 'Block' ),
],
] );
$this->assertEquals( [], $blocks->getLoadedControllerClasses() );
}
public function testLoadAllIgnoreWrongControllers() {
$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );
$namespace = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );
$blocks = $this->_getBlocks( $namespace, $rootDirectory, [
'Block' => [
'Block_C.php' => $this->_getControllerClassFile( "{$namespace}\Block", 'WrongBlock_C' ),
],
] );
$this->assertEquals( [], $blocks->getLoadedControllerClasses() );
}
////
public function testRenderBlockAddsControllerToUsedList() {
$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );
$namespace = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );
$blocks = $this->_getBlocks( $namespace, $rootDirectory, [] );
$controller = $this->_getController();
$blocks->renderBlock( $controller );
$this->assertEquals( [
get_class( $controller ),
], $blocks->getUsedControllerClasses() );
}
public function testRenderBlockAddsControllerDependenciesToUsedList() {
$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );
$namespace = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );
$blocks = $this->_getBlocks( $namespace, $rootDirectory, [] );
$controller = $this->_getController( [ 'A', ] );
$blocks->renderBlock( $controller );
$this->assertEquals( [
'A',
get_class( $controller ),
], $blocks->getUsedControllerClasses() );
}
public function testRenderBlockAddsDependenciesBeforeControllerToUsedList() {
$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );
$namespace = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );
$blocks = $this->_getBlocks( $namespace, $rootDirectory, [] );
$controller = $this->_getController( [ 'A', ] );
$blocks->renderBlock( $controller );
$this->assertEquals( [
'A',
get_class( $controller ),
], $blocks->getUsedControllerClasses() );
}
public function testRenderBlockIgnoreDuplicateControllerWhenAddsToUsedList() {
$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );
$namespace = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );
$blocks = $this->_getBlocks( $namespace, $rootDirectory, [] );
$controllerA = $this->_getController();
$blocks->renderBlock( $controllerA );
$blocks->renderBlock( $controllerA );
$this->assertEquals( [
get_class( $controllerA ),
], $blocks->getUsedControllerClasses() );
}
public function testRenderBlockIgnoreDuplicateControllerDependenciesWhenAddsToUsedList() {
$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );
$namespace = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );
$blocks = $this->_getBlocks( $namespace, $rootDirectory, [] );
$controllerA = $this->_getController( [ 'A', ] );
$controllerB = $this->_getController( [ 'A', ] );
$blocks->renderBlock( $controllerA );
$blocks->renderBlock( $controllerB );
$this->assertEquals( [
'A',
get_class( $controllerA ),// $controllerB has the same class
], $blocks->getUsedControllerClasses() );
}
////
public function testGetUsedResourcesWhenBlockWithResources() {
$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );
$namespace = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );
$blocks = $this->_getBlocks( $namespace, $rootDirectory, [
'Block' => [
'Block_C.php' => $this->_getControllerClassFile( "{$namespace}\Block", 'Block_C' ),
'block.css' => 'just css code',
],
], [
"{$namespace}\Block\Block_C",
] );
$this->assertEquals( 'just css code',
$blocks->getUsedResources( '.css', false ) );
}
public function testGetUsedResourcesWhenBlockWithoutResources() {
$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );
$namespace = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );
$blocks = $this->_getBlocks( $namespace, $rootDirectory, [
'Block' => [
'Block_C.php' => $this->_getControllerClassFile( "{$namespace}\Block", 'Block_C' ),
],
], [
"{$namespace}\Block\Block_C",
] );
$this->assertEquals( '',
$blocks->getUsedResources( '.css', false ) );
}
public function testGetUsedResourcesWhenSeveralBlocks() {
$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );
$namespace = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );
$blocks = $this->_getBlocks( $namespace, $rootDirectory, [
'BlockA' => [
'BlockA_C.php' => $this->_getControllerClassFile( "{$namespace}\BlockA", 'BlockA_C' ),
'block-a.css' => 'css code for a',
],
'BlockB' => [
'BlockB_C.php' => $this->_getControllerClassFile( "{$namespace}\BlockB", 'BlockB_C' ),
'block-b.css' => 'css code for b',
],
], [
"{$namespace}\BlockA\BlockA_C",
"{$namespace}\BlockB\BlockB_C",
] );
$this->assertEquals( 'css code for acss code for b',
$blocks->getUsedResources( '.css', false ) );
}
public function testGetUsedResourcesWithIncludedSource() {
$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );
$namespace = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );
$blocks = $this->_getBlocks( $namespace, $rootDirectory, [
'SimpleBlock' => [
'SimpleBlock_C.php' => $this->_getControllerClassFile( "{$namespace}\SimpleBlock", 'SimpleBlock_C' ),
'simple-block.css' => 'css code',
],
], [
"{$namespace}\SimpleBlock\SimpleBlock_C",
] );
$this->assertEquals( "\n/* simple-block */\ncss code",
$blocks->getUsedResources( '.css', true ) );
}
}
, - . composer , .. .
, css , - scss/webpack .
, BlockA BlockC , BlockB BlockC.
BlockA
BlockA.php
<?php
namespace LightSource\FrontBlocksExample\BlockA;
use LightSource\FrontBlocksFramework\MODEL;
class BlockA extends MODEL {
protected string $_name;
public function load() {
parent::_load();
$this->_name = 'I\'m BlockA';
}
}
BlockA_C.php
/sp
<?php
namespace LightSource\FrontBlocksExample\BlockA;
use LightSource\FrontBlocksFramework\Blocks;
use LightSource\FrontBlocksFramework\CONTROLLER;
class BlockA_C extends CONTROLLER {
public function getModel(): ?BlockA {
/** @noinspection PhpIncompatibleReturnTypeInspection */
return parent::getModel();
}
}
block-a.twig
/
<div class="block-a">
{{ name }}
</div>
block-a.css
Bl
.block-a {
color: green;
border:1px solid green;
padding: 10px;
}
BlockB
BlockB.php
<?php
namespace LightSource\FrontBlocksExample\BlockB;
use LightSource\FrontBlocksExample\BlockC\BlockC;
use LightSource\FrontBlocksFramework\MODEL;
class BlockB extends MODEL {
protected string $_name;
protected BlockC $_blockC;
public function __construct() {
parent::__construct();
$this->_blockC = new BlockC();
}
public function load() {
parent::_load();
$this->_name = 'I\'m BlockB, I contain another block';
$this->_blockC->load();
}
}
BlockB_C.php
<?php
namespace LightSource\FrontBlocksExample\BlockB;
use LightSource\FrontBlocksExample\BlockC\BlockC_C;
use LightSource\FrontBlocksFramework\CONTROLLER;
class BlockB_C extends CONTROLLER {
protected BlockC_C $_blockC;
public function getModel(): ?BlockB {
/** @noinspection PhpIncompatibleReturnTypeInspection */
return parent::getModel();
}
}
block-b.twig
<div class="block-b">
<p class="block-b__name">{{ name }}</p>
{{ _include(blockC) }}
</div>
block-b.css
Blo
.block-b {
color: orange;
border: 1px solid orange;
padding: 10px;
}
.block-b__name {
margin: 0 0 10px;
line-height: 1.5;
}
BlocksC
BlockC.php
<?php
namespace LightSource\FrontBlocksExample\BlockC;
use LightSource\FrontBlocksFramework\MODEL;
class BlockC extends MODEL {
protected string $_name;
public function load() {
parent::_load();
$this->_name = 'I\'m BlockC';
}
}
BlockC_C.php
/
<?php
namespace LightSource\FrontBlocksExample\BlockC;
use LightSource\FrontBlocksFramework\CONTROLLER;
class BlockC_C extends CONTROLLER {
public function getModel(): ?BlockC {
/** @noinspection PhpIncompatibleReturnTypeInspection */
return parent::getModel();
}
}
block-c.twig
<div class="block-c">
{{ name }}
</div>
block-c.css
.block-c {
color: black;
border: 1px solid black;
padding: 10px;
}
, html , css
example.php
<?php
use LightSource\FrontBlocksExample\{
BlockA\BlockA_C,
BlockB\BlockB_C,
};
use LightSource\FrontBlocksFramework\{
Blocks,
Settings
};
require_once __DIR__ . '/vendors/vendor/autoload.php';
//// settings
$settings = new Settings();
$settings->setBlocksDirNamespace( 'LightSource\FrontBlocksExample' );
$settings->setBlocksDirPath( __DIR__ . '/Blocks' );
$settings->setErrorCallback( function ( array $errors ) {
// todo log or any other actions
echo '<pre>' . print_r( $errors, true ) . '</pre>';
}
);
$blocks = new Blocks( $settings );
//// usage
$blockA_Controller = new BlockA_C();
$blockA_Controller->getModel()->load();
$blockB_Controller = new BlockB_C();
$blockB_Controller->getModel()->load();
$content = $blocks->renderBlock( $blockA_Controller );
$content .= $blocks->renderBlock( $blockB_Controller );
$css = $blocks->getUsedResources( '.css', true );
//// html
?>
<html>
<head>
<title>Example</title>
<style>
<?= $css ?>
</style>
<style>
.block-b {
margin-top: 10px;
}
</style>
</head>
<body>
<?= $content ?>
</body>
</html>
example.png
, , - . – .
, .
:
WordPress ( , ajax ).