Skip to content

Commit a2f1f4b

Browse files
author
Matthias Moser
committed
Add recipe provider which loads recipes directly from the bundle.
1 parent 0763da1 commit a2f1f4b

8 files changed

+326
-44
lines changed

src/Command/UpdateRecipesCommand.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,29 @@
2121
use Symfony\Component\Console\Input\InputInterface;
2222
use Symfony\Component\Console\Output\OutputInterface;
2323
use Symfony\Flex\Configurator;
24-
use Symfony\Flex\Downloader;
2524
use Symfony\Flex\Flex;
2625
use Symfony\Flex\GithubApi;
2726
use Symfony\Flex\InformationOperation;
2827
use Symfony\Flex\Lock;
2928
use Symfony\Flex\Recipe;
29+
use Symfony\Flex\RecipeProviderInterface;
3030
use Symfony\Flex\Update\RecipePatcher;
3131
use Symfony\Flex\Update\RecipeUpdate;
3232

3333
class UpdateRecipesCommand extends BaseCommand
3434
{
3535
/** @var Flex */
3636
private $flex;
37-
private $downloader;
37+
private RecipeProviderInterface $recipeProvider;
3838
private $configurator;
3939
private $rootDir;
4040
private $githubApi;
4141
private $processExecutor;
4242

43-
public function __construct(/* cannot be type-hinted */ $flex, Downloader $downloader, $httpDownloader, Configurator $configurator, string $rootDir)
43+
public function __construct(/* cannot be type-hinted */ $flex, RecipeProviderInterface $recipeProvider, $httpDownloader, Configurator $configurator, string $rootDir)
4444
{
4545
$this->flex = $flex;
46-
$this->downloader = $downloader;
46+
$this->recipeProvider = $recipeProvider;
4747
$this->configurator = $configurator;
4848
$this->rootDir = $rootDir;
4949
$this->githubApi = new GithubApi($httpDownloader);
@@ -268,7 +268,7 @@ private function getRecipe(PackageInterface $package, string $recipeRef = null,
268268
if (null !== $recipeRef) {
269269
$operation->setSpecificRecipeVersion($recipeRef, $recipeVersion);
270270
}
271-
$recipes = $this->downloader->getRecipes([$operation]);
271+
$recipes = $this->recipeProvider->getRecipes([$operation]);
272272

273273
if (0 === \count($recipes['manifests'] ?? [])) {
274274
return null;

src/CompositeRecipeProvider.php

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
namespace Symfony\Flex;
4+
5+
class CompositeRecipeProvider implements RecipeProviderInterface
6+
{
7+
/**
8+
* @var RecipeProviderInterface[]
9+
*/
10+
private array $recipeProviders;
11+
12+
/**
13+
* @param RecipeProviderInterface[] $recipeProviders
14+
*
15+
* @throws \InvalidArgumentException
16+
*/
17+
public function __construct(array $recipeProviders)
18+
{
19+
$this->recipeProviders = array_reduce(
20+
$recipeProviders,
21+
function (array $providers, RecipeProviderInterface $provider) {
22+
if (self::class == $provider::class) {
23+
throw new \InvalidArgumentException('You cannot add an instance of this provider to itself.');
24+
}
25+
$providers[$provider::class] = $provider;
26+
27+
return $providers;
28+
},
29+
[]);
30+
}
31+
32+
/**
33+
* This method adds an instance RecipeProviderInterface to this provider.
34+
* You can only have one instance per class registered in this provider.
35+
*
36+
* @return $this
37+
*
38+
* @throws \InvalidArgumentException
39+
*/
40+
public function add(RecipeProviderInterface $recipeProvider): self
41+
{
42+
if (self::class == $recipeProvider::class) {
43+
throw new \InvalidArgumentException('You cannot add an instance of this provider to itself.');
44+
}
45+
if (isset($this->recipeProviders[$recipeProvider::class])) {
46+
throw new \InvalidArgumentException('Given Provider has been added already.');
47+
}
48+
$this->recipeProviders[] = $recipeProvider;
49+
50+
return $this;
51+
}
52+
53+
/**
54+
* {@inheritDoc}
55+
*/
56+
public function isEnabled(): bool
57+
{
58+
return array_reduce($this->recipeProviders, function (bool $isEnabled, RecipeProviderInterface $provider) { return $provider->isEnabled() && $isEnabled; }, true);
59+
}
60+
61+
/**
62+
* {@inheritDoc}
63+
*/
64+
public function disable(): void
65+
{
66+
array_walk($this->recipeProviders, function (RecipeProviderInterface $provider) { $provider->disable(); });
67+
}
68+
69+
/**
70+
* {@inheritDoc}
71+
*/
72+
public function getVersions(): array
73+
{
74+
return array_reduce($this->recipeProviders, function (array $carry, RecipeProviderInterface $provider) { return array_merge($carry, $provider->getVersions()); }, []);
75+
}
76+
77+
/**
78+
* {@inheritDoc}
79+
*/
80+
public function getAliases(): array
81+
{
82+
return array_reduce($this->recipeProviders, function (array $carry, RecipeProviderInterface $provider) { return array_merge($carry, $provider->getAliases()); }, []);
83+
}
84+
85+
/**
86+
* {@inheritDoc}
87+
*/
88+
public function getRecipes(array $operations): array
89+
{
90+
return array_reduce($this->recipeProviders, function (array $carry, RecipeProviderInterface $provider) use ($operations) { return array_merge_recursive($carry, $provider->getRecipes($operations)); }, []);
91+
}
92+
93+
/**
94+
* {@inheritDoc}
95+
*/
96+
public function removeRecipeFromIndex(string $packageName, string $version): void
97+
{
98+
array_walk($this->recipeProviders, function (RecipeProviderInterface $provider) use ($packageName, $version) { $provider->removeRecipeFromIndex($packageName, $version); });
99+
}
100+
101+
public function getSessionId(): string
102+
{
103+
return implode(' ', array_reduce(
104+
$this->recipeProviders,
105+
function (array $carry, RecipeProviderInterface $provider) {
106+
$carry[] = $provider::class.'=>'.$provider->getSessionId();
107+
108+
return $carry;
109+
},
110+
[]));
111+
}
112+
}

src/Downloader.php

+23-12
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
* @author Fabien Potencier <fabien@symfony.com>
2727
* @author Nicolas Grekas <p@tchwork.com>
2828
*/
29-
class Downloader
29+
class Downloader implements RecipeProviderInterface
3030
{
3131
private const DEFAULT_ENDPOINTS = [
3232
'https://raw.githubusercontent.com/symfony/recipes/flex/main/index.json',
@@ -95,39 +95,52 @@ public function __construct(Composer $composer, IoInterface $io, HttpDownloader
9595
$this->composer = $composer;
9696
}
9797

98+
/**
99+
* {@inheritDoc}
100+
*/
98101
public function getSessionId(): string
99102
{
100103
return $this->sess;
101104
}
102105

103-
public function isEnabled()
106+
/**
107+
* {@inheritDoc}
108+
*/
109+
public function isEnabled(): bool
104110
{
105111
return $this->enabled;
106112
}
107113

108-
public function disable()
114+
/**
115+
* {@inheritDoc}
116+
*/
117+
public function disable(): void
109118
{
110119
$this->enabled = false;
111120
}
112121

113-
public function getVersions()
122+
/**
123+
* {@inheritDoc}
124+
*/
125+
public function getVersions(): array
114126
{
115127
$this->initialize();
116128

117129
return self::$versions ?? self::$versions = current($this->get([$this->legacyEndpoint.'/versions.json']));
118130
}
119131

120-
public function getAliases()
132+
/**
133+
* {@inheritDoc}
134+
*/
135+
public function getAliases(): array
121136
{
122137
$this->initialize();
123138

124139
return self::$aliases ?? self::$aliases = current($this->get([$this->legacyEndpoint.'/aliases.json']));
125140
}
126141

127142
/**
128-
* Downloads recipes.
129-
*
130-
* @param OperationInterface[] $operations
143+
* {@inheritDoc}
131144
*/
132145
public function getRecipes(array $operations): array
133146
{
@@ -307,11 +320,9 @@ public function getRecipes(array $operations): array
307320
}
308321

309322
/**
310-
* Used to "hide" a recipe version so that the next most-recent will be returned.
311-
*
312-
* This is used when resolving "conflicts".
323+
* @inheritDoc
313324
*/
314-
public function removeRecipeFromIndex(string $packageName, string $version)
325+
public function removeRecipeFromIndex(string $packageName, string $version): void
315326
{
316327
unset($this->index[$packageName][$version]);
317328
}

src/Flex.php

+20-16
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class Flex implements PluginInterface, EventSubscriberInterface
6565
private $config;
6666
private $options;
6767
private $configurator;
68-
private $downloader;
68+
private RecipeProviderInterface $recipeProvider;
6969

7070
/**
7171
* @var Installer
@@ -112,10 +112,14 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__)
112112

113113
$rfs = Factory::createHttpDownloader($this->io, $this->config);
114114

115-
$this->downloader = $downloader = new Downloader($composer, $io, $rfs);
115+
$this->recipeProvider = new CompositeRecipeProvider(
116+
[
117+
new Downloader($composer, $io, $rfs),
118+
new LocalRecipeProvider($composer),
119+
]);
116120

117121
if ($symfonyRequire) {
118-
$this->filter = new PackageFilter($io, $symfonyRequire, $this->downloader);
122+
$this->filter = new PackageFilter($io, $symfonyRequire, $this->recipeProvider);
119123
}
120124

121125
$composerFile = Factory::getComposerFile();
@@ -134,7 +138,7 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__)
134138
}
135139
}
136140
if ($disable) {
137-
$downloader->disable();
141+
$this->recipeProvider->disable();
138142
}
139143

140144
$backtrace = $this->configureInstaller();
@@ -153,7 +157,7 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__)
153157
$input = $trace['args'][0];
154158
$app = $trace['object'];
155159

156-
$resolver = new PackageResolver($this->downloader);
160+
$resolver = new PackageResolver($this->recipeProvider);
157161

158162
try {
159163
$command = $input->getFirstArgument();
@@ -186,7 +190,7 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__)
186190

187191
$app->add(new Command\RecipesCommand($this, $this->lock, $rfs));
188192
$app->add(new Command\InstallRecipesCommand($this, $this->options->get('root-dir'), $this->options->get('runtime')['dotenv_path'] ?? '.env'));
189-
$app->add(new Command\UpdateRecipesCommand($this, $this->downloader, $rfs, $this->configurator, $this->options->get('root-dir')));
193+
$app->add(new Command\UpdateRecipesCommand($this, $this->recipeProvider, $rfs, $this->configurator, $this->options->get('root-dir')));
190194
$app->add(new Command\DumpEnvCommand($this->config, $this->options));
191195

192196
break;
@@ -215,7 +219,7 @@ public function configureInstaller()
215219
}
216220

217221
if (isset($trace['object']) && $trace['object'] instanceof GlobalCommand) {
218-
$this->downloader->disable();
222+
$this->recipeProvider->disable();
219223
}
220224
}
221225

@@ -224,7 +228,7 @@ public function configureInstaller()
224228

225229
public function configureProject(Event $event)
226230
{
227-
if (!$this->downloader->isEnabled()) {
231+
if (!$this->recipeProvider->isEnabled()) {
228232
$this->io->writeError('<warning>Project configuration is disabled: "symfony/flex" not found in the root composer.json</>');
229233

230234
return;
@@ -312,7 +316,7 @@ public function update(Event $event, $operations = [])
312316
$manipulator = new JsonManipulator($contents);
313317
$sortPackages = $this->composer->getConfig()->get('sort-packages');
314318
$symfonyVersion = $json['extra']['symfony']['require'] ?? null;
315-
$versions = $symfonyVersion ? $this->downloader->getVersions() : null;
319+
$versions = $symfonyVersion ? $this->recipeProvider->getVersions() : null;
316320
foreach (['require', 'require-dev'] as $type) {
317321
if (!isset($json['flex-'.$type])) {
318322
continue;
@@ -363,15 +367,15 @@ public function install(Event $event)
363367
$this->finish($rootDir);
364368
}
365369

366-
if ($this->downloader->isEnabled()) {
370+
if ($this->recipeProvider->isEnabled()) {
367371
$this->io->writeError('Run <comment>composer recipes</> at any time to see the status of your Symfony recipes.');
368372
$this->io->writeError('');
369373
}
370374

371375
return;
372376
}
373377

374-
$this->io->writeError(sprintf('<info>Symfony operations: %d recipe%s (%s)</>', \count($recipes), \count($recipes) > 1 ? 's' : '', $this->downloader->getSessionId()));
378+
$this->io->writeError(sprintf('<info>Symfony operations: %d recipe%s (%s)</>', \count($recipes), \count($recipes) > 1 ? 's' : '', $this->recipeProvider->getSessionId()));
375379
$installContribs = $this->composer->getPackage()->getExtra()['symfony']['allow-contrib'] ?? false;
376380
$manifest = null;
377381
$originalComposerJsonHash = $this->getComposerJsonHash();
@@ -527,13 +531,13 @@ public function executeAutoScripts(Event $event)
527531
*/
528532
public function fetchRecipes(array $operations, bool $reset): array
529533
{
530-
if (!$this->downloader->isEnabled()) {
534+
if (!$this->recipeProvider->isEnabled()) {
531535
$this->io->writeError('<warning>Symfony recipes are disabled: "symfony/flex" not found in the root composer.json</>');
532536

533537
return [];
534538
}
535539
$devPackages = null;
536-
$data = $this->downloader->getRecipes($operations);
540+
$data = $this->recipeProvider->getRecipes($operations);
537541
$manifests = $data['manifests'] ?? [];
538542
$locks = $data['locks'] ?? [];
539543
// symfony/flex recipes should always be applied first
@@ -562,8 +566,8 @@ public function fetchRecipes(array $operations, bool $reset): array
562566
}
563567

564568
while ($this->doesRecipeConflict($manifests[$name] ?? [], $operation)) {
565-
$this->downloader->removeRecipeFromIndex($name, $manifests[$name]['version']);
566-
$newData = $this->downloader->getRecipes([$operation]);
569+
$this->recipeProvider->removeRecipeFromIndex($name, $manifests[$name]['version']);
570+
$newData = $this->recipeProvider->getRecipes([$operation]);
567571
$newManifests = $newData['manifests'] ?? [];
568572

569573
if (!isset($newManifests[$name])) {
@@ -751,7 +755,7 @@ private function unpack(Event $event)
751755
}
752756
}
753757

754-
$unpacker = new Unpacker($this->composer, new PackageResolver($this->downloader), $this->dryRun);
758+
$unpacker = new Unpacker($this->composer, new PackageResolver($this->recipeProvider), $this->dryRun);
755759
$result = $unpacker->unpack($unpackOp);
756760

757761
if (!$result->getUnpacked()) {

0 commit comments

Comments
 (0)