diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index 2caedb4e21b74..82171117ee739 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -696,6 +696,10 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode, callable $enabl
->{$enableIfStandalone('symfony/asset', Package::class)}()
->fixXmlConfig('base_url')
->children()
+ ->booleanNode('strict_mode')
+ ->info('Throw an exception if an entry is missing from the manifest.json')
+ ->defaultFalse()
+ ->end()
->scalarNode('version_strategy')->defaultNull()->end()
->scalarNode('version')->defaultNull()->end()
->scalarNode('version_format')->defaultValue('%%s?%%s')->end()
@@ -733,6 +737,10 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode, callable $enabl
->prototype('array')
->fixXmlConfig('base_url')
->children()
+ ->booleanNode('strict_mode')
+ ->info('Throw an exception if an entry is missing from the manifest.json')
+ ->defaultFalse()
+ ->end()
->scalarNode('version_strategy')->defaultNull()->end()
->scalarNode('version')
->beforeNormalization()
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 82267d24a87b4..3a5aa40ea7b09 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -1141,7 +1141,7 @@ private function registerAssetsConfiguration(array $config, ContainerBuilder $co
if ($config['version_strategy']) {
$defaultVersion = new Reference($config['version_strategy']);
} else {
- $defaultVersion = $this->createVersion($container, $config['version'], $config['version_format'], $config['json_manifest_path'], '_default');
+ $defaultVersion = $this->createVersion($container, $config['version'], $config['version_format'], $config['json_manifest_path'], '_default', $config['strict_mode']);
}
$defaultPackage = $this->createPackageDefinition($config['base_path'], $config['base_urls'], $defaultVersion);
@@ -1157,7 +1157,7 @@ private function registerAssetsConfiguration(array $config, ContainerBuilder $co
// let format fallback to main version_format
$format = $package['version_format'] ?: $config['version_format'];
$version = $package['version'] ?? null;
- $version = $this->createVersion($container, $version, $format, $package['json_manifest_path'], $name);
+ $version = $this->createVersion($container, $version, $format, $package['json_manifest_path'], $name, $package['strict_mode']);
}
$packageDefinition = $this->createPackageDefinition($package['base_path'], $package['base_urls'], $version)
@@ -1186,7 +1186,7 @@ private function createPackageDefinition(?string $basePath, array $baseUrls, Ref
return $package;
}
- private function createVersion(ContainerBuilder $container, ?string $version, ?string $format, ?string $jsonManifestPath, string $name): Reference
+ private function createVersion(ContainerBuilder $container, ?string $version, ?string $format, ?string $jsonManifestPath, string $name, bool $strictMode): Reference
{
// Configuration prevents $version and $jsonManifestPath from being set
if (null !== $version) {
@@ -1203,6 +1203,7 @@ private function createVersion(ContainerBuilder $container, ?string $version, ?s
if (null !== $jsonManifestPath) {
$def = new ChildDefinition('assets.json_manifest_version_strategy');
$def->replaceArgument(0, $jsonManifestPath);
+ $def->replaceArgument(2, $strictMode);
$container->setDefinition('assets._version_'.$name, $def);
return new Reference('assets._version_'.$name);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.php
index a6f278743a75f..1e250aab4dceb 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.php
@@ -80,6 +80,7 @@
->args([
abstract_arg('manifest path'),
service('http_client')->nullOnInvalid(),
+ false,
])
->set('assets.remote_json_manifest_version_strategy', RemoteJsonManifestVersionStrategy::class)
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
index edcaca3a90018..5082c3356a673 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
@@ -155,6 +155,7 @@
+
@@ -168,6 +169,7 @@
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
index 24220cc5e7629..9d1f5ea421d37 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
@@ -86,6 +86,7 @@ public function testAssetsCanBeEnabled()
'base_urls' => [],
'packages' => [],
'json_manifest_path' => null,
+ 'strict_mode' => false,
];
$this->assertEquals($defaultConfig, $config['assets']);
@@ -489,6 +490,7 @@ protected static function getBundleDefaultConfig()
'base_urls' => [],
'packages' => [],
'json_manifest_path' => null,
+ 'strict_mode' => false,
],
'cache' => [
'pools' => [],
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets.php
index ab16a52e21e9b..f26621001c9ec 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets.php
@@ -36,6 +36,10 @@
'env_manifest' => [
'json_manifest_path' => '%env(env_manifest)%',
],
+ 'strict_manifest_strategy' => [
+ 'json_manifest_path' => '/path/to/manifest.json',
+ 'strict_mode' => true,
+ ],
],
],
]);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets.xml
index ae0e0e099bc93..dadee4529d8b5 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets.xml
@@ -25,6 +25,7 @@
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets.yml
index ab9eb1b610ce8..cfd4f07b04346 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets.yml
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets.yml
@@ -25,6 +25,9 @@ framework:
json_manifest_path: '%var_json_manifest_path%'
env_manifest:
json_manifest_path: '%env(env_manifest)%'
+ strict_manifest_strategy:
+ json_manifest_path: '/path/to/manifest.json'
+ strict_mode: true
parameters:
var_json_manifest_path: 'https://cdn.example.com/manifest.json'
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
index b764b2b779a71..e20736f7b9d69 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
@@ -632,7 +632,7 @@ public function testAssets()
// packages
$packageTags = $container->findTaggedServiceIds('assets.package');
- $this->assertCount(9, $packageTags);
+ $this->assertCount(10, $packageTags);
$packages = [];
foreach ($packageTags as $serviceId => $tagAttributes) {
@@ -658,6 +658,7 @@ public function testAssets()
$versionStrategy = $container->getDefinition((string) $package->getArgument(1));
$this->assertEquals('assets.json_manifest_version_strategy', $versionStrategy->getParent());
$this->assertEquals('/path/to/manifest.json', $versionStrategy->getArgument(0));
+ $this->assertFalse($versionStrategy->getArgument(2));
$package = $container->getDefinition($packages['remote_manifest']);
$versionStrategy = $container->getDefinition($package->getArgument(1));
@@ -668,11 +669,19 @@ public function testAssets()
$versionStrategy = $container->getDefinition($package->getArgument(1));
$this->assertSame('assets.json_manifest_version_strategy', $versionStrategy->getParent());
$this->assertSame('https://cdn.example.com/manifest.json', $versionStrategy->getArgument(0));
+ $this->assertFalse($versionStrategy->getArgument(2));
$package = $container->getDefinition($packages['env_manifest']);
$versionStrategy = $container->getDefinition($package->getArgument(1));
$this->assertSame('assets.json_manifest_version_strategy', $versionStrategy->getParent());
$this->assertStringMatchesFormat('env_%s', $versionStrategy->getArgument(0));
+ $this->assertFalse($versionStrategy->getArgument(2));
+
+ $package = $container->getDefinition((string) $packages['strict_manifest_strategy']);
+ $versionStrategy = $container->getDefinition((string) $package->getArgument(1));
+ $this->assertEquals('assets.json_manifest_version_strategy', $versionStrategy->getParent());
+ $this->assertEquals('/path/to/manifest.json', $versionStrategy->getArgument(0));
+ $this->assertTrue($versionStrategy->getArgument(2));
}
public function testAssetsDefaultVersionStrategyAsService()
diff --git a/src/Symfony/Component/Asset/Exception/AssetNotFoundException.php b/src/Symfony/Component/Asset/Exception/AssetNotFoundException.php
new file mode 100644
index 0000000000000..954294bdc1fba
--- /dev/null
+++ b/src/Symfony/Component/Asset/Exception/AssetNotFoundException.php
@@ -0,0 +1,41 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Asset\Exception;
+
+/**
+ * Represents an asset not found in a manifest.
+ */
+class AssetNotFoundException extends RuntimeException
+{
+ private $alternatives;
+
+ /**
+ * @param string $message Exception message to throw
+ * @param array $alternatives List of similar defined names
+ * @param int $code Exception code
+ * @param \Throwable $previous Previous exception used for the exception chaining
+ */
+ public function __construct(string $message, array $alternatives = [], int $code = 0, \Throwable $previous = null)
+ {
+ parent::__construct($message, $code, $previous);
+
+ $this->alternatives = $alternatives;
+ }
+
+ /**
+ * @return array A list of similar defined names
+ */
+ public function getAlternatives(): array
+ {
+ return $this->alternatives;
+ }
+}
diff --git a/src/Symfony/Component/Asset/Exception/RuntimeException.php b/src/Symfony/Component/Asset/Exception/RuntimeException.php
new file mode 100644
index 0000000000000..13dc58b4d3f74
--- /dev/null
+++ b/src/Symfony/Component/Asset/Exception/RuntimeException.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Asset\Exception;
+
+/**
+ * Base RuntimeException for the Asset component.
+ */
+class RuntimeException extends \RuntimeException implements ExceptionInterface
+{
+}
diff --git a/src/Symfony/Component/Asset/Tests/VersionStrategy/JsonManifestVersionStrategyTest.php b/src/Symfony/Component/Asset/Tests/VersionStrategy/JsonManifestVersionStrategyTest.php
index 57f1618dda30c..d3748bb7532bc 100644
--- a/src/Symfony/Component/Asset/Tests/VersionStrategy/JsonManifestVersionStrategyTest.php
+++ b/src/Symfony/Component/Asset/Tests/VersionStrategy/JsonManifestVersionStrategyTest.php
@@ -12,6 +12,8 @@
namespace Symfony\Component\Asset\Tests\VersionStrategy;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Asset\Exception\AssetNotFoundException;
+use Symfony\Component\Asset\Exception\RuntimeException;
use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
@@ -19,7 +21,7 @@
class JsonManifestVersionStrategyTest extends TestCase
{
/**
- * @dataProvider ProvideValidStrategies
+ * @dataProvider provideValidStrategies
*/
public function testGetVersion(JsonManifestVersionStrategy $strategy)
{
@@ -27,7 +29,7 @@ public function testGetVersion(JsonManifestVersionStrategy $strategy)
}
/**
- * @dataProvider ProvideValidStrategies
+ * @dataProvider provideValidStrategies
*/
public function testApplyVersion(JsonManifestVersionStrategy $strategy)
{
@@ -35,7 +37,7 @@ public function testApplyVersion(JsonManifestVersionStrategy $strategy)
}
/**
- * @dataProvider ProvideValidStrategies
+ * @dataProvider provideValidStrategies
*/
public function testApplyVersionWhenKeyDoesNotExistInManifest(JsonManifestVersionStrategy $strategy)
{
@@ -43,20 +45,31 @@ public function testApplyVersionWhenKeyDoesNotExistInManifest(JsonManifestVersio
}
/**
- * @dataProvider ProvideMissingStrategies
+ * @dataProvider provideStrictStrategies
+ */
+ public function testStrictExceptionWhenKeyDoesNotExistInManifest(JsonManifestVersionStrategy $strategy, $path, $message)
+ {
+ $this->expectException(AssetNotFoundException::class);
+ $this->expectExceptionMessageMatches($message);
+
+ $strategy->getVersion($path);
+ }
+
+ /**
+ * @dataProvider provideMissingStrategies
*/
public function testMissingManifestFileThrowsException(JsonManifestVersionStrategy $strategy)
{
- $this->expectException(\RuntimeException::class);
+ $this->expectException(RuntimeException::class);
$strategy->getVersion('main.js');
}
/**
- * @dataProvider ProvideInvalidStrategies
+ * @dataProvider provideInvalidStrategies
*/
public function testManifestFileWithBadJSONThrowsException(JsonManifestVersionStrategy $strategy)
{
- $this->expectException(\RuntimeException::class);
+ $this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Error parsing JSON');
$strategy->getVersion('main.js');
}
@@ -100,4 +113,21 @@ public function provideStrategies(string $manifestPath)
yield [new JsonManifestVersionStrategy(__DIR__.'/../fixtures/'.$manifestPath)];
}
+
+ public function provideStrictStrategies()
+ {
+ $strategy = new JsonManifestVersionStrategy(__DIR__.'/../fixtures/manifest-valid.json', null, true);
+
+ yield [
+ $strategy,
+ 'css/styles.555def.css',
+ '~Asset "css/styles.555def.css" not found in manifest "(.*)/manifest-valid.json"\. Did you mean one of these\? "css/styles.css", "css/style.css".~',
+ ];
+
+ yield [
+ $strategy,
+ 'img/avatar.png',
+ '~Asset "img/avatar.png" not found in manifest "(.*)/manifest-valid.json"\.~',
+ ];
+ }
}
diff --git a/src/Symfony/Component/Asset/Tests/fixtures/manifest-valid.json b/src/Symfony/Component/Asset/Tests/fixtures/manifest-valid.json
index 546a0066d31ee..9bdbcc7b351ab 100644
--- a/src/Symfony/Component/Asset/Tests/fixtures/manifest-valid.json
+++ b/src/Symfony/Component/Asset/Tests/fixtures/manifest-valid.json
@@ -1,4 +1,6 @@
{
"main.js": "main.123abc.js",
- "css/styles.css": "css/styles.555def.css"
+ "css/styles.css": "css/styles.555def.css",
+ "css/style.css": "css/style.abcdef.css",
+ "main/home.css": "main/home.css"
}
diff --git a/src/Symfony/Component/Asset/VersionStrategy/JsonManifestVersionStrategy.php b/src/Symfony/Component/Asset/VersionStrategy/JsonManifestVersionStrategy.php
index 256bc21963720..3840fea59f2e4 100644
--- a/src/Symfony/Component/Asset/VersionStrategy/JsonManifestVersionStrategy.php
+++ b/src/Symfony/Component/Asset/VersionStrategy/JsonManifestVersionStrategy.php
@@ -11,6 +11,10 @@
namespace Symfony\Component\Asset\VersionStrategy;
+use Symfony\Component\Asset\Exception\AssetNotFoundException;
+use Symfony\Component\Asset\Exception\LogicException;
+use Symfony\Component\Asset\Exception\RuntimeException;
+use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -30,17 +34,20 @@ class JsonManifestVersionStrategy implements VersionStrategyInterface
private $manifestPath;
private $manifestData;
private $httpClient;
+ private $strictMode;
/**
* @param string $manifestPath Absolute path to the manifest file
+ * @param bool $strictMode Throws an exception for unknown paths
*/
- public function __construct(string $manifestPath, HttpClientInterface $httpClient = null)
+ public function __construct(string $manifestPath, HttpClientInterface $httpClient = null, $strictMode = false)
{
$this->manifestPath = $manifestPath;
$this->httpClient = $httpClient;
+ $this->strictMode = $strictMode;
if (null === $this->httpClient && ($scheme = parse_url($this->manifestPath, \PHP_URL_SCHEME)) && 0 === strpos($scheme, 'http')) {
- throw new \LogicException(sprintf('The "%s" class needs an HTTP client to use a remote manifest. Try running "composer require symfony/http-client".', self::class));
+ throw new LogicException(sprintf('The "%s" class needs an HTTP client to use a remote manifest. Try running "composer require symfony/http-client".', self::class));
}
}
@@ -68,20 +75,58 @@ private function getManifestPath(string $path): ?string
'headers' => ['accept' => 'application/json'],
])->toArray();
} catch (DecodingExceptionInterface $e) {
- throw new \RuntimeException(sprintf('Error parsing JSON from asset manifest URL "%s".', $this->manifestPath), 0, $e);
+ throw new RuntimeException(sprintf('Error parsing JSON from asset manifest URL "%s".', $this->manifestPath), 0, $e);
+ } catch (ClientExceptionInterface $e) {
+ throw new RuntimeException(sprintf('Error loading JSON from asset manifest URL "%s".', $this->manifestPath), 0, $e);
}
} else {
if (!is_file($this->manifestPath)) {
- throw new \RuntimeException(sprintf('Asset manifest file "%s" does not exist.', $this->manifestPath));
+ throw new RuntimeException(sprintf('Asset manifest file "%s" does not exist.', $this->manifestPath));
}
$this->manifestData = json_decode(file_get_contents($this->manifestPath), true);
if (0 < json_last_error()) {
- throw new \RuntimeException(sprintf('Error parsing JSON from asset manifest file "%s": ', $this->manifestPath).json_last_error_msg());
+ throw new RuntimeException(sprintf('Error parsing JSON from asset manifest file "%s": ', $this->manifestPath).json_last_error_msg());
}
}
}
- return $this->manifestData[$path] ?? null;
+ if (isset($this->manifestData[$path])) {
+ return $this->manifestData[$path];
+ }
+
+ if ($this->strictMode) {
+ $message = sprintf('Asset "%s" not found in manifest "%s".', $path, $this->manifestPath);
+ $alternatives = $this->findAlternatives($path, $this->manifestData);
+ if (\count($alternatives) > 0) {
+ $message .= sprintf(' Did you mean one of these? "%s".', implode('", "', $alternatives));
+ }
+
+ throw new AssetNotFoundException($message, $alternatives);
+ }
+
+ return null;
+ }
+
+ private function findAlternatives(string $path, array $manifestData): array
+ {
+ $path = strtolower($path);
+ $alternatives = [];
+
+ foreach ($manifestData as $key => $value) {
+ $lev = levenshtein($path, strtolower($key));
+ if ($lev <= \strlen($path) / 3 || false !== stripos($key, $path)) {
+ $alternatives[$key] = isset($alternatives[$key]) ? min($lev, $alternatives[$key]) : $lev;
+ }
+
+ $lev = levenshtein($path, strtolower($value));
+ if ($lev <= \strlen($path) / 3 || false !== stripos($key, $path)) {
+ $alternatives[$key] = isset($alternatives[$key]) ? min($lev, $alternatives[$key]) : $lev;
+ }
+ }
+
+ asort($alternatives);
+
+ return array_keys($alternatives);
}
}