Skip to content

Commit 7d60341

Browse files
committed
Suggest alternative asset names from the manifest
1 parent 92d5fde commit 7d60341

7 files changed

+127
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Asset\Exception;
13+
14+
/**
15+
* Represents an asset not found in a manifest.
16+
*
17+
* @author Jérôme Tamarelle <jerome@tamarelle.net>
18+
*/
19+
class AssetNotFoundException extends RuntimeException
20+
{
21+
private $alternatives;
22+
23+
/**
24+
* @param string $message Exception message to throw
25+
* @param array $alternatives List of similar defined names
26+
* @param int $code Exception code
27+
* @param \Throwable $previous Previous exception used for the exception chaining
28+
*/
29+
public function __construct(string $message, array $alternatives = [], int $code = 0, \Throwable $previous = null)
30+
{
31+
parent::__construct($message, $code, $previous);
32+
33+
$this->alternatives = $alternatives;
34+
}
35+
36+
/**
37+
* @return array A list of similar defined names
38+
*/
39+
public function getAlternatives()
40+
{
41+
return $this->alternatives;
42+
}
43+
}

src/Symfony/Component/Asset/Tests/VersionStrategy/JsonManifestVersionStrategyTest.php

+4-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Asset\Tests\VersionStrategy;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Asset\Exception\AssetNotFoundException;
1516
use Symfony\Component\Asset\Exception\RuntimeException;
1617
use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy;
1718

@@ -57,10 +58,10 @@ public function testStrictExceptionWhenKeyDoesNotExistInManifest()
5758
{
5859
$strategy = $this->createStrategy('manifest-valid.json', true);
5960

60-
$this->expectException(RuntimeException::class);
61-
$this->expectExceptionMessage('Asset "css/other.css" not found in manifest "');
61+
$this->expectException(AssetNotFoundException::class);
62+
$this->expectExceptionMessageRegExp('~Asset "css/styles.555def.css" not found in manifest "(.*)/manifest-valid.json". Did you mean one of these\? "css/styles.css", "css/style.css".~');
6263

63-
$strategy->getVersion('css/other.css');
64+
$strategy->getVersion('css/styles.555def.css');
6465
}
6566

6667
private function createStrategy($manifestFilename, $strict = false)

src/Symfony/Component/Asset/Tests/VersionStrategy/RemoteJsonManifestVersionStrategyTest.php

+11
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Asset\Tests\VersionStrategy;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Asset\Exception\AssetNotFoundException;
1516
use Symfony\Component\Asset\VersionStrategy\RemoteJsonManifestVersionStrategy;
1617
use Symfony\Component\HttpClient\Exception\JsonException;
1718
use Symfony\Component\HttpClient\MockHttpClient;
@@ -56,6 +57,16 @@ public function testManifestFileWithBadJSONThrowsException()
5657
$strategy->getVersion('main.js');
5758
}
5859

