- Base is done; most of the language's components are now working!

- Implemented OPCACHE caching
- Added View() token logic
- New FileFetcher added to gather files from given view path
This commit is contained in:
Dave M. 2019-11-01 14:46:01 -04:00
parent bcd49884fc
commit 6701a1cd59
16 changed files with 323 additions and 69 deletions

View File

@ -13,23 +13,23 @@ class Builder
$this->templatePath = $templatePath; $this->templatePath = $templatePath;
} }
public function build(Compiler\Context &$context, string $compiledSource) : array public function build(Compiler\Context &$context, string $compiledSource) : Compiler\Context
{ {
$className = static::generateClassName($compiledSource); $context->className = static::generateClassName($context->viewPath);
$replace = [ $replace = [
'%NAMESPACE%' => $context->namespace, '%NAMESPACE%' => $context->namespace,
'%USE%' => $context->renderUses(), '%USE%' => $context->renderUses(),
'%CLASSNAME%' => $className, '%CLASSNAME%' => $context->className,
'%EXTENDS%' => $context->extendFrom ? static::generateClassUID($context->extendFrom) : '', '%EXTENDS%' => $context->extendFrom ? "extends " . static::TEMPLATE_CLASSNAME_PREFIX . static::generateClassUID($context->extendFrom) : '',
'%EXTENDS_TEMPLATE%' => $context->extendFrom,
'%CONTENT%' => $compiledSource, '%CONTENT%' => $compiledSource,
'%PARENT_OUTPUT%' => $context->extendFrom ? "parent::output(\$variablesList);" : "",
]; ];
$classContent = str_replace(array_keys($replace), array_values($replace), file_get_contents($this->templatePath)); $context->compiledSource = str_replace(array_keys($replace), array_values($replace), file_get_contents($this->templatePath));
return [ return $context;
$context->namespace, $className, $classContent,
];
} }
public static function generateClassName(string $filepath) : string public static function generateClassName(string $filepath) : string

View File

