Skip to content

Commit e694b9e

Browse files
committed
feature #51847 [AssetMapper] Allowing for files to be written to some non-local location (weaverryan)
This PR was squashed before being merged into the 6.4 branch. Discussion ---------- [AssetMapper] Allowing for files to be written to some non-local location | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Fix #50221 | License | MIT Hi! An attempt at making AssetMapper flexible enough to support non-local filesystems. The "hook point" would be that you could replace the `asset_mapper.local_public_assets_filesystem` service with your own that implements `PublicAssetsFilesystemInterface`. I'm not worried about making the hook point *super* user-friendly: I just want the system to support this now, as trying to add this later (when we need to protect BC) will be harder. Cheers! Commits ------- 48a2d689e6e [AssetMapper] Allowing for files to be written to some non-local location
2 parents b3e1715 + 5336880 commit e694b9e

16 files changed

+316
-149
lines changed

AssetMapper.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
namespace Symfony\Component\AssetMapper;
1313

1414
use Symfony\Component\AssetMapper\Factory\MappedAssetFactoryInterface;
15-
use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface;
1615

1716
/**
1817
* Finds and returns assets in the pipeline.
@@ -28,7 +27,7 @@ class AssetMapper implements AssetMapperInterface
2827
public function __construct(
2928
private readonly AssetMapperRepository $mapperRepository,
3029
private readonly MappedAssetFactoryInterface $mappedAssetFactory,
31-
private readonly PublicAssetsPathResolverInterface $assetsPathResolver,
30+
private readonly CompiledAssetMapperConfigReader $compiledConfigReader,
3231
) {
3332
}
3433

@@ -78,12 +77,10 @@ public function getPublicPath(string $logicalPath): ?string
7877
private function loadManifest(): array
7978
{
8079
if (null === $this->manifestData) {
81-
$path = $this->assetsPathResolver->getPublicFilesystemPath().'/'.self::MANIFEST_FILE_NAME;
82-
83-
if (!is_file($path)) {
80+
if (!$this->compiledConfigReader->configExists(self::MANIFEST_FILE_NAME)) {
8481
$this->manifestData = [];
8582
} else {
86-
$this->manifestData = json_decode(file_get_contents($path), true);
83+
$this->manifestData = $this->compiledConfigReader->loadConfig(self::MANIFEST_FILE_NAME);
8784
}
8885
}
8986

Command/AssetMapperCompileCommand.php

Lines changed: 25 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,15 @@
1313

1414
use Symfony\Component\AssetMapper\AssetMapper;
1515
use Symfony\Component\AssetMapper\AssetMapperInterface;
16+
use Symfony\Component\AssetMapper\CompiledAssetMapperConfigReader;
1617
use Symfony\Component\AssetMapper\Event\PreAssetsCompileEvent;
1718
use Symfony\Component\AssetMapper\ImportMap\ImportMapGenerator;
18-
use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface;
19+
use Symfony\Component\AssetMapper\Path\PublicAssetsFilesystemInterface;
1920
use Symfony\Component\Console\Attribute\AsCommand;
2021
use Symfony\Component\Console\Command\Command;
21-
use Symfony\Component\Console\Exception\InvalidArgumentException;
2222
use Symfony\Component\Console\Input\InputInterface;
2323
use Symfony\Component\Console\Output\OutputInterface;
2424
use Symfony\Component\Console\Style\SymfonyStyle;
25-
use Symfony\Component\Filesystem\Filesystem;
2625
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
2726

2827
/**
@@ -36,12 +35,11 @@
3635
final class AssetMapperCompileCommand extends Command
3736
{
3837
public function __construct(
39-
private readonly PublicAssetsPathResolverInterface $publicAssetsPathResolver,
38+
private readonly CompiledAssetMapperConfigReader $compiledConfigReader,
4039
private readonly AssetMapperInterface $assetMapper,
4140
private readonly ImportMapGenerator $importMapGenerator,
42-
private readonly Filesystem $filesystem,
41+
private readonly PublicAssetsFilesystemInterface $assetsFilesystem,
4342
private readonly string $projectDir,
44-
private readonly string $publicDirName,
4543
private readonly bool $isDebug,
4644
private readonly ?EventDispatcherInterface $eventDispatcher = null,
4745
) {
@@ -51,7 +49,6 @@ public function __construct(
5149
protected function configure(): void
5250
{
5351
$this
54-
->addOption('clean', null, null, 'Whether to clean the public directory before compiling assets')
5552
->setHelp(<<<'EOT'
5653
The <info>%command.name%</info> command compiles and dumps all the assets in
5754
the asset mapper into the final public directory (usually <comment>public/assets</comment>).
@@ -64,61 +61,36 @@ protected function configure(): void
6461
protected function execute(InputInterface $input, OutputInterface $output): int
6562
{
6663
$io = new SymfonyStyle($input, $output);
67-
$publicDir = $this->projectDir.'/'.$this->publicDirName;
68-
if (!is_dir($publicDir)) {
69-
throw new InvalidArgumentException(sprintf('The public directory "%s" does not exist.', $publicDir));
70-
}
71-
72-
$outputDir = $this->publicAssetsPathResolver->getPublicFilesystemPath();
73-
if ($input->getOption('clean')) {
74-
$io->comment(sprintf('Cleaning <info>%s</info>', $outputDir));
75-
$this->filesystem->remove($outputDir);
76-
$this->filesystem->mkdir($outputDir);
77-
}
78-
79-
// set up the file paths
80-
$files = [];
81-
$manifestPath = $outputDir.'/'.AssetMapper::MANIFEST_FILE_NAME;
82-
$files[] = $manifestPath;
8364

84-
$importMapPath = $outputDir.'/'.ImportMapGenerator::IMPORT_MAP_CACHE_FILENAME;
85-
$files[] = $importMapPath;
65+
$this->eventDispatcher?->dispatch(new PreAssetsCompileEvent($output));
8666

87-
$entrypointFilePaths = [];
67+
// remove existing config files
68+
$this->compiledConfigReader->removeConfig(AssetMapper::MANIFEST_FILE_NAME);
69+
$this->compiledConfigReader->removeConfig(ImportMapGenerator::IMPORT_MAP_CACHE_FILENAME);
70+
$entrypointFiles = [];
8871
foreach ($this->importMapGenerator->getEntrypointNames() as $entrypointName) {
89-
$dumpedEntrypointPath = $outputDir.'/'.sprintf(ImportMapGenerator::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entrypointName);
90-
$files[] = $dumpedEntrypointPath;
91-
$entrypointFilePaths[$entrypointName] = $dumpedEntrypointPath;
72+
$path = sprintf(ImportMapGenerator::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entrypointName);
73+
$this->compiledConfigReader->removeConfig($path);
74+
$entrypointFiles[$entrypointName] = $path;
9275
}
9376

94-
// remove existing files
95-
foreach ($files as $file) {
96-
if (is_file($file)) {
97-
$this->filesystem->remove($file);
98-
}
99-
}
100-
101-
$this->eventDispatcher?->dispatch(new PreAssetsCompileEvent($outputDir, $output));
102-
103-
// dump new files
104-
$manifest = $this->createManifestAndWriteFiles($io, $publicDir);
105-
$this->filesystem->dumpFile($manifestPath, json_encode($manifest, \JSON_PRETTY_PRINT));
77+
$manifest = $this->createManifestAndWriteFiles($io);
78+
$manifestPath = $this->compiledConfigReader->saveConfig(AssetMapper::MANIFEST_FILE_NAME, $manifest);
10679
$io->comment(sprintf('Manifest written to <info>%s</info>', $this->shortenPath($manifestPath)));
10780

108-
$this->filesystem->dumpFile($importMapPath, json_encode($this->importMapGenerator->getRawImportMapData(), \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG));
81+
$importMapPath = $this->compiledConfigReader->saveConfig(ImportMapGenerator::IMPORT_MAP_CACHE_FILENAME, $this->importMapGenerator->getRawImportMapData());
10982
$io->comment(sprintf('Import map data written to <info>%s</info>.', $this->shortenPath($importMapPath)));
11083

111-
$entrypointNames = $this->importMapGenerator->getEntrypointNames();
112-
foreach ($entrypointFilePaths as $entrypointName => $path) {
113-
$this->filesystem->dumpFile($path, json_encode($this->importMapGenerator->findEagerEntrypointImports($entrypointName), \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG));
84+
foreach ($entrypointFiles as $entrypointName => $path) {
85+
$this->compiledConfigReader->saveConfig($path, $this->importMapGenerator->findEagerEntrypointImports($entrypointName));
11486
}
115-
$styledEntrypointNames = array_map(fn (string $entrypointName) => sprintf('<info>%s</>', $entrypointName), $entrypointNames);
116-
$io->comment(sprintf('Entrypoint metadata written for <comment>%d</> entrypoints (%s).', \count($entrypointNames), implode(', ', $styledEntrypointNames)));
87+
$styledEntrypointNames = array_map(fn (string $entrypointName) => sprintf('<info>%s</>', $entrypointName), array_keys($entrypointFiles));
88+
$io->comment(sprintf('Entrypoint metadata written for <comment>%d</> entrypoints (%s).', \count($entrypointFiles), implode(', ', $styledEntrypointNames)));
11789

11890
if ($this->isDebug) {
11991
$io->warning(sprintf(
120-
'You are compiling assets in development. Symfony will not serve any changed assets until you delete the "%s" directory.',
121-
$this->shortenPath($outputDir)
92+
'You are compiling assets in development. Symfony will not serve any changed assets until you delete the files in the "%s" directory.',
93+
$this->shortenPath(\dirname($manifestPath))
12294
));
12395
}
12496

@@ -130,20 +102,18 @@ private function shortenPath(string $path): string
130102
return str_replace($this->projectDir.'/', '', $path);
131103
}
132104

133-
private function createManifestAndWriteFiles(SymfonyStyle $io, string $publicDir): array
105+
private function createManifestAndWriteFiles(SymfonyStyle $io): array
134106
{
135107
$allAssets = $this->assetMapper->allAssets();
136108

137-
$io->comment(sprintf('Compiling assets to <info>%s%s</info>', $publicDir, $this->publicAssetsPathResolver->resolvePublicPath('')));
109+
$io->comment(sprintf('Compiling and writing asset files to <info>%s</info>', $this->shortenPath($this->assetsFilesystem->getDestinationPath())));
138110
$manifest = [];
139111
foreach ($allAssets as $asset) {
140-
// $asset->getPublicPath() will start with a "/"
141-
$targetPath = $publicDir.$asset->publicPath;
142112
if (null !== $asset->content) {
143113
// The original content has been modified by the AssetMapperCompiler
144-
$this->filesystem->dumpFile($targetPath, $asset->content);
114+
$this->assetsFilesystem->write($asset->publicPath, $asset->content);
145115
} else {
146-
$this->filesystem->copy($asset->sourcePath, $targetPath, true);
116+
$this->assetsFilesystem->copy($asset->sourcePath, $asset->publicPath);
147117
}
148118

149119
$manifest[$asset->logicalPath] = $asset->publicPath;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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;
13+
14+
use Symfony\Component\Filesystem\Path;
15+
16+
/**
17+
* Reads and writes compiled configuration files for asset mapper.
18+
*/
19+
class CompiledAssetMapperConfigReader
20+
{
21+
public function __construct(private readonly string $directory)
22+
{
23+
}
24+
25+
public function configExists(string $filename): bool
26+
{
27+
return is_file(Path::join($this->directory, $filename));
28+
}
29+
30+
public function loadConfig(string $filename): array
31+
{
32+
return json_decode(file_get_contents(Path::join($this->directory, $filename)), true, 512, \JSON_THROW_ON_ERROR);
33+
}
34+
35+
public function saveConfig(string $filename, array $data): string
36+
{
37+
$path = Path::join($this->directory, $filename);
38+
@mkdir(\dirname($path), 0777, true);
39+
file_put_contents($path, json_encode($data, \JSON_PRETTY_PRINT | \JSON_THROW_ON_ERROR));
40+
41+
return $path;
42+
}
43+
44+
public function removeConfig(string $filename): void
45+
{
46+
$path = Path::join($this->directory, $filename);
47+
48+
if (is_file($path)) {
49+
unlink($path);
50+
}
51+
}
52+
}

