Switch from Packages to PackageInterface
Add todo about possible debug mode implementation
Fix or extend documentation
Replace some SyntaxError with RuntimeError
Remove prefix usage, we now write to output relative to current
directory or with resolved bundle name
Retrieve output url from package getUrl method before resolving bundle
name
Better capture exception in atomic renaming section
Remove PackNode usage and use upsteam Node, SetNode, Textnode and
AssignNameExpression twig classes
Cleanup
-// src/Rapsys/PackBundle/Twig/PackTokenParser.php
namespace Rapsys\PackBundle\Twig;
use Symfony\Component\HttpKernel\Config\FileLocator;
namespace Rapsys\PackBundle\Twig;
use Symfony\Component\HttpKernel\Config\FileLocator;
-use Symfony\Component\Asset\Packages;
-
-class PackTokenParser extends \Twig_TokenParser {
+use Symfony\Component\Asset\PackageInterface;
+use Twig\Error\RuntimeError;
+use Twig\Error\SyntaxError;
+use Twig\Node\Expression\AssignNameExpression;
+use Twig\Node\Node;
+use Twig\Node\SetNode;
+use Twig\Node\TextNode;
+use Twig\Token;
+use Twig\TokenParser\AbstractTokenParser;
+
+class PackTokenParser extends AbstractTokenParser {
+ //The tag name
- * @param class $fileLocator The FileLocator instance
- * @param class $assetsPackages The Assets Packages instance
- * @param string $config The config path
- * @param string $tag The tag name
- * @param string $output The default output string
- * @param array $filters The default filters array
+ * @param class $locator The FileLocator instance
+ * @param class $package The Assets Package instance
+ * @param string $config The config path
+ * @param string $tag The tag name
+ * @param string $output The default output string
+ * @param array $filters The default filters array
- public function __construct(FileLocator $fileLocator, Packages $assetsPackages, $config, $tag, $output, $filters) {
- $this->fileLocator = $fileLocator;
- $this->assetsPackages = $assetsPackages;
+ public function __construct(FileLocator $locator, PackageInterface $package, $config, $tag, $output, $filters) {
+ //Save locator
+ $this->locator = $locator;
- //Set prefix
- $this->prefix = $config['prefix'];
+ //Save assets package
+ $this->package = $package;
- $this->name = $config['name'];
+ $this->name = $config['name'];
- $this->scheme = $config['scheme'];
+ $this->scheme = $config['scheme'];
- $this->timeout = $config['timeout'];
+ $this->timeout = $config['timeout'];
- $this->agent = $config['agent'];
+ $this->agent = $config['agent'];
- $this->redirect = $config['redirect'];
+ $this->redirect = $config['redirect'];
- $this->output = $output;
+ $this->output = $output;
- $this->filters = $filters;
+ $this->filters = $filters;
+ /**
+ * Get the tag name
+ */
public function getTag() {
return $this->tag;
}
public function getTag() {
return $this->tag;
}
- public function parse(\Twig_Token $token) {
+ /**
+ * Parse the token
+ *
+ * @param class $token The \Twig\Token instance
+ *
+ * @return class The PackNode
+ *
+ * @todo see if we can't add a debug mode behaviour
+ *
+ * If twig.debug or env=dev (or rapsys_pack.config.debug?) is set, it should be possible to loop on each input
+ * and process the captured body without applying requested filter.
+ *
+ * @todo list:
+ * - detect debug mode
+ * - retrieve fixe link from input s%@(Name)Bundle/Resources/public(/somewhere/file.ext)%/bundles/\L\1\E\2%
+ * - for each inputs:
+ * - generate a set asset_url=x
+ * - generate a body
+ */
+ public function parse(Token $token) {
$parser = $this->parser;
$stream = $this->parser->getStream();
$parser = $this->parser;
$stream = $this->parser->getStream();
$name = $this->name;
$output = $this->output;
$filters = $this->filters;
$content = '';
$name = $this->name;
$output = $this->output;
$filters = $this->filters;
$content = '';
- while (!$stream->test(\Twig_Token::BLOCK_END_TYPE)) {
- if ($stream->test(\Twig_Token::STRING_TYPE)) {
- // '@jquery', 'js/src/core/*', 'js/src/extra.js'
+ //Process the token block until end
+ while (!$stream->test(Token::BLOCK_END_TYPE)) {
+ //The files to process
+ if ($stream->test(Token::STRING_TYPE)) {
+ //'somewhere/somefile.(css,img,js)' 'somewhere/*' '@jquery'
$inputs[] = $stream->next()->getValue();
$inputs[] = $stream->next()->getValue();
- } elseif ($stream->test(\Twig_Token::NAME_TYPE, 'filters')) {
- // filter='yui_js'
+ //The filters token
+ } elseif ($stream->test(Token::NAME_TYPE, 'filters')) {
+ //filter='yui_js'
- $stream->expect(\Twig_Token::OPERATOR_TYPE, '=');
- $filters = array_merge($filters, array_filter(array_map('trim', explode(',', $stream->expect(\Twig_Token::STRING_TYPE)->getValue()))));
- } elseif ($stream->test(\Twig_Token::NAME_TYPE, 'output')) {
- // output='js/packed/*.js' OR output='js/core.js'
+ $stream->expect(Token::OPERATOR_TYPE, '=');
+ $filters = array_merge($filters, array_filter(array_map('trim', explode(',', $stream->expect(Token::STRING_TYPE)->getValue()))));
+ //The output token
+ } elseif ($stream->test(Token::NAME_TYPE, 'output')) {
+ //output='js/packed/*.js' OR output='js/core.js'
- $stream->expect(\Twig_Token::OPERATOR_TYPE, '=');
- $output = $stream->expect(\Twig_Token::STRING_TYPE)->getValue();
- } elseif ($stream->test(\Twig_Token::NAME_TYPE, 'name')) {
- // name='core_js'
+ $stream->expect(Token::OPERATOR_TYPE, '=');
+ $output = $stream->expect(Token::STRING_TYPE)->getValue();
+ //The name token
+ } elseif ($stream->test(Token::NAME_TYPE, 'name')) {
+ //name='core_js'
- $stream->expect(\Twig_Token::OPERATOR_TYPE, '=');
- $name = $stream->expect(\Twig_Token::STRING_TYPE)->getValue();
+ $stream->expect(Token::OPERATOR_TYPE, '=');
+ $name = $stream->expect(Token::STRING_TYPE)->getValue();
+ //Unexpected token
} else {
$token = $stream->getCurrent();
} else {
$token = $stream->getCurrent();
- throw new \Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s"', \Twig_Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $stream->getSourceContext());
+ throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s"', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $stream->getSourceContext());
- $stream->expect(\Twig_Token::BLOCK_END_TYPE);
+ //Process end block
+ $stream->expect(Token::BLOCK_END_TYPE);
+
+ //Process body
+ $body = $this->parser->subparse([$this, 'testEndTag'], true);
- $body = $this->parser->subparse(array($this, 'testEndTag'), true);
+ //Process end block
+ $stream->expect(Token::BLOCK_END_TYPE);
- $stream->expect(\Twig_Token::BLOCK_END_TYPE);
+ //TODO: debug mode should be inserted here before the output variable is rewritten
//Replace star with sha1
if (($pos = strpos($output, '*')) !== false) {
//Replace star with sha1
if (($pos = strpos($output, '*')) !== false) {
- #XXX: assetic code: substr(sha1(serialize($inputs).serialize($filters).serialize($options)), 0, 7)
+ //XXX: assetic use substr(sha1(serialize($inputs).serialize($filters).serialize($options)), 0, 7)
$output = substr($output, 0, $pos).sha1(serialize($inputs).serialize($filters)).substr($output, $pos + 1);
}
$output = substr($output, 0, $pos).sha1(serialize($inputs).serialize($filters)).substr($output, $pos + 1);
}
for($k = 0; $k < count($inputs); $k++) {
//Deal with generic url
if (strpos($inputs[$k], '//') === 0) {
for($k = 0; $k < count($inputs); $k++) {
//Deal with generic url
if (strpos($inputs[$k], '//') === 0) {
} elseif (strpos($inputs[$k], '://') === false) {
//Check if we have a bundle path
if ($inputs[$k][0] == '@') {
} elseif (strpos($inputs[$k], '://') === false) {
//Check if we have a bundle path
if ($inputs[$k][0] == '@') {
- //Check that we don't have only a path
+ //Check that we have a / separator between bundle name and path
if (($pos = strpos($inputs[$k], '/')) === false) {
if (($pos = strpos($inputs[$k], '/')) === false) {
- #TODO: @jquery support (if we really want it)
- #header('Content-Type: text/plain');
- #var_dump($inputs);
- #if ($inputs[0] == '@jquery') {
- # exit;
- #}
- throw new \Twig_Error_Syntax(sprintf('Invalid input path "%s"', $inputs[$k]), $token->getLine(), $stream->getSourceContext());
+ //TODO: add a @jquery magic feature ?
+ /*
+ header('Content-Type: text/plain');
+ var_dump($inputs);
+ if ($inputs[0] == '@jquery') {
+ exit;
+ }
+ */
+ throw new RuntimeError(sprintf('Invalid input path "%s"', $inputs[$k]), $token->getLine(), $stream->getSourceContext());
}
//Resolve bundle prefix
}
//Resolve bundle prefix
- $inputs[$k] = $this->fileLocator->locate(substr($inputs[$k], 0, $pos)).substr($inputs[$k], $pos + 1);
+ $inputs[$k] = $this->locator->locate(substr($inputs[$k], 0, $pos)).substr($inputs[$k], $pos + 1);
}
//Deal with globs
if (strpos($inputs[$k], '*') !== false || (($a = strpos($inputs[$k], '{')) !== false && ($b = strpos($inputs[$k], ',', $a)) !== false && strpos($inputs[$k], '}', $b) !== false)) {
}
//Deal with globs
if (strpos($inputs[$k], '*') !== false || (($a = strpos($inputs[$k], '{')) !== false && ($b = strpos($inputs[$k], ',', $a)) !== false && strpos($inputs[$k], '}', $b) !== false)) {
$replacement = glob($inputs[$k], GLOB_NOSORT|GLOB_BRACE);
//Check that these are working files
foreach($replacement as $input) {
$replacement = glob($inputs[$k], GLOB_NOSORT|GLOB_BRACE);
//Check that these are working files
foreach($replacement as $input) {
+ //Check that it's a file
- throw new \Twig_Error_Syntax(sprintf('Input path "%s" from "%s" is not a file', $input, $inputs[$k]), $token->getLine(), $stream->getSourceContext());
+ throw new RuntimeError(sprintf('Input path "%s" from "%s" is not a file', $input, $inputs[$k]), $token->getLine(), $stream->getSourceContext());
}
}
//Replace with glob path
}
}
//Replace with glob path
$k += count($replacement) - 1;
//Check that it's a file
} elseif (!is_file($inputs[$k])) {
$k += count($replacement) - 1;
//Check that it's a file
} elseif (!is_file($inputs[$k])) {
- throw new \Twig_Error_Syntax(sprintf('Input path "%s" is not a file', $inputs[$k]), $token->getLine(), $stream->getSourceContext());
+ throw new RuntimeError(sprintf('Input path "%s" is not a file', $inputs[$k]), $token->getLine(), $stream->getSourceContext());
}
}
}
//Init context
$ctx = stream_context_create(
}
}
}
//Init context
$ctx = stream_context_create(
- array(
- 'http' => array(
'timeout' => $this->timeout,
'user_agent' => $this->agent,
'redirect' => $this->redirect,
'timeout' => $this->timeout,
'user_agent' => $this->agent,
'redirect' => $this->redirect,
foreach($inputs as $input) {
//Try to retrieve content
if (($data = file_get_contents($input, false, $ctx)) === false) {
foreach($inputs as $input) {
//Try to retrieve content
if (($data = file_get_contents($input, false, $ctx)) === false) {
- throw new \Twig_Error_Syntax(sprintf('Unable to retrieve input path "%s"', $input), $token->getLine(), $stream->getSourceContext());
+ throw new RuntimeError(sprintf('Unable to retrieve input path "%s"', $input), $token->getLine(), $stream->getSourceContext());
}
//Append content
$content .= $data;
}
} else {
}
//Append content
$content .= $data;
}
} else {
- #TODO: trigger error about empty inputs ?
+ //Trigger error about empty inputs
+ //XXX: There may be a legitimate case where inputs is empty, if so please contact the author
+ throw new RuntimeError('Empty inputs token', $token->getLine(), $stream->getSourceContext());
//Apply all filters
foreach($filters as $filter) {
//Init args
//Apply all filters
foreach($filters as $filter) {
//Init args
- $args = array($stream->getSourceContext(), $token->getLine());
+ $args = [$stream->getSourceContext(), $token->getLine()];
//Check if args is available
if (!empty($filter['args'])) {
//Append args if provided
//Check if args is available
if (!empty($filter['args'])) {
//Append args if provided
unset($tool, $reflection);
}
} else {
unset($tool, $reflection);
}
} else {
- #TODO: trigger error about empty filters ?
+ //Trigger error about empty filters
+ //XXX: There may be a legitimate case where filters is empty, if so please contact the author
+ throw new RuntimeError('Empty filters token', $token->getLine(), $stream->getSourceContext());
+ }
+
+ //Retrieve asset uri
+ //XXX: this path is the merge of services.assets.path_package.arguments[0] and rapsys_pack.output.(css,img,js)
+ if (($outputUrl = $this->package->getUrl($output)) === false) {
+ throw new RuntimeError(sprintf('Unable to get url for asset: %s', $output), $token->getLine(), $stream->getSourceContext());
- //Create output dir on demand
- if (!is_dir($parent = $dir = dirname($this->prefix.$output))) {
+ //Check if we have a bundle path
+ if ($output[0] == '@') {
+ //Check that we have a / separator between bundle name and path
+ if (($pos = strpos($output, '/')) === false) {
+ throw new RuntimeError(sprintf('Invalid output path "%s"', $output), $token->getLine(), $stream->getSourceContext());
+ }
+ //Resolve bundle prefix
+ $output = $this->locator->locate(substr($output, 0, $pos)).substr($output, $pos + 1);
+ }
+
+ //Create output dir if not present
+ if (!is_dir($dir = dirname($output))) {
try {
//XXX: set as 0777, symfony umask (0022) will reduce rights (0755)
mkdir($dir, 0777, true);
} catch (\Exception $e) {
try {
//XXX: set as 0777, symfony umask (0022) will reduce rights (0755)
mkdir($dir, 0777, true);
} catch (\Exception $e) {
- throw new \Twig_Error_Syntax(sprintf('Unable to create directory: %s', $dir), $token->getLine(), $stream->getSourceContext());
+ throw new RuntimeError(sprintf('Output directory "%s" do not exists and unable to create it', $dir), $token->getLine(), $stream->getSourceContext());
- //XXX: atomic rotation is required to avoid partial content in reverse cache
- if (file_put_contents($this->prefix.$output.'.new', $content) === false) {
- throw new \Twig_Error_Syntax(sprintf('Unable to write to: %s', $prefix.$output.'.new'), $token->getLine(), $stream->getSourceContext());
+ //XXX: to avoid partial content in reverse cache we use atomic rotation write, unlink and move
+ try {
+ if (file_put_contents($output.'.new', $content) === false) {
+ throw new \Exception();
+ }
+ } catch(\Exception $e) {
+ throw new RuntimeError(sprintf('Unable to write to: %s', $output.'.new'), $token->getLine(), $stream->getSourceContext());
- if (is_file($this->prefix.$output) && unlink($this->prefix.$output) === false) {
- throw new \Twig_Error_Syntax(sprintf('Unable to unlink: %s', $prefix.$output), $token->getLine(), $stream->getSourceContext());
+ if (is_file($output)) {
+ try {
+ if (unlink($output) === false) {
+ throw new \Exception();
+ }
+ } catch (\Exception $e) {
+ throw new RuntimeError(sprintf('Unable to unlink: %s', $output), $token->getLine(), $stream->getSourceContext());
+ }
- if (rename($this->prefix.$output.'.new', $this->prefix.$output) === false) {
- throw new \Twig_Error_Syntax(sprintf('Unable to rename: %s to %s', $prefix.$output.'.new', $prefix.$output), $token->getLine(), $stream->getSourceContext());
+ try {
+ if (rename($output.'.new', $output) === false) {
+ throw new \Exception();
+ }
+ } catch (\Exception $e) {
+ throw new RuntimeError(sprintf('Unable to rename: %s to %s', $output.'.new', $output), $token->getLine(), $stream->getSourceContext());
- //Retrieve asset uri
- //XXX: was next line to support module specific asset configuration
- #if (($output = $this->assetsPackages->getUrl($output, 'rapsys_pack')) === false) {
- if (($output = $this->assetsPackages->getUrl($output)) === false) {
- #throw new \Twig_Error_Syntax(sprintf('Unable to get url for asset: %s with package %s', $output, 'rapsys_pack'), $token->getLine(), $stream->getSourceContext());
- throw new \Twig_Error_Syntax(sprintf('Unable to get url for asset: %s', $output), $token->getLine(), $stream->getSourceContext());
- }
+ //Set name in context key
+ $ref = new AssignNameExpression($name, $token->getLine());
+
+ //Set output in context value
+ $value = new TextNode($outputUrl, $token->getLine());
- //Send pack node
- return new PackNode(array('value' => $body), array('inputs' => $inputs, 'filters' => $filters, 'name' => $name, 'output' => $output), $token->getLine(), $this->getTag());
+ //Send body with context set
+ return new Node([
+ //This define name in twig template by prepending $context['<name>'] = '<output>';
+ new SetNode(true, $ref, $value, $token->getLine(), $this->getTag()),
+ //The tag captured body
+ $body
+ ]);
- public function testEndTag(\Twig_Token $token) {
- return $token->test(array('end'.$this->getTag()));
+ /**
+ * Test for tag end
+ *
+ * @param class $token The \Twig\Token instance
+ *
+ * @return bool
+ */
+ public function testEndTag(Token $token) {
+ return $token->test(['end'.$this->getTag()]);