8000 feature #50286 [AssetMapper] Add cached asset factory (weaverryan) · symfony/symfony@3514acd · GitHub
[go: up one dir, main page]

Skip to content

Commit 3514acd

Browse files
committed
feature #50286 [AssetMapper] Add cached asset factory (weaverryan)
This PR was squashed before being merged into the 6.3 branch. Discussion ---------- [AssetMapper] Add cached asset factory | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | None | License | MIT | Doc PR | Still TODO Hi! This is built on top of #50264. I CAN separate them if needed - sorry for the noise. tl;dr When using asset mapper, to load the page, we need to build the importmap & resolve a few paths, like `{{ asset('styles/app.css') }}`. Because asset mapper loads the contents and performs some regex on CSS and JS files, this can slow down the page in dev (in prod, `asset-map:compile` removes all overhead). This PR fixes that (I can see a big improvement in my test app). We "cache" the `MappedAsset` objects via the config cache system so that if an underlying file changes (e.g. `assets/styles/app.css`), we only need to rebuild that ONE `MappedAsset` when the page is loading. Cheers! Commits ------- ce77ed8 [AssetMapper] Add cached asset factory
2 parents 3fcf300 + ce77ed8 commit 3514acd

19 files changed

+629
-220
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
use Symfony\Component\AssetMapper\Compiler\CssAssetUrlCompiler;
2626
use Symfony\Component\AssetMapper\Compiler\JavaScriptImportPathCompiler;
2727
use Symfony\Component\AssetMapper\Compiler\SourceMappingUrlsCompiler;
28+
use Symfony\Component\AssetMapper\Factory\CachedMappedAssetFactory;
29+
use Symfony\Component\AssetMapper\Factory\MappedAssetFactory;
2830
use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
2931
use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer;
3032
use Symfony\Component\AssetMapper\MapperAwareAssetPackage;
@@ -36,11 +38,25 @@
3638
->set('asset_mapper', AssetMapper::class)
3739
->args([
3840
service('asset_mapper.repository'),
39-
service('asset_mapper_compiler'),
41+
service('asset_mapper.mapped_asset_factory'),
4042
service('asset_mapper.public_assets_path_resolver'),
4143
])
4244
->alias(AssetMapperInterface::class, 'asset_mapper')
4345