Event/PreAssetsCompileEvent.php

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,13 @@
2121
*/
2222
class PreAssetsCompileEvent extends Event
2323
{
24-
private string $outputDir;
2524
private OutputInterface $output;
2625

27-
public function __construct(string $outputDir, OutputInterface $output)
26+
public function __construct(OutputInterface $output)
2827
{
29-
$this->outputDir = $outputDir;
3028
$this->output = $output;
3129
}
3230

33-
public function getOutputDir(): string
34-
{
35-
return $this->outputDir;
36-
}
37-
3831
public function getOutput(): OutputInterface
3932
{
4033
return $this->output;

ImportMap/ImportMapGenerator.php

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
namespace Symfony\Component\AssetMapper\ImportMap;
1313

1414
use Symfony\Component\AssetMapper\AssetMapperInterface;
15+
use Symfony\Component\AssetMapper\CompiledAssetMapperConfigReader;
1516
use Symfony\Component\AssetMapper\Exception\LogicException;
1617
use Symfony\Component\AssetMapper\MappedAsset;
17-
use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface;
1818

1919
/**
2020
* Provides data needed to write the importmap & preloads.
@@ -26,7 +26,7 @@ class ImportMapGenerator
2626

2727
public function __construct(
2828
private readonly AssetMapperInterface $assetMapper,
29-
private readonly PublicAssetsPathResolverInterface $assetsPathResolver,
29+
private readonly CompiledAssetMapperConfigReader $compiledConfigReader,
3030
private readonly ImportMapConfigReader $importMapConfigReader,
3131
) {
3232
}
@@ -87,9 +87,8 @@ public function getImportMapData(array $entrypointNames): array
8787
*/
8888
public function getRawImportMapData(): array
8989
{
90-
$dumpedImportMapPath = $this->assetsPathResolver->getPublicFilesystemPath().'/'.self::IMPORT_MAP_CACHE_FILENAME;
91-
if (is_file($dumpedImportMapPath)) {
92-
return json_decode(file_get_contents($dumpedImportMapPath), true, 512, \JSON_THROW_ON_ERROR);
90+
if ($this->compiledConfigReader->configExists(self::IMPORT_MAP_CACHE_FILENAME)) {
91+
return $this->compiledConfigReader->loadConfig(self::IMPORT_MAP_CACHE_FILENAME);
9392
}
9493

9594
$allEntries = [];
@@ -122,9 +121,8 @@ public function getRawImportMapData(): array
122121
*/
123122
public function findEagerEntrypointImports(string $entryName): array
124123
{
125-
$dumpedEntrypointPath = $this->assetsPathResolver->getPublicFilesystemPath().'/'.sprintf(self::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entryName);
126-
if (is_file($dumpedEntrypointPath)) {
127-
return json_decode(file_get_contents($dumpedEntrypointPath), true, 512, \JSON_THROW_ON_ERROR);
124+
if ($this->compiledConfigReader->configExists(sprintf(self::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entryName))) {
125+
return $this->compiledConfigReader->loadConfig(sprintf(self::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entryName));
128126
}
129127

130128
$rootImportEntries = $this->importMapConfigReader->getEntries();
@@ -202,13 +200,6 @@ private function addImplicitEntries(ImportMapEntry $entry, array $currentImportE
202200
return $currentImportEntries;
203201
}
204202

205-
private function findRootImportMapEntry(string $moduleName): ?ImportMapEntry
206-
{
207-
$entries = $this->importMapConfigReader->getEntries();
208-
209-
return $entries->has($moduleName) ? $entries->get($moduleName) : null;
210-
}
211-
212203
/**
213204
* Finds the MappedAsset allowing for a "logical path", relative or absolute filesystem path.
214205
*/
Lines changed: 43 additions & 0 deletions
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 <[email protected]>
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\Path;
13+
14+
use Symfony\Component\Filesystem\Filesystem;
15+
16+
class LocalPublicAssetsFilesystem implements PublicAssetsFilesystemInterface
17+
{
18+
private Filesystem $filesystem;
19+
20+
public function __construct(private readonly string $publicDir)
21+
{
22+
$this->filesystem = new Filesystem();
23+
}
24+
25+
public function write(string $path, string $contents): void
26+
{
27+
$targetPath = $this->publicDir.'/'.ltrim($path, '/');
28+
29+
$this->filesystem->dumpFile($targetPath, $contents);
30+
}
31+
32+
public function copy(string $originPath, string $path): void
33+
{
34+
$targetPath = $this->publicDir.'/'.ltrim($path, '/');
35+
36+
$this->filesystem->copy($originPath, $targetPath, true);
37+
}
38+
39+
public function getDestinationPath(): string
40+
{
41+
return $this->publicDir;
42+
}
43+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Path;
13+
14+
/**
15+
* Writes asset files to their public location.
16+
*/
17+
interface PublicAssetsFilesystemInterface
18+
{
19+
/**
20+
* Write the contents of a file to the public location.
21+
*/
22+
public function write(string $path, string $contents): void;
23+
24+
/**
25+
* Copy a local file to the public location.
26+
*/
27+
public function copy(string $originPath, string $path): void;
28+
29+
/**
30+
* A string representation of the public directory, used for feedback.
31+
*/
32+
public function getDestinationPath(): string;
33+
}

0 commit comments

Comments
 (0)