X-Git-Url: https://git.rapsys.eu/packbundle/blobdiff_plain/2c3f6d889b2f7825120cd42b019dbf9fa29b6594..3fdef4d1590ac30c760dbe80c62416a07f182aca:/Twig/PackTokenParser.php diff --git a/Twig/PackTokenParser.php b/Twig/PackTokenParser.php index 3c54a14..3319ab2 100644 --- a/Twig/PackTokenParser.php +++ b/Twig/PackTokenParser.php @@ -2,30 +2,32 @@ namespace Rapsys\PackBundle\Twig; -use Symfony\Component\HttpKernel\Config\FileLocator; use Symfony\Component\Asset\PackageInterface; -use Twig\Error\RuntimeError; -use Twig\Error\SyntaxError; +use Symfony\Component\Filesystem\Exception\IOExceptionInterface; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpKernel\Config\FileLocator; +use Twig\Error\Error; use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Node; use Twig\Node\SetNode; use Twig\Node\TextNode; +use Twig\Source; use Twig\Token; use Twig\TokenParser\AbstractTokenParser; class PackTokenParser extends AbstractTokenParser { - //The tag name - private $tag; + ///The tag name + protected $tag; /** * Constructor * - * @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 + * @param FileLocator locator The FileLocator instance + * @param PackageInterface package The Assets Package instance + * @param array 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 $locator, PackageInterface $package, $config, $tag, $output, $filters) { //Save locator @@ -61,6 +63,8 @@ class PackTokenParser extends AbstractTokenParser { /** * Get the tag name + * + * @return string This tag name */ public function getTag() { return $this->tag; @@ -69,9 +73,9 @@ class PackTokenParser extends AbstractTokenParser { /** * Parse the token * - * @param class $token The \Twig\Token instance + * @param Token token The \Twig\Token instance * - * @return class The PackNode + * @return Node The PackNode * * @todo see if we can't add a debug mode behaviour * @@ -123,7 +127,7 @@ class PackTokenParser extends AbstractTokenParser { //Unexpected token } else { $token = $stream->getCurrent(); - throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s"', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $stream->getSourceContext()); + throw new Error(sprintf('Unexpected token "%s" of value "%s"', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $stream->getSourceContext()); } } @@ -154,21 +158,10 @@ class PackTokenParser extends AbstractTokenParser { } elseif (strpos($inputs[$k], '://') === false) { //Check if we have a bundle path if ($inputs[$k][0] == '@') { - //Check that we have a / separator between bundle name and path - if (($pos = strpos($inputs[$k], '/')) === false) { - //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->locator->locate(substr($inputs[$k], 0, $pos)).substr($inputs[$k], $pos + 1); + //Resolve it + $inputs[$k] = $this->getLocated($inputs[$k], $token->getLine(), $stream->getSourceContext()); } + //Deal with globs if (strpos($inputs[$k], '*') !== false || (($a = strpos($inputs[$k], '{')) !== false && ($b = strpos($inputs[$k], ',', $a)) !== false && strpos($inputs[$k], '}', $b) !== false)) { //Get replacement @@ -177,7 +170,7 @@ class PackTokenParser extends AbstractTokenParser { foreach($replacement as $input) { //Check that it's a file if (!is_file($input)) { - throw new RuntimeError(sprintf('Input path "%s" from "%s" is not a file', $input, $inputs[$k]), $token->getLine(), $stream->getSourceContext()); + throw new Error(sprintf('Input path "%s" from "%s" is not a file', $input, $inputs[$k]), $token->getLine(), $stream->getSourceContext()); } } //Replace with glob path @@ -186,7 +179,7 @@ class PackTokenParser extends AbstractTokenParser { $k += count($replacement) - 1; //Check that it's a file } elseif (!is_file($inputs[$k])) { - throw new RuntimeError(sprintf('Input path "%s" is not a file', $inputs[$k]), $token->getLine(), $stream->getSourceContext()); + throw new Error(sprintf('Input path "%s" is not a file', $inputs[$k]), $token->getLine(), $stream->getSourceContext()); } } } @@ -208,15 +201,18 @@ class PackTokenParser extends AbstractTokenParser { foreach($inputs as $input) { //Try to retrieve content if (($data = file_get_contents($input, false, $ctx)) === false) { - throw new RuntimeError(sprintf('Unable to retrieve input path "%s"', $input), $token->getLine(), $stream->getSourceContext()); + throw new Error(sprintf('Unable to retrieve input path "%s"', $input), $token->getLine(), $stream->getSourceContext()); } //Append content $content .= $data; } } else { - //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()); + //Trigger error about empty inputs ? + //XXX: There may be a legitimate case where we want an empty file or an error, feel free to contact the author in such case + #throw new Error('Empty inputs token', $token->getLine(), $stream->getSourceContext()); + + //Send an empty node without inputs + return new Node(); } //Check filters @@ -240,65 +236,47 @@ class PackTokenParser extends AbstractTokenParser { unset($tool, $reflection); } } else { - //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()); + //Trigger error about empty filters ? + //XXX: There may be a legitimate case where we want only a merged file or an error, feel free to contact the author in such case + #throw new Error('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()); + throw new Error(sprintf('Unable to get url for asset: %s', $output), $token->getLine(), $stream->getSourceContext()); } //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); + //Resolve it + $output = $this->getLocated($output, $token->getLine(), $stream->getSourceContext()); } + //Get filesystem + $filesystem = new Filesystem(); + //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 RuntimeError(sprintf('Output directory "%s" do not exists and unable to create it', $dir), $token->getLine(), $stream->getSourceContext()); + //Create dir + //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) + $filesystem->mkdir($dir, 0775); + } catch (IOExceptionInterface $e) { + //Throw error + throw new Error(sprintf('Output directory "%s" do not exists and unable to create it', $dir), $token->getLine(), $stream->getSourceContext(), $e); } } //Send file content - //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($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 - 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()); + //Write content to file + //XXX: this call is (maybe) atomic + //XXX: see https://symfony.com/doc/current/components/filesystem.html#dumpfile + $filesystem->dumpFile($output, $content); + } catch (IOExceptionInterface $e) { + //Throw error + throw new Error(sprintf('Unable to write to: %s', $output), $token->getLine(), $stream->getSourceContext(), $e); } //Set name in context key @@ -319,11 +297,96 @@ class PackTokenParser extends AbstractTokenParser { /** * Test for tag end * - * @param class $token The \Twig\Token instance + * @param Token token The \Twig\Token instance * * @return bool */ public function testEndTag(Token $token) { return $token->test(['end'.$this->getTag()]); } + + /** + * Get path from bundled file + * + * @param string file The bundled file path + * @param int lineno The template line where the error occurred + * @param Source source The source context where the error occurred + * @param \Exception prev The previous exception + * + * @return string The resolved file path + * + * @todo Try retrive public dir from the member function BundleNameBundle::getPublicDir() return value ? + * @xxx see https://symfony.com/doc/current/bundles.html#overridding-the-bundle-directory-structure + */ + public function getLocated($file, int $lineno = 0, Source $source = null, \Exception $prev = null) { + /*TODO: add a @jquery magic feature ? + if ($file == '@jquery') { + #header('Content-Type: text/plain'); + #var_dump($inputs); + #exit; + return $this->config['jquery']; + }*/ + + //Check that we have a / separator between bundle name and path + if (($pos = strpos($file, '/')) === false) { + throw new Error(sprintf('Invalid path "%s"', $file), $token->getLine(), $stream->getSourceContext()); + } + + //Set bundle + $bundle = substr($file, 0, $pos); + + //Set path + $path = substr($file, $pos + 1); + + //Check for bundle suffix presence + //XXX: use "bundle templates automatic namespace" mimicked behaviour to find intended bundle and/or path + //XXX: see https://symfony.com/doc/4.3/templates.html#bundle-templates + if (strlen($bundle) < strlen('Bundle') || substr($bundle, -strlen('Bundle')) !== 'Bundle') { + //Append Bundle in an attempt to fix it's naming for locator + $bundle .= 'Bundle'; + + //Check for public resource prefix presence + if (strlen($path) < strlen('Resources/public') || substr($path, 0, strlen('Resources/public')) != 'Resources/public') { + //Prepend standard public path + $path = 'Resources/public/'.$path; + } + } + + //Resolve bundle prefix + try { + $prefix = $this->locator->locate($bundle); + //Catch bundle does not exist or is not enabled exception + } catch(\InvalidArgumentException $e) { + //Fix lowercase first bundle character + if ($bundle[1] > 'Z' || $bundle[1] < 'A') { + $bundle[1] = strtoupper($bundle[1]); + } + + //Detect double bundle suffix + if (strlen($bundle) > strlen('_bundleBundle') && substr($bundle, -strlen('_bundleBundle')) == '_bundleBundle') { + //Strip extra bundle + $bundle = substr($bundle, 0, -strlen('Bundle')); + } + + //Convert snake case in camel case + if (strpos($bundle, '_') !== false) { + //Fix every first character following a _ + while(($cur = strpos($bundle, '_')) !== false) { + $bundle = substr($bundle, 0, $cur).ucfirst(substr($bundle, $cur + 1)); + } + } + + //Resolve fixed bundle prefix + try { + $prefix = $this->locator->locate($bundle); + //Catch bundle does not exist or is not enabled exception again + } catch(\InvalidArgumentException $e) { + //Bail out as bundle or path is invalid and we have no way to know what was meant + throw new Error(sprintf('Invalid bundle name "%s" in path "%s". Maybe you meant "%s"', substr($file, 1, $pos - 1), $file, $bundle.'/'.$path), $token->getLine(), $stream->getSourceContext(), $e); + } + } + + //Return solved bundle prefix and path + return $prefix.$path; + } }