46+
->set('asset_mapper.mapped_asset_factory', MappedAssetFactory::class)
47+
->args([
48+
service('asset_mapper.public_assets_path_resolver'),
49+
service('asset_mapper_compiler'),
50+
])
51+
52+
->set('asset_mapper.cached_mapped_asset_factory', CachedMappedAssetFactory::class)
53+
->args([
54+
service('.inner'),
55+
param('kernel.cache_dir').'/asset_mapper',
56+
param('kernel.debug'),
57+
])
58+
->decorate('asset_mapper.mapped_asset_factory')
59+
4460
->set('asset_mapper.repository', AssetMapperRepository::class)
4561
->args([
4662
abstract_arg('array of asset mapper paths'),
@@ -93,6 +109,7 @@
93109
->set('asset_mapper_compiler', AssetMapperCompiler::class)
94110
->args([
95111
tagged_iterator('asset_mapper.compiler'),
112+
service_closure('asset_mapper'),
96113
])
97114

98115
->set('asset_mapper.compiler.css_asset_url_compiler', CssAssetUrlCompiler::class)

src/Symfony/Component/AssetMapper/AssetDependency.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,20 @@
1919
final class AssetDependency
2020
{
2121
/**
22-
* @param bool $isLazy whether this dependency is immediately needed
22+
* @param bool $isLazy Whether the dependent asset will need to be loaded eagerly
23+
* by the parent asset (e.g. a CSS file that imports another
24+
* CSS file) or if it will be loaded lazily (e.g. an async
25+
* JavaScript import).
26+
* @param bool $isContentDependency Whether the parent asset's content depends
27+
* on the child asset's content - e.g. if a CSS
28+
* file imports another CSS file, then the parent's
29+
* content depends on the child CSS asset, because
30+
* the child's digested filename will be included.
2331
*/
2432
public function __construct(
2533
public readonly MappedAsset $asset,
26-
public readonly bool $isLazy,
34+
public readonly bool $isLazy = false,
35+
public readonly bool $isContentDependency = true,
2736
) {
2837
}
2938
}

src/Symfony/Component/AssetMapper/AssetMapper.php

Lines changed: 8 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\AssetMapper;
1313

14+
use Symfony\Component\AssetMapper\Factory\MappedAssetFactoryInterface;
1415
use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface;
1516

1617
/**
@@ -23,50 +24,24 @@
2324
class AssetMapper implements AssetMapperInterface
2425
{
2526
public const MANIFEST_FILE_NAME = 'manifest.json';
26-
private const PREDIGESTED_REGEX = '/-([0-9a-zA-Z]{7,128}\.digested)/';
2727

2828
private ?array $manifestData = null;
29-
private array $fileContentsCache = [];
30-
private array $assetsBeingCreated = [];
31-
32-
private array $assetsCache = [];
3329

3430
public function __construct(
3531
private readonly AssetMapperRepository $mapperRepository,
36-
private readonly AssetMapperCompiler $compiler,
32+
private readonly MappedAssetFactoryInterface $mappedAssetFactory,
3733
private readonly PublicAssetsPathResolverInterface $assetsPathResolver,
3834
) {
3935
}
4036

4137
public function getAsset(string $logicalPath): ?MappedAsset
4238
{
43-
if (\in_array($logicalPath, $this->assetsBeingCreated, true)) {
44-
throw new \RuntimeException(sprintf('Circular reference detected while creating asset for "%s": "%s".', $logicalPath, implode(' -> ', $this->assetsBeingCreated).' -> '.$logicalPath));
45-
}
46-
47-
if (!isset($this->assetsCache[$logicalPath])) {
48-
$this->assetsBeingCreated[] = $logicalPath;
49-
50-
$filePath = $this->mapperRepository->find($logicalPath);
51-
if (null === $filePath) {
52-
return null;
53-
}
54-
55-
$asset = new MappedAsset($logicalPath);
56-
$this->assetsCache[$logicalPath] = $asset;
57-
$asset->setSourcePath($filePath);
58-
59-
$asset->setPublicPathWithoutDigest($this->assetsPathResolver->resolvePublicPath($logicalPath));
60-
$publicPath = $this->getPublicPath($logicalPath);
61-
$asset->setPublicPath($publicPath);
62-
[$digest, $isPredigested] = $this->getDigest($asset);
63-
$asset->setDigest($digest, $isPredigested);
64-
$asset->setContent($this->calculateContent($asset));
65-
66-
array_pop($this->assetsBeingCreated);
39+
$filePath = $this->mapperRepository->find($logicalPath);
40+
if (null === $filePath) {
41+
return null;
6742
}
6843

69-
return $this->assetsCache[$logicalPath];
44+
return $this->mappedAssetFactory->createMappedAsset($logicalPath, $filePath);
7045
}
7146

7247
/**
@@ -103,56 +78,9 @@ public function getPublicPath(string $logicalPath): ?string
10378
return $manifestData[$logicalPath];
10479
}
10580

106-
$filePath = $this->mapperRepository->find($logicalPath);
107-
if (null === $filePath) {
108-
return null;
109-
}
110-
111-
// grab the Asset - first look in the cache, as it may only be partially created
112-
$asset = $this->assetsCache[$logicalPath] ?? $this->getAsset($logicalPath);
113-
[$digest, $isPredigested] = $this->getDigest($asset);
114-
115-
if ($isPredigested) {
116-
return $this->assetsPathResolver->resolvePublicPath($logicalPath);
117-
}
118-
119-
$digestedPath = preg_replace_callback('/\.(\w+)$/', function ($matches) use ($digest) {
120-
return "-{$digest}{$matches[0]}";
121-
}, $logicalPath);
122-
123-
return $this->assetsPathResolver->resolvePublicPath($digestedPath);
124-
}
125-
126-
/**
127-
* Returns an array of "string digest" and "bool predigested".
128-
*
129-
* @return array{0: string, 1: bool}
130-
*/
131-
private function getDigest(MappedAsset $asset): array
132-
{
133-
// check for a pre-digested file
134-
if (1 === preg_match(self::PREDIGESTED_REGEX, $asset->getLogicalPath(), $matches)) {
135-
return [$matches[1], true];
136-
}
137-
138-
return [
139-
hash('xxh128', $this->calculateContent($asset)),
140-
false,
141-
];
142-
}
143-
144-
private function calculateContent(MappedAsset $asset): string
145-
{
146-
if (isset($this->fileContentsCache[$asset->getLogicalPath()])) {
147-
return $this->fileContentsCache[$asset->getLogicalPath()];
148-
}
149-
150-
$content = file_get_contents($asset->getSourcePath());
151-
$content = $this->compiler->compile($content, $asset, $this);
152-
153-
$this->fileContentsCache[$asset->getLogicalPath()] = $content;
81+
$asset = $this->getAsset($logicalPath);
15482

155-
return $content;
83+
return $asset?->getPublicPath();
15684
}
15785

15886
private function loadManifest(): array

src/Symfony/Component/AssetMapper/AssetMapperCompiler.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,24 @@
2222
*/
2323
class AssetMapperCompiler
2424
{
25+
private AssetMapperInterface $assetMapper;
26+
2527
/**
2628
* @param iterable<AssetCompilerInterface> $assetCompilers
29+
* @param \Closure(): AssetMapperInterface $assetMapperFactory
2730
*/
28-
public function __construct(private iterable $assetCompilers)
31+
public function __construct(private readonly iterable $assetCompilers, private readonly \Closure $assetMapperFactory)
2932
{
3033
}
3134

32-
public function compile(string $content, MappedAsset $mappedAsset, AssetMapperInterface $assetMapper): string
35+
public function compile(string $content, MappedAsset $mappedAsset): string
3336
{
3437
foreach ($this->assetCompilers as $compiler) {
3538
if (!$compiler->supports($mappedAsset)) {
3639
continue;
3740
}
3841

39-
$content = $compiler->compile($content, $mappedAsset, $assetMapper);
42+
$content = $compiler->compile($content, $mappedAsset, $this->assetMapper ??= ($this->assetMapperFactory)());
4043
}
4144

4245
return $content;

src/Symfony/Component/AssetMapper/Compiler/CssAssetUrlCompiler.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\AssetMapper\Compiler;
1313

14+
use Symfony\Component\AssetMapper\AssetDependency;
1415
use Symfony\Component\AssetMapper\AssetMapperInterface;
1516
use Symfony\Component\AssetMapper\MappedAsset;
1617

@@ -47,7 +48,7 @@ public function compile(string $content, MappedAsset $asset, AssetMapperInterfac
4748
return $matches[0];
4849
}
4950

50-
$asset->addDependency($dependentAsset);
51+
$asset->addDependency(new AssetDependency($dependentAsset));
5152
$relativePath = $this->createRelativePath($asset->getPublicPathWithoutDigest(), $dependentAsset->getPublicPath());
5253

5354
return 'url("'.$relativePath.'")';

src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\AssetMapper\Compiler;
1313

14+
use Symfony\Component\AssetMapper\AssetDependency;
1415
use Symfony\Component\AssetMapper\AssetMapperInterface;
1516
use Symfony\Component\AssetMapper\MappedAsset;
1617

@@ -54,7 +55,7 @@ public function compile(string $content, MappedAsset $asset, AssetMapperInterfac
5455
// This will cause the asset to be included in the importmap.
5556
$isLazy = str_contains($matches[0], 'import(');
5657

57-
$asset->addDependency($dependentAsset, $isLazy);
58+
$asset->addDependency(new AssetDependency($dependentAsset, $isLazy, false));
5859

5960
$relativeImportPath = $this->createRelativePath($asset->getPublicPathWithoutDigest(), $dependentAsset->getPublicPathWithoutDigest());
6061
$relativeImportPath = $this->makeRelativeForJavaScript($relativeImportPath);

src/Symfony/Component/AssetMapper/Compiler/SourceMappingUrlsCompiler.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\AssetMapper\Compiler;
1313

14+
use Symfony\Component\AssetMapper\AssetDependency;
1415
use Symfony\Component\AssetMapper\AssetMapperInterface;
1516
use Symfony\Component\AssetMapper\MappedAsset;
1617

@@ -43,7 +44,7 @@ public function compile(string $content, MappedAsset $asset, AssetMapperInterfac
4344
return $matches[0];
4445
}
4546

46-
$asset->addDependency($dependentAsset);
47+
$asset->addDependency(new AssetDependency($dependentAsset));
4748
$relativePath = $this->createRelativePath($asset->getPublicPathWithoutDigest(), $dependentAsset->getPublicPath());
4849

4950
return $matches[1].'# sourceMappingURL='.$relativePath;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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\AssetMapper\Factory;
13+
14+
use Symfony\Component\AssetMapper\MappedAsset;
15+
use Symfony\Component\Config\ConfigCache;
16+
use Symfony\Component\Config\Resource\FileResource;
17+
use Symfony\Component\Config\Resource\ResourceInterface;
18+
19+
/**
20+
* Decorates the asset factory to load MappedAssets from cache when possible.
21+
*/
22+
class CachedMappedAssetFactory implements MappedAssetFactoryInterface
23+
{
24+
public function __construct(
25+
private readonly MappedAssetFactoryInterface $innerFactory,
26+
private readonly string $cacheDir,
27+
private readonly bool $debug,
28+
) {
29+
}
30+
31+
public function createMappedAsset(string $logicalPath, string $sourcePath): ?MappedAsset
32+
{
33+
$cachePath = $this->getCacheFilePath($logicalPath, $sourcePath);
34+
$configCache = new ConfigCache($cachePath, $this->debug);
35+
36+
if ($configCache->isFresh()) {
37+
return unserialize(file_get_contents($cachePath));
38+
}
39+
40+
$mappedAsset = $this->innerFactory->createMappedAsset($logicalPath, $sourcePath);
41+
42+
if (!$mappedAsset) {
43+
return null;
44+
}
45+
46+
$resources = $this->collectResourcesFromAsset($mappedAsset);
47+
$configCache->write(serialize($mappedAsset), $resources);
48+
49+
return $mappedAsset;
50+
}
51+
52+
private function getCacheFilePath(string $logicalPath, string $sourcePath): string
53+
{
54+
return $this->cacheDir.'/'.hash('xxh128', $logicalPath.':'.$sourcePath).'.php';
55+
}
56+
57+
/**
58+
* @return ResourceInterface[]
59+
*/
60+
private function collectResourcesFromAsset(MappedAsset $mappedAsset): array
61+
{
62+
$resources = [new FileResource($mappedAsset->getSourcePath())];
63+
64+
foreach ($mappedAsset->getDependencies() as $dependency) {
65+
if (!$dependency->isContentDependency) {
66+
continue;
67+
}
68+
69+
$resources = array_merge($resources, $this->collectResourcesFromAsset($dependency->asset));
70+
}
71+
72+
return $resources;
73+
}
74+
}

0 commit comments

Comments
 (0)
0