From ddc3ae704a66be28b1165543bbda3ae27b8d70bc Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Thu, 23 Jan 2020 15:28:05 -0500 Subject: [PATCH] - Work on Language Handler and extension. - Added an URL extension which allows routes and URLs. - Fixed some bug within ClassTemplate --- src/Builder.php | 3 +- src/Builder/ClassTemplate.php | 30 ++++-- src/Compiler.php | 13 ++- src/Compiler/Context.php | 42 +++++++- src/ControlStructure/SectionToken.php | 2 +- src/ControlStructure/ViewToken.php | 2 +- src/Extension/LanguageExtension.php | 44 ++++++++- src/Extension/LanguageHandler.php | 8 ++ src/Extension/UrlExtension.php | 134 ++++++++++++++++++-------- src/Picea.php | 71 ++++++++++---- 10 files changed, 267 insertions(+), 82 deletions(-) create mode 100644 src/Extension/LanguageHandler.php diff --git a/src/Builder.php b/src/Builder.php index 5d023a5..90d7eda 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -19,11 +19,12 @@ class Builder $replace = [ '%NAMESPACE%' => $context->namespace, - '%USE%' => $context->renderUses(), + '%USE%' => ( $uses = $context->renderUses() ) ? "use $uses;" : false, '%CLASSNAME%' => $context->className, '%EXTENDS%' => $context->extendFrom ? "extends " . static::TEMPLATE_CLASSNAME_PREFIX . static::generateClassUID($context->extendFrom) : '', '%EXTENDS_TEMPLATE%' => $context->extendFrom, '%CONTENT%' => $compiledSource, + '%FUNCTIONS%' => $context->functionStack ? $context->renderFunctions() : "", '%PARENT_OUTPUT%' => $context->extendFrom ? "parent::output(\$variablesList);" : "", ]; diff --git a/src/Builder/ClassTemplate.php b/src/Builder/ClassTemplate.php index db48740..d07add7 100644 --- a/src/Builder/ClassTemplate.php +++ b/src/Builder/ClassTemplate.php @@ -8,29 +8,34 @@ class %CLASSNAME% %EXTENDS% { public array $sectionList = []; - public array $variableList = []; + public array $variableList = []; public ?object $thisProxy = null; public \Picea\Picea $picea; + public static $context; + public function __construct(\Picea\Picea $picea, array $variablesList = [], ?object $thisProxy = null) { $this->picea = $picea; $this->variableList = $variablesList; $this->thisProxy = $thisProxy; + $this->exportFunctions(); + + static::$context = $picea->context; } - public function output(array $variablesList = []) : void - { - ( function($___class__template, $___global_variables, $___variables, $___picea) { - extract($___global_variables); unset($___global_variables); - extract($___variables, \EXTR_OVERWRITE); unset($___variables); + public function output(array $variablesList = []) : void + { + ( function($___class__template, $___global_variables, $___variables, $picea) { + extract($___global_variables); + extract($___variables, \EXTR_OVERWRITE); ?>%CONTENT%call($this->thisProxy ?? new class(){}, $this, $this->variableList, $variablesList, $this->picea); %PARENT_OUTPUT% - } + } - public function renderSection($name) : void + public function renderSection($name) : void { foreach([ 'prepend', 'default', 'append' ] as $item) { usort($this->sectionList[$name][$item], fn($a, $b) => $a['order'] <=> $b['order']); @@ -44,8 +49,15 @@ class %CLASSNAME% %EXTENDS% { } } } + + public function exportFunctions() : void + { + static $caching = []; + %FUNCTIONS% + } - public function __invoke(array $variablesList = []) { + public function __invoke(array $variablesList = []) : string + { ob_start(); $this->output($variablesList); return ob_get_clean(); diff --git a/src/Compiler.php b/src/Compiler.php index 6e34108..ae93322 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -23,11 +23,11 @@ class Compiler public function __construct(?LanguageRegistration $languageRegistration = null) { $this->languageRegistration = $languageRegistration; + $this->languageRegistration->registerAll($this); } public function compile(Compiler\Context $context) : string { - $this->languageRegistration->registerAll($this); $context->compiler = $this; /** @@ -82,7 +82,7 @@ class Compiler public function registerControlStructure(ControlStructure\ControlStructure $controlStructureObject) : self { - foreach($controlStructureObject->tokens ?? (array) $controlStructureObject->token as $token) { + foreach($controlStructureObject->tokens ?? (array) ( $controlStructureObject->token ?? [] ) as $token) { $this->tagList[$token] = $controlStructureObject; } @@ -91,7 +91,7 @@ class Compiler public function registerExtension(Extension\Extension $extension) : self { - foreach($extension->tokens ?? (array) $extension->token as $token) { + foreach($extension->tokens ?? (array) ( $extension->token ?? [] ) as $token) { $this->extensionList[$token] = $extension; } @@ -108,4 +108,11 @@ class Compiler return $this->extensionList[$name]; } + + public function exportFunctions() : void + { + static $caching = []; + eval( $this->context->renderFunctions() ); + } + } diff --git a/src/Compiler/Context.php b/src/Compiler/Context.php index a02bcab..c72218f 100644 --- a/src/Compiler/Context.php +++ b/src/Compiler/Context.php @@ -22,6 +22,8 @@ abstract class Context { public array $useStack = []; + public array $functionStack = []; + public array $hooks = []; public Compiler $compiler; @@ -45,13 +47,38 @@ abstract class Context { public function renderUses() : string { - return implode(",", $context->useStack ?? []); + return implode(",", $this->useStack ?? []); } - #public function variables(array $variables) : void - #{ - # $this->variables = $variables; - #} + public function renderFunctions() : string + { + $cls = $this->compiledClassPath(); + $ns = $this->namespace ? "\\{$this->namespace}\\" : ""; + + foreach($this->functionStack as $name => $function) { + $list[] = <<functionStack['$name'](...\$arguments); + } + } + FUNC; + } + + return implode(PHP_EOL, $list); + } + + public function exportFunctions() : void + { + + } + + public function pushFunction($name, Callable $callable) : void + { + $this->functionStack[$name] = $callable; + } public function compiledClassPath() : string { @@ -61,4 +88,9 @@ abstract class Context { return $this->className; } + + public function cacheFilename() : string + { + return strtolower(str_replace("\\", DIRECTORY_SEPARATOR, $this->namespace)) . ".context"; + } } diff --git a/src/ControlStructure/SectionToken.php b/src/ControlStructure/SectionToken.php index 293f9d5..be3cdc3 100644 --- a/src/ControlStructure/SectionToken.php +++ b/src/ControlStructure/SectionToken.php @@ -31,7 +31,7 @@ class SectionToken implements ControlStructure { $order = $options['order'] ?? "count(\$___class__template->sectionList[$name]['$action'])"; return "sectionList[$name] ??= [ 'prepend' => [], 'append' => [], 'default' => [] ]; - \$___class__template->sectionList[$name]['$action'][] = [ 'order' => $order, 'callback' => function() use (\$___class__template, \$___global_variables, \$___variables) { + \$___class__template->sectionList[$name]['$action'][] = [ 'order' => $order, 'callback' => function() use (\$picea, \$___class__template, \$___global_variables, \$___variables) { extract(\$___global_variables); extract(\$___variables, \EXTR_OVERWRITE); ?>"; } } diff --git a/src/ControlStructure/ViewToken.php b/src/ControlStructure/ViewToken.php index 30a82d4..ddf2078 100644 --- a/src/ControlStructure/ViewToken.php +++ b/src/ControlStructure/ViewToken.php @@ -7,6 +7,6 @@ class ViewToken implements ControlStructure { public string $token = "view"; public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { - return "picea->renderHtml($arguments, get_defined_vars()); ?>"; + return "picea->renderHtml($arguments, get_defined_vars(), \$this); ?>"; } } diff --git a/src/Extension/LanguageExtension.php b/src/Extension/LanguageExtension.php index 2b01c36..d1e8677 100644 --- a/src/Extension/LanguageExtension.php +++ b/src/Extension/LanguageExtension.php @@ -2,12 +2,50 @@ namespace Picea\Extension; +use Picea\Compiler\Context; + class LanguageExtension implements Extension { - public array $tokens = [ "lang", "_" ]; + public array $tokens = [ "lang", "_", "language.set" ]; - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { - return ""; + public string $currentLanguage = ""; + + protected LanguageHandler $languageHandler; + + public function __construct(Context $context, LanguageHandler $handler) { + $this->register($context); + $this->languageHandler = $handler; } + public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) : string + { + switch($token) { + case "language.set": + return "compiler->getExtensionFromToken('$token')->currentLanguage = $arguments; ?>"; + + case "lang": + return "compiler->getExtensionFromToken('$token')->absoluteLang($arguments) ?>"; + + case "_": + return "compiler->getExtensionFromToken('$token')->relativeLang($arguments) ?>"; + } + + return ""; + } + + public function register(Context $context) : void + { + $context->pushFunction("_", [ $this, 'relativeLang' ]); + $context->pushFunction("lang", [ $this, 'absoluteLang' ]); + } + + public function relativeLang(string $key, array $variables = []) : string + { + return $this->languageHandler->languageFromKey("{$this->currentLanguage}.{$key}", $variables); + } + + public function absoluteLang(string $key, array $variables = []) : string + { + return $this->languageHandler->languageFromKey($key, $variables); + } } diff --git a/src/Extension/LanguageHandler.php b/src/Extension/LanguageHandler.php new file mode 100644 index 0000000..4f97cfd --- /dev/null +++ b/src/Extension/LanguageHandler.php @@ -0,0 +1,8 @@ +urlBase = $urlBase; $this->assetToken = $assetToken; - } + $this->register($context); + } - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) : ?string + public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) : ?string { switch($token) { - case "asset": - return "compiler->getExtensionFromToken('$token')->buildAssetUrl($arguments) ?>"; - - case "route": - return "compiler->getExtensionFromToken('$token')->buildUrl($arguments) ?>"; - - case "url": - return "compiler->getExtensionFromToken('$token')->buildUrl($arguments) ?>"; + case "asset": + return "compiler->getExtensionFromToken('$token')->buildAssetUrl($arguments) ?>"; + + case "route": + return "compiler->getExtensionFromToken('$token')->buildRouteUrl($arguments) ?>"; + + case "url": + return "compiler->getExtensionFromToken('$token')->buildUrl($arguments) ?>"; } - + return null; } - public function buildUrl(string $uri, array $parameters = []) : string + public function register(Context $context) : void { - return $this->url() . $uri . ( $parameters ? "?" . http_build_query($parameters) : "" ); + $context->pushFunction("url", [ $this, 'buildUrl' ]); + $context->pushFunction("asset", [ $this, 'buildAssetUrl' ]); + $context->pushFunction("route", [ $this, 'buildRouteUrl' ]); } - + + public function buildUrl(string $uri = "", array $parameters = []) : string + { + return $this->url() . "/" . ltrim($uri, "/") . ( $parameters ? "?" . http_build_query($parameters) : "" ); + } + public function buildAssetUrl(string $uri, array $parameters = []) : string { - return $this->url() . $uri . "?" . http_build_query( array_replace([ 'token' => $this->assetToken ], $parameters) ); + return $this->buildUrl($uri, array_replace([ 'v' => $this->assetToken ], $parameters)); } - + + public function buildRouteUrl(string $name, array $parameters = []) : string + { + if ( false !== ( $route = $this->routes[$name] ?? false ) ) { + return $this->buildUrl($this->prepareRoute($route, $parameters), $parameters); + } + + $routeList = json_encode($this->routes, \JSON_PRETTY_PRINT); + + throw new \RuntimeException(" + Given route `$name` could not be found within current route list: + $routeList + "); + } + public function url() : string { return $this->scheme() . $this->domain() . $this->base(); } - + + public function registerRoute(string $name, string $route) : void + { + $this->routes[$name] = $route; + } + /** * Return URI formatted */ @@ -60,33 +91,33 @@ class UrlExtension implements Extension { if ( ($base = $this->config['base'] ?? false) && ( stripos($uri, $base) === 0 ) ) { $uri = substr($uri, strlen($base)); } - + return '/' . ltrim($uri, '/'); } - + /** * Return query string */ - protected function queryString() : string + protected function queryString() : string { return isset($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : ""; } - - protected function scheme() : string + + protected function scheme() : string { return ( $this->isHttps() ? "https" : "http" ) . "://"; } - - protected function base() : string + + protected function base() : string { return $this->urlBase ? $this->urlBase . "/" : ""; } - + /** * Check if Uri's segments are valid * @param array $segments The uri's segments */ - protected function secure($segments = []) : array + protected function secure($segments = []) : array { return array_diff($segments, [ '..', '://' ]); } @@ -95,25 +126,46 @@ class UrlExtension implements Extension { { return strtolower($_SERVER['HTTP_HOST']); } - - protected function isDefaultPort() : bool + + protected function isDefaultPort() : bool { return in_array($_SERVER['SERVER_PORT'], [ 80, 443 ]); } - - protected function isHttps() : bool + + protected function isHttps() : bool { - $header = in_array('https', array_map("strtolower", [ + $header = in_array('https', array_map("strtolower", [ $_SERVER['X-Url-Scheme'] ?? "", - $_SERVER['Front-End-Https'] ?? "", + $_SERVER['Front-End-Https'] ?? "", $_SERVER['X-Forwarded-Ssl'] ?? "", - $_SERVER['X-Forwarded-Proto'] ?? "", + $_SERVER['X-Forwarded-Proto'] ?? "", $_SERVER['X-Forwarded-Protocol'] ?? "", ])); - - return $header - || ( "443" === $_SERVER['SERVER_PORT'] ?? "" ) - || ( "https" === $_SERVER['REQUEST_SCHEME'] ?? "http" ) + + return $header + || ( "443" === $_SERVER['SERVER_PORT'] ?? "" ) + || ( "https" === $_SERVER['REQUEST_SCHEME'] ?? "http" ) || ( "off" !== strtolower($_SERVER['HTTPS'] ?? "off") ); } -} \ No newline at end of file + + protected function prepareRoute(string $route, array &$arguments) + { + if ( preg_match_all('~{(.*?)}~si', $route, $matches, PREG_SET_ORDER) ) { + $search = []; + + foreach($matches as $item) { + if ( ! array_key_exists($item[1], $arguments) ) { + throw new \InvalidArgumentException("Argument `{$item[1]}` is missing within required route parameter(s)"); + } + + $search[ $item[0] ] = $arguments[ $item[1] ]; + unset($arguments[ $item[1] ]); + } + + $route = str_replace(array_keys($search), array_values($search), $route); + } + + return $route; + + } +} diff --git a/src/Picea.php b/src/Picea.php index 99f36aa..0a44531 100644 --- a/src/Picea.php +++ b/src/Picea.php @@ -27,13 +27,13 @@ class Picea implements LanguageRegistration public bool $debug; public function __construct( - ?Closure $responseHtml = null, - ?Compiler\Context $context = null, - ?Caching\Cache $cache = null, - ?Compiler $compiler = null, - ?LanguageRegistration $languageRegistration = null, - ?FileFetcher $fileFetcher = null, - ?string $builderTemplatePath = null, + ? Closure $responseHtml = null, + ? Compiler\Context $context = null, + ? Caching\Cache $cache = null, + ? Compiler $compiler = null, + ? LanguageRegistration $languageRegistration = null, + ? FileFetcher $fileFetcher = null, + ? string $builderTemplatePath = null, bool $debug = false ){ $this->response = $responseHtml; @@ -44,6 +44,9 @@ class Picea implements LanguageRegistration $this->compiler = $compiler ?? $this->instanciateCompiler(); $this->fileFetcher = $fileFetcher ?? $this->instanciateFileFetcher(); $this->debug = $debug; + + #$this->context->exportFunctions(); + $this->renderContext($this->context); } public function renderHtml(string $viewPath, array $variables = [], ?object $proxy = null) : string @@ -55,21 +58,31 @@ class Picea implements LanguageRegistration return $object(); } - public function outputHtml(string $viewPath, array $variables) : ResponseInterface + public function renderContext(Compiler\Context $context) : object { - if ( $this->response ?? false ) { - if ( false === $content = $this->cache->handle($viewPath) ) { - - } - - $source = $this->compileSource($source ?? "test"); - $response = $this->response; - return $response("abc"); + if ( null === $object = $this->contextFromCache($context) ) { + throw new \RuntimeException("An error occured while trying to save a compiled template."); } - throw new \InvalidArgumentException("No \Psr\Http\Message\ResponseInterface closure provided. Please provide one using the constructor or assigning it to the class variable `responseHtml`."); + return $object; } + + #public function outputHtml(string $viewPath, array $variables) : ResponseInterface + #{ + # if ( $this->response ?? false ) { + # if ( false === $content = $this->cache->handle($viewPath) ) { + # + # } + + # $source = $this->compileSource($source ?? "test"); + # $response = $this->response; + # return $response("abc"); + # } + + # 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 { $this->compiler->loadSourceCode($source); @@ -94,7 +107,7 @@ class Picea implements LanguageRegistration $context = clone $this->context; $context->viewPath = $viewPath; $context->source = $this->compiler->compile($context); - + return $context; } @@ -129,6 +142,16 @@ class Picea implements LanguageRegistration return $this->cache->load($viewPath, $this, $variables, $proxy); } + public function contextFromCache(Compiler\Context $context) : ?object + { + if ( $this->debug || ! $this->cache->compiled( $context->cacheFilename() ) ) { + $context = $this->compileContext($context); + $this->cache->save($context); + } + + return $this->cache->load($context->cacheFilename(), $this); + } + public function instanciateCompiler() : Compiler { return new Compiler($this->languageRegistration); @@ -182,4 +205,16 @@ class Picea implements LanguageRegistration return $context; } + + protected function compileContext(Compiler\Context $context) : Compiler\Context + { + $tmpFolder = sys_get_temp_dir(); + $builder = $this->instanciateBuilder(); + $compiled = $this->compileSource(""); + $context->viewPath = $context->cacheFilename(); + $builder->build($context, ""); + $context->classPath = $tmpFolder . DIRECTORY_SEPARATOR . $context->cacheFilename() . ".php"; + + return $context; + } }