@ -6,21 +6,36 @@ namespace %NAMESPACE%;
class %CLASSNAME% %EXTENDS% { class %CLASSNAME% %EXTENDS% {
protected array $__section_list = []; public array $sectionList = [];
protected array $__variables_list = []; public array $variableList = [];
public final function __output() : void public ?object $thisProxy = null;
{
extract($this->__variables_list); ?>%CONTENT%<?php public \Picea\Picea $picea;
public function __construct(\Picea\Picea $picea, array $variablesList = [], ?object $thisProxy = null) {
$this->picea = $picea;
$this->variableList = $variablesList;
$this->thisProxy = $thisProxy;
} }
public final function __renderSection($name) : void public function output(array $variablesList = []) : void
{
( function($___class__template, $___global_variables, $___variables) {
extract($___global_variables);
extract($___variables, \EXTR_OVERWRITE);
?>%CONTENT%<?php
} )->call($this->thisProxy ?? new class(){}, $this, $this->variableList, $variablesList);
%PARENT_OUTPUT%
}
public function renderSection($name) : void
{ {
foreach([ 'prepend', 'default', 'append' ] as $item) { foreach([ 'prepend', 'default', 'append' ] as $item) {
usort($this->__section_list[$name][$item], fn($a, $b) => $a['order'] <=> $b['order']); usort($this->sectionList[$name][$item], fn($a, $b) => $a['order'] <=> $b['order']);
foreach($this->__section_list[$name][$item] as $section) { foreach($this->sectionList[$name][$item] as $section) {
$section['callback'](); $section['callback']();
if ( $item === 'default' ) { if ( $item === 'default' ) {
@ -30,9 +45,11 @@ class %CLASSNAME% %EXTENDS% {
} }
} }
public final function __invoke() { public function __invoke(array $variablesList = []) {
ob_start(); ob_start();
$this->__output(); $this->output($variablesList);
return ob_get_clean(); return ob_get_clean();
} }
} }
return [ 'classname' => "%CLASSNAME%", 'namespace' => "%NAMESPACE%", 'extends' => "%EXTENDS_TEMPLATE%" ];

View File

@ -2,10 +2,31 @@
namespace Picea\Caching; namespace Picea\Caching;
use Picea\Compiler\Context;
interface Cache { interface Cache {
public function handle(string $fileName) : bool; /**
* Should return true if a cache instance of given view is found.
*
* @param string $viewPath
* @return bool
*/
public function compiled(string $viewPath) : bool;
public function load(); /**
* Load given view path content based on cache.
*
* @param string $viewPath
* @return string
*/
public function load(string $viewPath, ...$arguments) : object;
/**
* Save a given view path with a compiled context
*
* @param Context $context
* @return bool
*/
public function save(Context $context) : bool;
} }

View File

@ -2,48 +2,76 @@
namespace Picea\Caching; namespace Picea\Caching;
use Picea\Compiler\Context;
class Opcache implements Cache { class Opcache implements Cache {
protected string $cacheFolder = "picea";
protected string $cachePath = ""; protected string $cachePath = "";
protected array $compiled = [];
public function __construct(?string $cachePath = null) public function __construct(?string $cachePath = null)
{ {
$this->cachePath = $cachePath ?? sys_get_temp_dir(); $this->cachePath = $cachePath ?? sys_get_temp_dir();
} }
public function handle(string $fileName) : bool public function load(string $viewPath, ...$arguments) : object
{ {
if ( file_exists($path = $this->filePath($fileName)) ) { if ( empty($viewPath) ) {
require_once($path); throw new \RuntimeException("Given view path is empty.");
}
if ( ! $this->compiled($viewPath) ) {
throw new \RuntimeException("You must compile your view `$viewPath` before trying to load it.");
}
$compiledContext = $this->compiled[$viewPath];
if ( $compiledContext['extends'] ) {
$this->load($compiledContext['extends'], ...$arguments);
}
$fullName = isset($compiledContext['namespace']) ? "\\{$compiledContext['namespace']}\\{$compiledContext['classname']}" : $compiledContext['classname'];
return new $fullName(...$arguments);
}
public function save(Context $context) : bool
{
$fileName = $context->compiledClassPath();
$this->validateCachePath();
file_put_contents($this->cachePath($context->viewPath), $context->compiledSource);
return false;
}
public function compiled(string $viewPath) : bool
{
if ( false !== ( $this->compiled[$viewPath] ?? false ) ) {
return true;
}
if ( file_exists($path = $this->cachePath($viewPath)) ) {
$this->compiled[$viewPath] = require_once($path);
return true; return true;
} }
return false; return false;
} }
public function saveFile(string $fileName, string $content) : bool
{
$this->validateCachePath();
file_put_contents($this->filePath($fileName), $content);
return false;
}
public function filePath(string $fileName) : string
{
return $this->cachePath . $fileName;
}
protected function validateCachePath() : bool protected function validateCachePath() : bool
{ {
if ( ! file_exists($this->cachePath) ) { $fullPath = $this->cachePath();
mkdir($this->cachePath, 0750, true);
if ( ! file_exists($fullPath) ) {
mkdir($fullPath, 0750, true);
} }
if ( ! is_writeable($this->cachePath) ) { if ( ! is_writeable($fullPath) ) {
throw new \RuntimeException( throw new \RuntimeException(
sprintf("Given cache path `%s` is not writeable by `%s`", $this->cachePath, static::class) sprintf("Given cache path `%s` is not writeable by `%s`", $fullPath, static::class)
); );
return false; return false;
@ -51,4 +79,9 @@ class Opcache implements Cache {
return true; return true;
} }
protected function cachePath(string $fileName = "") : string
{
return implode(DIRECTORY_SEPARATOR, array_filter([ $this->cachePath, $this->cacheFolder, $fileName ? str_replace([ "/", DIRECTORY_SEPARATOR ], "_", $fileName . ".php") : null ]));
}
} }

View File

@ -2,4 +2,12 @@
namespace Picea\Compiler; namespace Picea\Compiler;
class BaseContext extends Context { } class BaseContext extends Context {
public function __construct(?string $defaultNamespace = null) {
if ( $defaultNamespace !== null ) {
$this->namespace = $defaultNamespace;
}
}
}

View File

@ -10,6 +10,12 @@ abstract class Context {
public string $extendFrom = ""; public string $extendFrom = "";
public string $className = "";
public string $compiledSource = "";
public string $viewPath = "";
public array $switchStack = []; public array $switchStack = [];
public array $iterateStack = []; public array $iterateStack = [];
@ -42,8 +48,17 @@ abstract class Context {
return implode(",", $context->useStack ?? []); return implode(",", $context->useStack ?? []);
} }
public function variables(array $variables) : void #public function variables(array $variables) : void
#{
# $this->variables = $variables;
#}
public function compiledClassPath() : string
{ {
$this->variables = $variables; if ($this->namespace) {
return "\\{$this->namespace}\\{$this->className}";
}
return $this->className;
} }
} }

View File

@ -12,7 +12,7 @@ class EndSectionToken implements ControlStructure {
} }
$section = array_pop($context->sections); $section = array_pop($context->sections);
$build = $context->extendFrom ? "" : "\$this->__renderSection({$section['name']});"; $build = $context->extendFrom ? "" : "\$___class__template->renderSection({$section['name']});";
return "<?php }]; $build?>"; return "<?php }]; $build?>";
} }
} }

View File

@ -13,8 +13,8 @@ class SectionToken implements ControlStructure {
$options = eval($options); $options = eval($options);
} }
if ( ! ctype_alnum(str_replace([".", "\"", "'"], "", $name)) ) { if ( ! ctype_alnum(str_replace([".", "\"", "'", "-", "_"], "", $name)) ) {
throw new \RuntimeException("Your section named `{$name}` contains invalid character. Allowed are only letters, numbers and dots"); throw new \RuntimeException("Your section named `{$name}` contains invalid character. Allowed are only letters, numbers, dashes, underscores and dots");
} }
$context->sections[] = [ $context->sections[] = [
@ -28,10 +28,10 @@ class SectionToken implements ControlStructure {
throw new \RuntimeException("An unsupported action `$action` was given as an option of a {% section %} tag"); throw new \RuntimeException("An unsupported action `$action` was given as an option of a {% section %} tag");
} }
$order = $options['order'] ?? "count(\$this->__section_list[$name]['$action'])"; $order = $options['order'] ?? "count(\$___class__template->sectionList[$name]['$action'])";
return "<?php \$this->__section_list[$name] ??= [ 'prepend' => [], 'append' => [], 'default' => [] ]; return "<?php \$___class__template->sectionList[$name] ??= [ 'prepend' => [], 'append' => [], 'default' => [] ];
\$this->__section_list[$name]['$action'][] = [ 'order' => $order, 'callback' => function() { \$___class__template->sectionList[$name]['$action'][] = [ 'order' => $order, 'callback' => function() use (\$___class__template, \$___global_variables, \$___variables) {
extract(\$this->__variables_list); ?>"; extract(\$___global_variables); extract(\$___variables, \EXTR_OVERWRITE); ?>";
} }
} }

View File

@ -7,7 +7,6 @@ class ViewToken implements ControlStructure {
public string $token = "view"; public string $token = "view";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) {
#$context->switchStack[] = true; return "<?php echo \$___class__template->picea->renderHtml($arguments, get_defined_vars()); ?>";
#return "<?php switch($arguments):";
} }
} }

