From 2c3f6d889b2f7825120cd42b019dbf9fa29b6594 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Tue, 26 Nov 2019 15:56:25 +0100 Subject: [PATCH] Add twig classes uses 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 --- Twig/PackTokenParser.php | 263 ++++++++++++++++++++++++++------------- 1 file changed, 176 insertions(+), 87 deletions(-) diff --git a/Twig/PackTokenParser.php b/Twig/PackTokenParser.php index 54133cf..3c54a14 100644 --- a/Twig/PackTokenParser.php +++ b/Twig/PackTokenParser.php @@ -1,108 +1,150 @@ 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; //Set name - $this->name = $config['name']; + $this->name = $config['name']; //Set scheme - $this->scheme = $config['scheme']; + $this->scheme = $config['scheme']; //Set timeout - $this->timeout = $config['timeout']; + $this->timeout = $config['timeout']; //Set agent - $this->agent = $config['agent']; + $this->agent = $config['agent']; //Set redirect - $this->redirect = $config['redirect']; + $this->redirect = $config['redirect']; //Set tag - $this->tag = $tag; + $this->tag = $tag; //Set output - $this->output = $output; + $this->output = $output; //Set filters - $this->filters = $filters; + $this->filters = $filters; } + /** + * Get the tag name + */ 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(); - $inputs = array(); + $inputs = []; $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(); - } 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->next(); - $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->next(); - $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->next(); - $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(); - 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) { - #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); } - //Deal with inputs + //Process inputs for($k = 0; $k < count($inputs); $k++) { //Deal with generic url if (strpos($inputs[$k], '//') === 0) { @@ -112,18 +154,20 @@ class PackTokenParser extends \Twig_TokenParser { } 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) { - #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 - $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)) { @@ -131,8 +175,9 @@ class PackTokenParser extends \Twig_TokenParser { $replacement = glob($inputs[$k], GLOB_NOSORT|GLOB_BRACE); //Check that these are working files foreach($replacement as $input) { + //Check that it's a file if (!is_file($input)) { - 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 @@ -141,20 +186,20 @@ class PackTokenParser extends \Twig_TokenParser { $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( - array( - 'http' => array( + [ + 'http' => [ 'timeout' => $this->timeout, 'user_agent' => $this->agent, 'redirect' => $this->redirect, - ) - ) + ] + ] ); //Check inputs @@ -163,13 +208,15 @@ class PackTokenParser extends \Twig_TokenParser { 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 { - #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()); } //Check filters @@ -177,7 +224,7 @@ class PackTokenParser extends \Twig_TokenParser { //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 @@ -193,48 +240,90 @@ class PackTokenParser extends \Twig_TokenParser { 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) { - 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()); } } //Send file content - //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()); } //Remove old file - 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()); + } } //Rename it - 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[''] = ''; + 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()]); } } -- 2.41.0