Skip to content

Commit

Permalink
Major overhaul, with many breaking changes and new features.
Browse files Browse the repository at this point in the history
Now possible to share data across templates.
Now possible to preassign data to specific templates.
Templates variables are now accessed without the $this pseudo-variable.
Improvements to how extensions are registered.
Now possible to add one-off template functions (without using an extension).
New folder theme mode (missing folder templates will fall back to the default folder).
New optional compiler adds automatic escaping and a cleaner template syntax.
  • Loading branch information
reinink committed Jul 17, 2014
1 parent fb5e204 commit 8a51dcc
Show file tree
Hide file tree
Showing 13 changed files with 793 additions and 362 deletions.
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
"role": "Developer"
}
],
"require": {
"nikic/php-parser": "1.0.0-beta1"
},
"require-dev": {
"phpunit/phpunit": "~4.0",
"mockery/mockery": "~0.9",
Expand Down
159 changes: 159 additions & 0 deletions src/Compiler/Compiler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php

namespace League\Plates\Compiler;

/**
* Optional compiler that enables automatic escaping and shorter template syntax.
*/
class Compiler
{
/**
* Instance of the template engine.
* @var League\Plates\Engine
*/
private $engine;

/**
* Path to compiled template cache directory.
* @var string
*/
private $cacheDirectory;

/**
* Create new Compiler instance.
* @param Engine $engine
* @param string|null $cacheDirectory
*/
public function __construct(\League\Plates\Engine $engine, $cacheDirectory = null)
{
$this->engine = $engine;
$this->setCacheDirectory($cacheDirectory);
}

/**
* Set path to cache directory.
* @param string|null $cacheDirectory Pass null to disable caching.
* @return Compiler
*/
public function setCacheDirectory($cacheDirectory)
{
if (!is_null($cacheDirectory) and !is_string($cacheDirectory)) {
throw new \LogicException(
'The cache directory must be a string or null, ' . gettype($cacheDirectory) . ' given.'
);
}

if (is_string($cacheDirectory) and !is_dir($cacheDirectory)) {
throw new \LogicException('The specified cache directory "' . $cacheDirectory . '" does not exist.');
}

$this->cacheDirectory = $cacheDirectory;

return $this;
}

/**
* Compile template based on compiling flags.
* @param string $name
* @return string
*/
public function compile($sourcePath)
{
$destinationPath = $this->getCompiledPath($sourcePath);

if ($this->isCacheExpired($sourcePath, $destinationPath)) {
try {

// Get source code from template
$sourceCode = file_get_contents($sourcePath);

// Convert short open tags "<?" to normal open tags "<?php"
$sourceCode = preg_replace('/<\?(?!xml|php|=)/s', '<?php', $sourceCode);

// Parse
$parser = new \PhpParser\Parser(new \PhpParser\Lexer);
$statements = $parser->parse($sourceCode);

// Modify
$traverser = new \PhpParser\NodeTraverser;
$traverser->addVisitor(
new NodeVisitor(
$this->getTemplateFunctions(),
$this->getRawTemplateFunctions()
)
);
$statements = $traverser->traverse($statements);

// Generate
$prettyPrinter = new \PhpParser\PrettyPrinter\Standard;
$sourceCode = '<?php ' . $prettyPrinter->prettyPrint($statements) . ' ?>';

// Save to disk
file_put_contents($destinationPath, $sourceCode);
} catch (\PhpParser\Error $e) {
echo 'Parse Error: ', $e->getMessage();
}
}

return $destinationPath;
}

/**
* Get the compiled template path based on the template name.
* @param string $name
* @return string
*/
public function getCompiledPath($sourcePath)
{
if ($this->cacheDirectory) {
return $this->cacheDirectory . DIRECTORY_SEPARATOR . md5($sourcePath);
} else {
return tempnam(sys_get_temp_dir(), 'plates_');
}
}

/**
* Determine if the template cache has expired.
* @param string $sourcePath
* @param string $destinationPath
* @return bool
*/
public function isCacheExpired($sourcePath, $destinationPath)
{
if (is_null($this->cacheDirectory)) {
return true;
}

if (!is_file($destinationPath)) {
return true;
}

return filemtime($sourcePath) >= filemtime($destinationPath);
}

/**
* Get array of all template function names.
* @return array
*/
public function getTemplateFunctions()
{
return array_merge(array_keys($this->engine->getAllFunctions()), array('layout', 'start', 'stop', 'section'));
}

/**
* Get array of all raw template function names.
* @return array
*/
public function getRawTemplateFunctions()
{
$functions = array('section');

foreach ($this->engine->getAllFunctions() as $function) {
if ($function->isRaw()) {
$functions[] = $function->getName();
}
}

return $functions;
}
}
78 changes: 78 additions & 0 deletions src/Compiler/NodeVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

namespace League\Plates\Compiler;

class NodeVisitor extends \PhpParser\NodeVisitorAbstract
{
private $functions;

private $rawFunctions;

public function __construct(array $functions = array(), array $rawFunctions = array())
{
$this->functions = $functions;
$this->rawFunctions = $rawFunctions;
}

public function enterNode(\PhpParser\Node $node)
{
if ($node instanceof \PhpParser\Node\Stmt\Echo_) {

// Is the raw() function
$isTheRawFunction = (
$node->exprs[0] instanceof \PhpParser\Node\Expr\FuncCall and
$node->exprs[0]->name == 'raw'
);

// Is the raw() method
$isTheRawMethod = (
$node->exprs[0] instanceof \PhpParser\Node\Expr\MethodCall and
$node->exprs[0]->var->name == 'this' and
$node->exprs[0]->name === 'raw'
);

// Is a "raw function"
$isARawFunction = (
$node->exprs[0] instanceof \PhpParser\Node\Expr\FuncCall and
in_array($node->exprs[0]->name, $this->rawFunctions)
);

// Is a "raw method"
$isARawMethod = (
$node->exprs[0] instanceof \PhpParser\Node\Expr\MethodCall and
$node->exprs[0]->var->name == 'this' and
in_array($node->exprs[0]->name, $this->rawFunctions)
);

if ($isTheRawFunction or $isTheRawMethod) {

// Remove raw() function/method
$node->exprs = $node->exprs[0]->args;

} elseif (!$isARawFunction and !$isARawMethod) {

// Insert escape method
$node->exprs = array(
new \PhpParser\Node\Expr\MethodCall(
new \PhpParser\Node\Expr\Variable('this'),
new \PhpParser\Node\Name('escape'),
$node->exprs
)
);
}
}
}

public function leaveNode(\PhpParser\Node $node)
{
if ($node instanceof \PhpParser\Node\Expr\FuncCall and in_array($node->name, $this->functions)) {

// Convert template functions to methods
return new \PhpParser\Node\Expr\MethodCall(
new \PhpParser\Node\Expr\Variable('this'),
new \PhpParser\Node\Name($node->name),
$node->args
);
}
}
}

0 comments on commit 8a51dcc

Please sign in to comment.