Bloques de front-end modulares: escribir su propio mini framework

Buen día, queridos lectores de Habr. Cada año en el desarrollo web hay más y más soluciones diversas que utilizan un enfoque modular y simplifican el desarrollo y la edición de código. En este artículo, le ofrezco mi visión de lo que pueden ser los bloques de front-end reutilizables (para proyectos con un backend php) y le sugiero que siga todos los pasos desde la idea hasta la implementación conmigo. ¿Suena interesante? Entonces bienvenido a cat.





Prefacio

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





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





La motivación para escribir este artículo fue el deseo de compartir una solución para organizar bloques modulares, así como el deseo del lector de Habr de escribir su propio artículo, que es similar al deseo de crear su propio paquete, que a veces surge para principiantes. para usar composer o paquetes npm listos para usar.





Como puede concluir del texto anterior, este es mi primer artículo sobre Habré, por lo tanto, no tire tomates, no juzgue estrictamente.





Formulación del problema

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





html php, , php. , , , css-in-js bem-json - , .. html, css js .





-:









  • ()









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





.









  1. :





    1. (css/js/twig)





    2. ( twig )





    3. ( , twig )





  2. : Settings ( , ..), Twig





  3. Blocks





    , :





    1. (Settings, Twig)









    2. , (css/js)





    3. ( , , )





, – :





  • 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

, , - . – .





, .





:













scss js (webpack )





WordPress ( , ajax ).








All Articles