- 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

@ -13,23 +13,23 @@ class Builder
$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 = [
'%NAMESPACE%' => $context->namespace,
'%USE%' => $context->renderUses(),
'%CLASSNAME%' => $className,
'%EXTENDS%' => $context->extendFrom ? static::generateClassUID($context->extendFrom) : '',
'%CLASSNAME%' => $context->className,
'%EXTENDS%' => $context->extendFrom ? "extends " . static::TEMPLATE_CLASSNAME_PREFIX . static::generateClassUID($context->extendFrom) : '',
'%EXTENDS_TEMPLATE%' => $context->extendFrom,
'%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 [
$context->namespace, $className, $classContent,
];
return $context;
}
public static function generateClassName(string $filepath) : string

@ -6,21 +6,36 @@ namespace %NAMESPACE%;
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;
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 function output(array $variablesList = []) : void
{
extract($this->__variables_list); ?>%CONTENT%<?php
( 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 final function __renderSection($name) : void
public function renderSection($name) : void
{
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']();
if ( $item === 'default' ) {
@ -30,9 +45,11 @@ class %CLASSNAME% %EXTENDS% {
}
}
public final function __invoke() {
public function __invoke(array $variablesList = []) {
ob_start();
$this->__output();
$this->output($variablesList);
return ob_get_clean();
}
}
return [ 'classname' => "%CLASSNAME%", 'namespace' => "%NAMESPACE%", 'extends' => "%EXTENDS_TEMPLATE%" ];

@ -2,10 +2,31 @@
namespace Picea\Caching;
use Picea\Compiler\Context;
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;
}

@ -2,48 +2,76 @@
namespace Picea\Caching;
use Picea\Compiler\Context;
class Opcache implements Cache {
protected string $cacheFolder = "picea";
protected string $cachePath = "";
protected array $compiled = [];
public function __construct(?string $cachePath = null)
{
$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)) ) {
require_once($path);
if ( empty($viewPath) ) {
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 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
{
if ( ! file_exists($this->cachePath) ) {
mkdir($this->cachePath, 0750, true);
$fullPath = $this->cachePath();
if ( ! file_exists($fullPath) ) {
mkdir($fullPath, 0750, true);
}
if ( ! is_writeable($this->cachePath) ) {
if ( ! is_writeable($fullPath) ) {
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;
@ -51,4 +79,9 @@ class Opcache implements Cache {
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 ]));
}
}

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

@ -10,6 +10,12 @@ abstract class Context {
public string $extendFrom = "";
public string $className = "";
public string $compiledSource = "";
public string $viewPath = "";
public array $switchStack = [];
public array $iterateStack = [];
@ -42,8 +48,17 @@ abstract class Context {
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;
}
}

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

@ -13,8 +13,8 @@ class SectionToken implements ControlStructure {
$options = eval($options);
}
if ( ! ctype_alnum(str_replace([".", "\"", "'"], "", $name)) ) {
throw new \RuntimeException("Your section named `{$name}` contains invalid character. Allowed are only letters, numbers and dots");
if ( ! ctype_alnum(str_replace([".", "\"", "'", "-", "_"], "", $name)) ) {
throw new \RuntimeException("Your section named `{$name}` contains invalid character. Allowed are only letters, numbers, dashes, underscores and dots");
}
$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");
}
$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' => [] ];
\$this->__section_list[$name]['$action'][] = [ 'order' => $order, 'callback' => function() {
extract(\$this->__variables_list); ?>";
return "<?php \$___class__template->sectionList[$name] ??= [ 'prepend' => [], 'append' => [], 'default' => [] ];
\$___class__template->sectionList[$name]['$action'][] = [ 'order' => $order, 'callback' => function() use (\$___class__template, \$___global_variables, \$___variables) {
extract(\$___global_variables); extract(\$___variables, \EXTR_OVERWRITE); ?>";
}
}

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

@ -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' ?>";
}
}

@ -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) */ ?>";
}
}

@ -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 ?? */ ?>";
}
}

@ -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

@ -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));
}
}

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

@ -22,36 +22,52 @@ class Picea implements LanguageRegistration
public Caching\Cache $cache;
public FileFetcher $fileFetcher;
public bool $debug;
public function __construct(
?Closure $responseHtml = null,
?Cache $cache = null,
?Context $context = null,
?Compiler\Context $context = null,
?Caching\Cache $cache = null,
?Compiler $compiler = null,
?LanguageRegistration $languageRegistration = null,
?string $builderTemplatePath = null
?FileFetcher $fileFetcher = null,
?string $builderTemplatePath = null,
bool $debug = false
){
$this->response = $responseHtml;
$this->cache = $cache ?? new Caching\Memory("");
$this->context = $context ?? new Compiler\BaseContext();
$this->compiler = $compiler ?? $this->instanciateCompiler();
$this->languageRegistration = $languageRegistration ?? new Language\DefaultRegistrations();
$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
{
if ( $this->response ?? false ) {
if ( false === $content = $this->cache->handle($viewPath) ) {
# if ( )
}
$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 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
@ -64,22 +80,22 @@ class Picea implements LanguageRegistration
];
}
public function compileFile(string $filePath) : array
public function compileFile(string $viewPath) : array
{
if (! file_exists($filePath) ) {
throw new \InvalidArgumentException("File `$filePath` cannot be found or there is a permission problem.");
if (! file_exists($viewPath) ) {
throw new \InvalidArgumentException("File `$viewPath` cannot be found or there is a permission problem.");
}
if ( false === $fileContent = file_get_contents($filePath) ) {
throw new \ErrorException("Given file could not be opened `$filePath`. This could indicate a permission misconfiguration on your file or folder.");
if ( false === $fileContent = file_get_contents($viewPath) ) {
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);
$context = clone $this->context;
$context->viewPath = $viewPath;
$context->source = $this->compiler->compile($context);
return [
'context' => $context = clone $this->context,
'source' => $this->compiler->compile($context),
];
return $context;
}
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
@ -114,6 +139,11 @@ class Picea implements LanguageRegistration
return new Builder($this->builderTemplatePath);
}
public function instanciateFileFetcher() : FileFetcher
{
return new FileFetcher();
}
public function registerAll(Compiler $compiler) : void
{
$this->registerSyntax($compiler);
@ -135,4 +165,21 @@ class Picea implements LanguageRegistration
{
$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;
}
}