From: Raphaƫl Gertz <git@rapsys.eu>
Date: Sun, 8 Dec 2024 06:49:54 +0000 (+0100)
Subject: Define member variables
X-Git-Tag: 0.5.4~3
X-Git-Url: https://git.rapsys.eu/packbundle/commitdiff_plain/3bc9a721a4d6aaa777029a064fb48eb6f568e4e6

Define member variables
Drop ctx member variable
Improve bundle and file extraction
Improve bundle public directory detection
Use slugger hash instead of assetic like hashing
Use route instead of outputUrl
Cleanup
---

diff --git a/Parser/TokenParser.php b/Parser/TokenParser.php
index 67e87eb..7d73117 100644
--- a/Parser/TokenParser.php
+++ b/Parser/TokenParser.php
@@ -11,12 +11,19 @@
 
 namespace Rapsys\PackBundle\Parser;
 
+use Psr\Container\ContainerInterface;
+
 use Rapsys\PackBundle\RapsysPackBundle;
+use Rapsys\PackBundle\Util\SluggerUtil;
 
 use Symfony\Component\Asset\PackageInterface;
 use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
 use Symfony\Component\Filesystem\Filesystem;
 use Symfony\Component\HttpKernel\Config\FileLocator;
+use Symfony\Component\Routing\Exception\InvalidParameterException;
+use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
+use Symfony\Component\Routing\Exception\RouteNotFoundException;
+use Symfony\Component\Routing\RouterInterface;
 
 use Twig\Error\Error;
 use Twig\Node\Expression\AssignNameExpression;