60+
public function testStrictExceptionWhenKeyDoesNotExistInManifest()
61+
{
62+
$strategy = $this->createStrategy('https://cdn.example.com/manifest-valid.json', true);
63+
64+
$this->expectException(AssetNotFoundException::class);
65+
$this->expectExceptionMessageRegExp('~Asset "/home.css" not found in manifest "(.*)/manifest-valid.json". Did you mean one of these\? "main/home.css".~');
66+
67+
$strategy->getVersion('/home.css');
68+
}
69+
5970
private function createStrategy($manifestUrl, $strict = false)
6071
{
6172
$httpClient = new MockHttpClient(function ($method, $url, $options) {
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
{
22
"main.js": "main.123abc.js",
3-
"css/styles.css": "css/styles.555def.css"
3+
"css/styles.css": "css/styles.555def.css",
4+
"css/style.css": "css/style.abcdef.css",
5+
"main/home.css": "main/home.css"
46
}

src/Symfony/Component/Asset/VersionStrategy/JsonManifestVersionStrategy.php

+11-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Asset\VersionStrategy;
1313

14+
use Symfony\Component\Asset\Exception\AssetNotFoundException;
1415
use Symfony\Component\Asset\Exception\RuntimeException;
1516

1617
/**
@@ -26,6 +27,8 @@
2627
*/
2728
class JsonManifestVersionStrategy implements VersionStrategyInterface
2829
{
30+
use ManifestAlternativesTrait;
31+
2932
private $manifestPath;
3033
private $manifestData;
3134
private $strict;
@@ -34,7 +37,7 @@ class JsonManifestVersionStrategy implements VersionStrategyInterface
3437
* @param string $manifestPath Absolute path to the manifest file
3538
* @param bool $strict Throws an exception for unknown paths
3639
*/
37-
public function __construct(string $manifestPath, $strict = false)
40+
public function __construct(string $manifestPath, bool $strict = false)
3841
{
3942
$this->manifestPath = $manifestPath;
4043
$this->strict = $strict;
@@ -68,7 +71,13 @@ public function applyVersion(string $path)
6871
}
6972

7073
if ($this->strict) {
71-
throw new RuntimeException(sprintf('Asset "%s" not found in manifest "%s".', $path, $this->manifestPath));
74+
$message = sprintf('Asset "%s" not found in manifest "%s".', $path, $this->manifestPath);
75+
$alternatives = $this->findAlternatives($path, $this->manifestData);
76+
if (\count($alternatives) > 0) {
77+
$message .= sprintf(' Did you mean one of these? "%s".', implode('", "', $alternatives));
78+
}
79+
80+
throw new AssetNotFoundException($message, $alternatives);
7281
}
7382

7483
return $path;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Asset\VersionStrategy;
13+
14+
/**
15+
* @author Jérôme Tamarelle <jerome@tamarelle.net>
16+
*/
17+
trait ManifestAlternativesTrait
18+
{
19+
/**
20+
* Finds alternative of $path among $manifestData.
21+
*
22+
* @return string[] A sorted array of similar string
23+
*/
24+
private function findAlternatives(string $path, array $manifestData): array
25+
{
26+
$alternatives = [];
27+
28+
foreach ($manifestData as $key => $value) {
29+
$lev = levenshtein($path, $key);
30+
if ($lev <= \strlen($path) / 3 || false !== strpos($key, $path)) {
31+
$alternatives[$key] = isset($alternatives[$key]) ? min($lev, $alternatives[$key]) : $lev;
32+
}
33+
34+
$lev = levenshtein($path, $value);
35+
if ($lev <= \strlen($path) / 3 || false !== strpos($key, $path)) {
36+
$alternatives[$key] = isset($alternatives[$key]) ? min($lev, $alternatives[$key]) : $lev;
37+
}
38+
}
39+
40+
asort($alternatives);
41+
42+
return array_keys($alternatives);
43+
}
44+
}

src/Symfony/Component/Asset/VersionStrategy/RemoteJsonManifestVersionStrategy.php

+11-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
namespace Symfony\Component\Asset\VersionStrategy;
1313

14-
use Symfony\Component\Asset\Exception\RuntimeException;
14+
use Symfony\Component\Asset\Exception\AssetNotFoundException;
1515
use Symfony\Contracts\HttpClient\HttpClientInterface;
1616

1717
/**
@@ -27,6 +27,8 @@
2727
*/
2828
class RemoteJsonManifestVersionStrategy implements VersionStrategyInterface
2929
{
30+
use ManifestAlternativesTrait;
31+
3032
private $manifestData;
3133
private $manifestUrl;
3234
private $httpClient;
@@ -36,7 +38,7 @@ class RemoteJsonManifestVersionStrategy implements VersionStrategyInterface
3638
* @param string $manifestUrl Absolute URL to the manifest file
3739
* @param bool $strict Throws an exception for unknown paths
3840
*/
39-
public function __construct(string $manifestUrl, HttpClientInterface $httpClient, $strict = false)
41+
public function __construct(string $manifestUrl, HttpClientInterface $httpClient, bool $strict = false)
4042
{
4143
$this->manifestUrl = $manifestUrl;
4244
$this->httpClient = $httpClient;
@@ -66,7 +68,13 @@ public function applyVersion(string $path)
6668
}
6769

6870
if ($this->strict) {
69-
throw new RuntimeException(sprintf('Asset "%s" not found in manifest "%s".', $path, $this->manifestUrl));
71+
$message = sprintf('Asset "%s" not found in manifest "%s".', $path, $this->manifestUrl);
72+
$alternatives = $this->findAlternatives($path, $this->manifestData);
73+
if (\count($alternatives) > 0) {
74+
$message .= sprintf(' Did you mean one of these? "%s".', implode('", "', $alternatives));
75+
}
76+
77+
throw new AssetNotFoundException($message, $alternatives);
7078
}
7179

7280
return $path;

0 commit comments

Comments
 (0)