View File

@ -0,0 +1,13 @@
<?php
namespace Picea\Extension;
class AssetExtension implements Extension {
public string $token = "asset";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) {
return "<?php echo 'assets! $arguments' ?>";
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Picea\Extension;
class LanguageExtension implements Extension {
public array $tokens = [ "lang", "_" ];
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) {
return "<?php /* echo lang($arguments) */ ?>";
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Picea\Extension;
class TitleExtension implements Extension {
public string $token = "title";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) {
return "<?php /* echo title ?? */ ?>";
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Picea\Extension;
class UrlExtension implements Extension {
public array $tokens = [ "url" , "route" ];
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) {
return "<?php /* echo url ?? */ ?>";
}
}

60
src/FileFetcher.php Normal file
View File

@ -0,0 +1,60 @@
<?php declare(strict_types=1);
namespace Picea;
class FileFetcher
{
protected array $folderList = [];
public array $supportedExtensionList = [
"phtml", "php", "html",
];
public function __construct(?array $folderList = null) {
if ( $folderList !== null ) {
$this->folderList = $folderList;
}
}
public function addFolder(string $folder, int $order = 100) : void
{
$folder = rtrim($folder, DIRECTORY_SEPARATOR);
$this->folderList[$folder] = [
'path' => $folder,
'order' => $order,
];
}
public function addFolders(array $folderList) : void
{
$this->folderList = array_replace($this->folderList, $folderList);
}
public function folderList(?array $set = null) : ?array
{
return $set === null ? $this->folderList : $this->folderList = $set;
}
public function findFile(string $fileName) : string
{
usort($this->folderList, fn($a, $b) => $a['order'] <=> $b['order']);
foreach($this->folderList as $folder) {
foreach($this->supportedExtensionList as $extension) {
$file = $folder['path'] . DIRECTORY_SEPARATOR . "$fileName.$extension";
if ( file_exists($file) ) {
return $file;
}
}
}
throw new \RuntimeException("Given view file `$fileName` can not be found within given folder list..");
}
public function getFileContent(string $fileName) : string
{
return file_get_contents($this->findFile($fileName));
}
}

View File

@ -46,10 +46,12 @@ class DefaultRegistrations implements LanguageRegistration
$compiler->registerControlStructure(new \Picea\ControlStructure\ExtendsToken()); $compiler->registerControlStructure(new \Picea\ControlStructure\ExtendsToken());
$compiler->registerControlStructure(new \Picea\ControlStructure\SectionToken()); $compiler->registerControlStructure(new \Picea\ControlStructure\SectionToken());
$compiler->registerControlStructure(new \Picea\ControlStructure\EndSectionToken()); $compiler->registerControlStructure(new \Picea\ControlStructure\EndSectionToken());
$compiler->registerControlStructure(new \Picea\ControlStructure\ViewToken());
} }
public function registerExtension(Compiler $compiler) : void public function registerExtension(Compiler $compiler) : void
{ {
$compiler->registerExtension(new \Picea\Extension\JsonExtension()); $compiler->registerExtension(new \Picea\Extension\JsonExtension());
$compiler->registerExtension(new \Picea\Extension\AssetExtension());
} }
} }