@@ -32,32 +39,49 @@ use Twig\TokenParser\AbstractTokenParser;
  */
 class TokenParser extends AbstractTokenParser {
 	/**
-	 * The stream context instance
+	 * Filters array
+	 */
+	protected array $filters;
+
+	/**
+	 * Output string
+	 */
+	protected string $output;
+
+	/**
+	 * Route string
+	 */
+	protected string $route;
+
+	/**
+	 * Token string
 	 */
-	protected mixed $ctx;
+	protected string $token;
 
 	/**
 	 * Constructor
 	 *
+	 * @param ContainerInterface $container The ContainerInterface instance
 	 * @param FileLocator $locator The FileLocator instance
-	 * @param PackageInterface $package The Assets Package instance
-	 * @param string $token The token name
+	 * @param RouterInterface $router The RouterInterface instance
+	 * @param SluggerUtil $slugger The SluggerUtil instance
+	 * @param array $config The config
+	 * @param mixed $ctx The context stream instance
+	 * @param string $prefix The output prefix
 	 * @param string $tag The tag name
-	 * @param string $output The default output string
-	 * @param array $filters The default filter array
 	 */
-	public function __construct(protected FileLocator $locator, protected PackageInterface $package, protected string $token, protected string $tag, protected string $output, protected array $filters) {
-		//Set ctx
-		$this->ctx = stream_context_create(
-			[
-				'http' => [
-					#'header' => ['Referer: https://www.openstreetmap.org/'],
-					'max_redirects' => $_ENV['RAPSYSPACK_REDIRECT'] ?? 20,
-					'timeout' => $_ENV['RAPSYSPACK_TIMEOUT'] ?? (($timeout = ini_get('default_socket_timeout')) !== false && $timeout !== "" ? (float)$timeout : 60),
-					'user_agent' => $_ENV['RAPSYSPACK_AGENT'] ?? (($agent = ini_get('user_agent')) !== false && $agent !== "" ? (string)$agent : RapsysPackBundle::getAlias().'/'.RapsysPackBundle::getVersion())
-				]
-			]
-		);
+	public function __construct(protected ContainerInterface $container, protected FileLocator $locator, protected RouterInterface $router, protected SluggerUtil $slugger, protected array $config, protected mixed $ctx, protected string $prefix, protected string $tag) {
+		//Set filters
+		$this->filters = $config['filters'][$prefix];
+
+		//Set output
+		$this->output = $config['public'].'/'.$config['prefixes']['pack'].'/'.$config['prefixes'][$prefix].'/*.'.$prefix;
+
+		//Set route
+		$this->route = $config['routes'][$prefix];
+
+		//Set token
+		$this->token = $config['tokens'][$prefix];
 	}
 
 	/**
@@ -96,7 +120,7 @@ class TokenParser extends AbstractTokenParser {
 		while (!$stream->test(Token::BLOCK_END_TYPE)) {
 			//The files to process
 			if ($stream->test(Token::STRING_TYPE)) {
-				//'somewhere/somefile.(css,img,js)' 'somewhere/*' '@jquery'
+				//'somewhere/somefile.(css|img|js)' 'somewhere/*' '@jquery'
 				$inputs[] = $stream->next()->getValue();
 			//The filters token
 			} elseif ($stream->test(Token::NAME_TYPE, 'filters')) {
@@ -104,12 +128,19 @@ class TokenParser extends AbstractTokenParser {
 				$stream->next();
 				$stream->expect(Token::OPERATOR_TYPE, '=');
 				$this->filters = array_merge($this->filters, array_filter(array_map('trim', explode(',', $stream->expect(Token::STRING_TYPE)->getValue()))));
+			//The route token
+			} elseif ($stream->test(Token::NAME_TYPE, 'route')) {
+				//output='rapsyspack_css' OR output='rapsyspack_js' OR output='rapsyspack_img'
+				$stream->next();
+				$stream->expect(Token::OPERATOR_TYPE, '=');
+				$this->route = $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(Token::OPERATOR_TYPE, '=');
 				$this->output = $stream->expect(Token::STRING_TYPE)->getValue();
+			//TODO: add format ? jpeg|png|gif|webp|webm ???
 			//The token name
 			} elseif ($stream->test(Token::NAME_TYPE, 'token')) {
 				//name='core_js'
@@ -119,6 +150,7 @@ class TokenParser extends AbstractTokenParser {
 			//Unexpected token
 			} else {
 				$token = $stream->getCurrent();
+				//Throw error
 				throw new Error(sprintf('Unexpected token "%s" of value "%s"', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $stream->getSourceContext());
 			}
 		}
@@ -132,12 +164,25 @@ class TokenParser extends AbstractTokenParser {
 		//Process end block
 		$stream->expect(Token::BLOCK_END_TYPE);
 
-		//Replace star with sha1
-		if (($pos = strpos($this->output, '*')) !== false) {
-			//XXX: assetic use substr(sha1(serialize($inputs).serialize($this->filters).serialize($this->output)), 0, 7)
-			$this->output = substr($this->output, 0, $pos).sha1(serialize($inputs).serialize($this->filters)).substr($this->output, $pos + 1);
+		//Without valid output
+		if (($pos = strpos($this->output, '*')) === false || $pos !== strrpos($this->output, '*')) {
+			//Throw error
+			throw new Error(sprintf('Invalid output "%s"', $this->output), $token->getLine(), $stream->getSourceContext());
+		}
+
+		//Without existing route
+		if ($this->router->getRouteCollection()->get($this->route) === null) {
+			//Throw error
+			throw new Error(sprintf('Invalid route "%s"', $this->route), $token->getLine(), $stream->getSourceContext());
 		}
 
+		//Set file
+		//XXX: assetic use substr(sha1(serialize($inputs).serialize($this->filters).serialize($this->output)), 0, 7)
+		$file = $this->slugger->hash([$inputs, $this->filters, $this->output, $this->route, $this->token]);
+
+		//Replace star by file
+		$this->output = substr($this->output, 0, $pos).$file.substr($this->output, $pos + 1);
+
 		//Process inputs
 		for($k = 0; $k < count($inputs); $k++) {
 			//Deal with generic url
@@ -161,6 +206,7 @@ class TokenParser extends AbstractTokenParser {
 					foreach($replacement as $input) {
 						//Check that it's a file
 						if (!is_file($input)) {
+							//Throw error
 							throw new Error(sprintf('Input path "%s" from "%s" is not a file', $input, $inputs[$k]), $token->getLine(), $stream->getSourceContext());
 						}
 					}
@@ -172,17 +218,21 @@ class TokenParser extends AbstractTokenParser {
 					$k += count($replacement) - 1;
 				//Check that it's a file
 				} elseif (!is_file($inputs[$k])) {
+					//Throw error
 					throw new Error(sprintf('Input path "%s" is not a file', $inputs[$k]), $token->getLine(), $stream->getSourceContext());
 				}
 			}
 		}
 
+		#TODO: move the inputs reading from here to inside the filters ?
+
 		//Check inputs
 		if (!empty($inputs)) {
 			//Retrieve files content
 			foreach($inputs as $input) {
 				//Try to retrieve content
 				if (($data = file_get_contents($input, false, $this->ctx)) === false) {
+					//Throw error
 					throw new Error(sprintf('Unable to retrieve input path "%s"', $input), $token->getLine(), $stream->getSourceContext());
 				}
 
@@ -229,12 +279,6 @@ class TokenParser extends AbstractTokenParser {
 			#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 rapsyspack.output.(css,img,js)
-		if (($outputUrl = $this->package->getUrl($this->output)) === false) {
-			throw new Error(sprintf('Unable to get url for asset: %s', $this->output), $token->getLine(), $stream->getSourceContext());
-		}
-
 		//Check if we have a bundle path
 		if ($this->output[0] == '@') {
 			//Resolve it
@@ -264,14 +308,31 @@ class TokenParser extends AbstractTokenParser {
 			$filesystem->dumpFile($this->output, $content);
 		} catch (IOExceptionInterface $e) {
 			//Throw error
-			throw new Error(sprintf('Unable to write to: %s', $this->output), $token->getLine(), $stream->getSourceContext(), $e);
+			throw new Error(sprintf('Unable to write "%s"', $this->output), $token->getLine(), $stream->getSourceContext(), $e);
+		}
+
+		//Without output file mtime
+		if (($mtime = filemtime($this->output)) === false) {
+			//Throw error
+			throw new Error(sprintf('Unable to get "%s" mtime', $this->output), $token->getLine(), $stream->getSourceContext(), $e);
+		}
+
+		//TODO: get mimetype for images ? and set _format ?
+
+		try {
+			//Generate asset url
+			$asset = $this->router->generate($this->route, [ 'file' => $file, 'u' => $mtime ]);
+		//Catch router exceptions
+		} catch (RouteNotFoundException|MissingMandatoryParametersException|InvalidParameterException $e) {
+			//Throw error
+			throw new Error(sprintf('Unable to generate asset route "%s"', $this->route), $token->getLine(), $stream->getSourceContext(), $e);
 		}
 
 		//Set name in context key
 		$ref = new AssignNameExpression($this->token, $token->getLine());
 
 		//Set output in context value
-		$value = new TextNode($outputUrl, $token->getLine());
+		$value = new TextNode($asset, $token->getLine());
 
 		//Send body with context set
 		return new Node([
@@ -312,62 +373,50 @@ class TokenParser extends AbstractTokenParser {
 			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), $lineno, $source);
+		//Extract bundle
+		if (($bundle = strstr($file, '/', true)) === false) {
+			throw new Error(sprintf('Invalid bundle "%s"', $file), $lineno, $source);
 		}
 
-		//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;
-			}
+		//Extract path
+		if (($path = strstr($file, '/')) === false) {
+			throw new Error(sprintf('Invalid path "%s"', $file), $lineno, $source);
 		}
 
-		//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'));
-			}
+		//Extract alias
+		$alias = strtolower(substr($bundle, 1));
 
-			//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));
-				}
+		//With public parameter
+		if ($this->container->hasParameter($alias.'.public')) {
+			//Set prefix
+			$prefix = $this->container->getParameter($alias.'.public');
+		//Without public parameter
+		} else {
+			//Without 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
+				$bundle .= 'Bundle';
 			}
 
-			//Resolve fixed bundle prefix
+			//Try to resolve bundle prefix
 			try {
 				$prefix = $this->locator->locate($bundle);
-				//Catch bundle does not exist or is not enabled exception again
+			//Catch bundle does not exist or is not enabled exception
 			} 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), $lineno, $source, $e);
+				throw new Error(sprintf('Unlocatable bundle "%s"', $bundle), $lineno, $source, $e);
+			}
+
+			//With Resources/public subdirectory
+			if (is_dir($prefix.'Resources/public')) {
+				$prefix .= 'Resources/public';
+			//With public subdirectory
+			} elseif (is_dir($prefix.'public')) {
+				$prefix .= 'public';
+			//Without any public subdirectory
+			} else {
+				throw new Error(sprintf('Bundle "%s" lacks a public subdirectory', $bundle), $lineno, $source, $e);
 			}
 		}