Skip to content

Commit

Permalink
Define member variables
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Raphaël Gertz committed Dec 8, 2024
1 parent 5cf3827 commit 3bc9a72
Showing 1 changed file with 126 additions and 77 deletions.
203 changes: 126 additions & 77 deletions Parser/TokenParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,32 +39,49 @@
*/
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];
}

/**
Expand Down Expand Up @@ -96,20 +120,27 @@ public function parse(Token $token): Node {
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')) {
//filter='yui_js'
$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'
Expand All @@ -119,6 +150,7 @@ public function parse(Token $token): Node {
//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());
}
}
Expand All @@ -132,12 +164,25 @@ public function parse(Token $token): Node {
//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
Expand All @@ -161,6 +206,7 @@ public function parse(Token $token): Node {
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());
}
}
Expand All @@ -172,17 +218,21 @@ public function parse(Token $token): Node {
$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());
}

Expand Down Expand Up @@ -229,12 +279,6 @@ public function parse(Token $token): Node {
#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
Expand Down Expand Up @@ -264,14 +308,31 @@ public function parse(Token $token): Node {
$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([
Expand Down Expand Up @@ -312,62 +373,50 @@ public function getLocated(string $file, int $lineno = 0, ?Source $source = null
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);
}
}

Expand Down

0 comments on commit 3bc9a72

Please sign in to comment.