View File

@ -22,36 +22,52 @@ class Picea implements LanguageRegistration
public Caching\Cache $cache; public Caching\Cache $cache;
public FileFetcher $fileFetcher;
public bool $debug;
public function __construct( public function __construct(
?Closure $responseHtml = null, ?Closure $responseHtml = null,
?Cache $cache = null, ?Compiler\Context $context = null,
?Context $context = null, ?Caching\Cache $cache = null,
?Compiler $compiler = null, ?Compiler $compiler = null,
?LanguageRegistration $languageRegistration = null, ?LanguageRegistration $languageRegistration = null,
?string $builderTemplatePath = null ?FileFetcher $fileFetcher = null,
?string $builderTemplatePath = null,
bool $debug = false
){ ){
$this->response = $responseHtml; $this->response = $responseHtml;
$this->cache = $cache ?? new Caching\Memory(""); $this->cache = $cache ?? new Caching\Memory("");
$this->context = $context ?? new Compiler\BaseContext(); $this->context = $context ?? new Compiler\BaseContext();
$this->compiler = $compiler ?? $this->instanciateCompiler();
$this->languageRegistration = $languageRegistration ?? new Language\DefaultRegistrations(); $this->languageRegistration = $languageRegistration ?? new Language\DefaultRegistrations();
$this->builderTemplatePath = $builderTemplatePath ?? dirname(__FILE__) . static::DEFAULT_BUILDER_TEMPLATE; $this->builderTemplatePath = $builderTemplatePath ?? dirname(__FILE__) . static::DEFAULT_BUILDER_TEMPLATE;
$this->compiler = $compiler ?? $this->instanciateCompiler();
$this->fileFetcher = $fileFetcher ?? $this->instanciateFileFetcher();
$this->debug = $debug;
}
public function renderHtml(string $viewPath, array $variables = [], ?object $proxy = null) : string
{
if ( null === $object = $this->fetchFromCache($viewPath, $variables, $proxy) ) {
throw new \RuntimeException("An error occured while trying to save a compiled template.");
}
return $object();
} }
public function outputHtml(string $viewPath, array $variables) : ResponseInterface public function outputHtml(string $viewPath, array $variables) : ResponseInterface
{ {
if ( $this->response ?? false ) { if ( $this->response ?? false ) {
if ( false === $content = $this->cache->handle($viewPath) ) { if ( false === $content = $this->cache->handle($viewPath) ) {
# if ( )
} }
$source = $this->compileSource($source ?? "test"); $source = $this->compileSource($source ?? "test");
$response = $this->response; $response = $this->response;
return $response("abc"); return $response("abc");
} }
throw new \InvalidArgumentException("No \Psr\Http\Message\ResponseInterface closure provided. Please provide one using the constructor or assinging it to the class variable `responseHtml`."); throw new \InvalidArgumentException("No \Psr\Http\Message\ResponseInterface closure provided. Please provide one using the constructor or assigning it to the class variable `responseHtml`.");
} }
public function compileSource(string $source) : array public function compileSource(string $source) : array
@ -64,22 +80,22 @@ class Picea implements LanguageRegistration
]; ];
} }
public function compileFile(string $filePath) : array public function compileFile(string $viewPath) : array
{ {
if (! file_exists($filePath) ) { if (! file_exists($viewPath) ) {
throw new \InvalidArgumentException("File `$filePath` cannot be found or there is a permission problem."); throw new \InvalidArgumentException("File `$viewPath` cannot be found or there is a permission problem.");
} }
if ( false === $fileContent = file_get_contents($filePath) ) { if ( false === $fileContent = file_get_contents($viewPath) ) {
throw new \ErrorException("Given file could not be opened `$filePath`. This could indicate a permission misconfiguration on your file or folder."); throw new \ErrorException("Given file could not be opened `$viewPath`. This could indicate a permission misconfiguration on your file or folder.");
} }
$this->compiler->loadSourceCode($fileContent); $this->compiler->loadSourceCode($fileContent);
$context = clone $this->context;
$context->viewPath = $viewPath;
$context->source = $this->compiler->compile($context);
return [ return $context;
'context' => $context = clone $this->context,
'source' => $this->compiler->compile($context),
];
} }
public function buildFromSource(string $source) : array public function buildFromSource(string $source) : array
@ -99,9 +115,18 @@ class Picea implements LanguageRegistration
]; ];
} }
public function buildFromPath(string $path) : string public function fetchFromCache(string $viewPath, array $variables = [], ?object $proxy = null) : ?object
{ {
if ( $this->debug || ! $this->cache->compiled($viewPath) ) {
$context = $this->compileView($viewPath);
$this->cache->save($context);
if ( $context->extendFrom ) {
$this->fetchFromCache($context->extendFrom, $variables, $proxy);
}
}
return $this->cache->load($viewPath, $this, $variables, $proxy);
} }
public function instanciateCompiler() : Compiler public function instanciateCompiler() : Compiler
@ -114,6 +139,11 @@ class Picea implements LanguageRegistration
return new Builder($this->builderTemplatePath); return new Builder($this->builderTemplatePath);
} }
public function instanciateFileFetcher() : FileFetcher
{
return new FileFetcher();
}
public function registerAll(Compiler $compiler) : void public function registerAll(Compiler $compiler) : void
{ {
$this->registerSyntax($compiler); $this->registerSyntax($compiler);
@ -135,4 +165,21 @@ class Picea implements LanguageRegistration
{ {
$this->languageRegistration->registerExtension($compiler); $this->languageRegistration->registerExtension($compiler);
} }
protected function compileView(string $viewPath) : Compiler\Context
{
$tmpFolder = sys_get_temp_dir();
$builder = $this->instanciateBuilder();
$compiled = $this->compileSource($this->fileFetcher->getFileContent($viewPath));
$context = $compiled['context'];
$context->viewPath = $viewPath;
$context = $builder->build($compiled['context'], $compiled['source']) ;
$context->classPath = $tmpFolder . DIRECTORY_SEPARATOR . $context->className . ".php";
if ( $context->extendFrom ) {
$this->compileView($context->extendFrom);
}
return $context;
}
} }