Ajout des vendor

This commit is contained in:
2022-04-07 13:06:23 +02:00
parent ea47c93aa7
commit 5c116e15b1
1348 changed files with 184572 additions and 1 deletions

View File

@ -0,0 +1,18 @@
<?php
$finder = PhpCsFixer\Finder::create()->in(__DIR__);
return (new PhpCsFixer\Config())
->setFinder($finder)
->setRules(array(
'@Symfony' => true,
'@Symfony:risky' => true,
'fopen_flags' => false,
'@PHPUnit48Migration:risky' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => true,
'php_unit_no_expectation_annotation' => false, // part of `PHPUnitXYMigration:risky` ruleset, to be enabled when PHPUnit 4.x support will be dropped, as we don't want to rewrite exceptions handling twice
'protected_to_private' => false,
))
->setRiskyAllowed(true)
;

19
vendor/symfony/flex/LICENSE vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2016-2019 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

10
vendor/symfony/flex/README.md vendored Normal file
View File

@ -0,0 +1,10 @@
<p align="center"><a href="https://symfony.com" target="_blank">
<img src="https://symfony.com/logos/symfony_black_02.svg">
</a></p>
[Symfony Flex][1] helps developers create [Symfony][2] applications, from the most
simple micro-style projects to the more complex ones with dozens of
dependencies.
[1]: https://symfony.com/doc/current/setup/flex.html
[2]: https://symfony.com

32
vendor/symfony/flex/composer.json vendored Normal file
View File

@ -0,0 +1,32 @@
{
"name": "symfony/flex",
"type": "composer-plugin",
"description": "Composer plugin for Symfony",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien.potencier@gmail.com"
}
],
"minimum-stability": "dev",
"require": {
"php": ">=7.1",
"composer-plugin-api": "^1.0|^2.0"
},
"require-dev": {
"composer/composer": "^1.0.2|^2.0",
"symfony/dotenv": "^4.4|^5.0|^6.0",
"symfony/filesystem": "^4.4|^5.0|^6.0",
"symfony/phpunit-bridge": "^4.4.12|^5.0|^6.0",
"symfony/process": "^4.4|^5.0|^6.0"
},
"autoload": {
"psr-4": {
"Symfony\\Flex\\": "src"
}
},
"extra": {
"class": "Symfony\\Flex\\Flex"
}
}

161
vendor/symfony/flex/src/Cache.php vendored Normal file
View File

@ -0,0 +1,161 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\Cache as BaseCache;
use Composer\IO\IOInterface;
use Composer\Package\RootPackageInterface;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\VersionParser;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class Cache extends BaseCache
{
private $versions;
private $versionParser;
private $symfonyRequire;
private $rootConstraints = [];
private $symfonyConstraints;
private $downloader;
private $io;
public function setSymfonyRequire(string $symfonyRequire, RootPackageInterface $rootPackage, Downloader $downloader, IOInterface $io = null)
{
$this->versionParser = new VersionParser();
$this->symfonyRequire = $symfonyRequire;
$this->symfonyConstraints = $this->versionParser->parseConstraints($symfonyRequire);
$this->downloader = $downloader;
$this->io = $io;
foreach ($rootPackage->getRequires() + $rootPackage->getDevRequires() as $name => $link) {
$this->rootConstraints[$name] = $link->getConstraint();
}
}
public function read($file)
{
$content = parent::read($file);
if (0 === strpos($file, 'provider-symfony$') && \is_array($data = json_decode($content, true))) {
$content = json_encode($this->removeLegacyTags($data));
}
return $content;
}
public function removeLegacyTags(array $data): array
{
if (!$this->symfonyConstraints || !isset($data['packages'])) {
return $data;
}
foreach ($data['packages'] as $name => $versions) {
if (!isset($this->getVersions()['splits'][$name])) {
continue;
}
$rootConstraint = $this->rootConstraints[$name] ?? null;
$rootVersions = [];
foreach ($versions as $version => $composerJson) {
if (null !== $alias = $composerJson['extra']['branch-alias'][$version] ?? null) {
$normalizedVersion = $this->versionParser->normalize($alias);
} elseif (null === $normalizedVersion = $composerJson['version_normalized'] ?? null) {
continue;
}
$constraint = new Constraint('==', $normalizedVersion);
if ($rootConstraint && $rootConstraint->matches($constraint)) {
$rootVersions[$version] = $composerJson;
}
if (!$this->symfonyConstraints->matches($constraint)) {
if (null !== $this->io) {
$this->io->writeError(sprintf('<info>Restricting packages listed in "symfony/symfony" to "%s"</>', $this->symfonyRequire));
$this->io = null;
}
unset($versions[$version]);
}
}
if ($rootConstraint && !array_intersect_key($rootVersions, $versions)) {
$versions = $rootVersions;
}
$data['packages'][$name] = $versions;
}
if (null === $symfonySymfony = $data['packages']['symfony/symfony'] ?? null) {
return $data;
}
foreach ($symfonySymfony as $version => $composerJson) {
if (null !== $alias = $composerJson['extra']['branch-alias'][$version] ?? null) {
$normalizedVersion = $this->versionParser->normalize($alias);
} elseif (null === $normalizedVersion = $composerJson['version_normalized'] ?? null) {
continue;
}
if (!$this->symfonyConstraints->matches(new Constraint('==', $normalizedVersion))) {
unset($symfonySymfony[$version]);
}
}
if ($symfonySymfony) {
$data['packages']['symfony/symfony'] = $symfonySymfony;
}
return $data;
}
private function getVersions(): array
{
if (null !== $this->versions) {
return $this->versions;
}
$versions = $this->downloader->getVersions();
$this->downloader = null;
$okVersions = [];
if (!isset($versions['splits'])) {
throw new \LogicException('The Flex index is missing a "splits" entry. Did you forget to add "flex://defaults" in the "extra.symfony.endpoint" array of your composer.json?');
}
foreach ($versions['splits'] as $name => $vers) {
foreach ($vers as $i => $v) {
if (!isset($okVersions[$v])) {
$okVersions[$v] = false;
for ($j = 0; $j < 60; ++$j) {
if ($this->symfonyConstraints->matches(new Constraint('==', $v.'.'.$j.'.0'))) {
$okVersions[$v] = true;
break;
}
}
}
if (!$okVersions[$v]) {
unset($vers[$i]);
}
}
if (!$vers || $vers === $versions['splits'][$name]) {
unset($versions['splits'][$name]);
}
}
return $this->versions = $versions;
}
}

View File

@ -0,0 +1,147 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Command;
use Composer\Command\BaseCommand;
use Composer\Config;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Dotenv\Dotenv;
use Symfony\Flex\Options;
class DumpEnvCommand extends BaseCommand
{
private $config;
private $options;
public function __construct(Config $config, Options $options)
{
$this->config = $config;
$this->options = $options;
parent::__construct();
}
protected function configure()
{
$this->setName('symfony:dump-env')
->setAliases(['dump-env'])
->setDescription('Compiles .env files to .env.local.php.')
->setDefinition([
new InputArgument('env', InputArgument::OPTIONAL, 'The application environment to dump .env files for - e.g. "prod".'),
])
->addOption('empty', null, InputOption::VALUE_NONE, 'Ignore the content of .env files')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$runtime = $this->options->get('runtime') ?? [];
$envKey = $runtime['env_var_name'] ?? 'APP_ENV';
if ($env = $input->getArgument('env') ?? $runtime['env'] ?? null) {
$_SERVER[$envKey] = $env;
}
$path = $this->options->get('root-dir').'/'.($runtime['dotenv_path'] ?? '.env');
if (!$env || !$input->getOption('empty')) {
$vars = $this->loadEnv($path, $env, $runtime);
$env = $vars[$envKey];
}
if ($input->getOption('empty')) {
$vars = [$envKey => $env];
}
$vars = var_export($vars, true);
$vars = <<<EOF
<?php
// This file was generated by running "composer dump-env $env"
return $vars;
EOF;
file_put_contents($path.'.local.php', $vars, \LOCK_EX);
$this->getIO()->writeError('Successfully dumped .env files in <info>.env.local.php</>');
return 0;
}
private function loadEnv(string $path, ?string $env, array $runtime): array
{
if (!file_exists($autoloadFile = $this->config->get('vendor-dir').'/autoload.php')) {
throw new \RuntimeException(sprintf('Please run "composer install" before running this command: "%s" not found.', $autoloadFile));
}
require $autoloadFile;
if (!class_exists(Dotenv::class)) {
throw new \RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.');
}
$envKey = $runtime['env_var_name'] ?? 'APP_ENV';
$globalsBackup = [$_SERVER, $_ENV];
unset($_SERVER[$envKey]);
$_ENV = [$envKey => $env];
$_SERVER['SYMFONY_DOTENV_VARS'] = implode(',', array_keys($_SERVER));
putenv('SYMFONY_DOTENV_VARS='.$_SERVER['SYMFONY_DOTENV_VARS']);
try {
if (method_exists(Dotenv::class, 'usePutenv')) {
$dotenv = new Dotenv();
} else {
$dotenv = new Dotenv(false);
}
if (!$env && file_exists($p = "$path.local")) {
$env = $_ENV[$envKey] = $dotenv->parse(file_get_contents($p), $p)[$envKey] ?? null;
}
if (!$env) {
throw new \RuntimeException(sprintf('Please provide the name of the environment either by passing it as command line argument or by defining the "%s" variable in the ".env.local" file.', $envKey));
}
$testEnvs = $runtime['test_envs'] ?? ['test'];
if (method_exists($dotenv, 'loadEnv')) {
$dotenv->loadEnv($path, $envKey, 'dev', $testEnvs);
} else {
// fallback code in case your Dotenv component is not 4.2 or higher (when loadEnv() was added)
$dotenv->load(file_exists($path) || !file_exists($p = "$path.dist") ? $path : $p);
if (!\in_array($env, $testEnvs, true) && file_exists($p = "$path.local")) {
$dotenv->load($p);
}
if (file_exists($p = "$path.$env")) {
$dotenv->load($p);
}
if (file_exists($p = "$path.$env.local")) {
$dotenv->load($p);
}
}
unset($_ENV['SYMFONY_DOTENV_VARS']);
$env = $_ENV;
} finally {
list($_SERVER, $_ENV) = $globalsBackup;
}
return $env;
}
}

View File

@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class GenerateIdCommand extends Command
{
public function __construct()
{
// No-op to support downgrading to v1.12.x
parent::__construct();
}
protected function configure()
{
$this->setName('symfony:generate-id');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$ui = new SymfonyStyle($input, $output);
$ui->error('This command is a noop and should not be used anymore.');
return 1;
}
}

View File

@ -0,0 +1,181 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Command;
use Composer\Command\BaseCommand;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\Util\ProcessExecutor;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Flex\Event\UpdateEvent;
use Symfony\Flex\Flex;
class InstallRecipesCommand extends BaseCommand
{
/** @var Flex */
private $flex;
private $rootDir;
private $dotenvPath;
public function __construct(/* cannot be type-hinted */ $flex, string $rootDir, string $dotenvPath = '.env')
{
$this->flex = $flex;
$this->rootDir = $rootDir;
$this->dotenvPath = $dotenvPath;
parent::__construct();
}
protected function configure()
{
$this->setName('symfony:recipes:install')
->setAliases(['recipes:install', 'symfony:sync-recipes', 'sync-recipes', 'fix-recipes'])
->setDescription('Installs or reinstalls recipes for already installed packages.')
->addArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Recipes that should be installed.')
->addOption('force', null, InputOption::VALUE_NONE, 'Overwrite existing files when a new version of a recipe is available')
->addOption('reset', null, InputOption::VALUE_NONE, 'Reset all recipes back to their initial state (should be combined with --force)')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$win = '\\' === \DIRECTORY_SEPARATOR;
$force = (bool) $input->getOption('force');
if ($force && !@is_executable(strtok(exec($win ? 'where git' : 'command -v git'), \PHP_EOL))) {
throw new RuntimeException('Cannot run "sync-recipes --force": git not found.');
}
$symfonyLock = $this->flex->getLock();
$composer = $this->getComposer();
$locker = $composer->getLocker();
$lockData = $locker->getLockData();
$packages = [];
$totalPackages = [];
foreach ($lockData['packages'] as $pkg) {
$totalPackages[] = $pkg['name'];
if ($force || !$symfonyLock->has($pkg['name'])) {
$packages[] = $pkg['name'];
}
}
foreach ($lockData['packages-dev'] as $pkg) {
$totalPackages[] = $pkg['name'];
if ($force || !$symfonyLock->has($pkg['name'])) {
$packages[] = $pkg['name'];
}
}
$io = $this->getIO();
if (!$io->isVerbose()) {
$io->writeError([
'Run command with <info>-v</info> to see more details',
'',
]);
}
if ($targetPackages = $input->getArgument('packages')) {
if ($invalidPackages = array_diff($targetPackages, $totalPackages)) {
$io->writeError(sprintf('<warning>Cannot update: some packages are not installed:</warning> %s', implode(', ', $invalidPackages)));
return 1;
}
if ($packagesRequiringForce = array_diff($targetPackages, $packages)) {
$io->writeError(sprintf('Recipe(s) already installed for: <info>%s</info>', implode(', ', $packagesRequiringForce)));
$io->writeError('Re-run the command with <info>--force</info> to re-install the recipes.');
$io->writeError('');
}
$packages = array_diff($targetPackages, $packagesRequiringForce);
}
if (!$packages) {
$io->writeError('No recipes to install.');
return 0;
}
$composer = $this->getComposer();
$installedRepo = $composer->getRepositoryManager()->getLocalRepository();
$operations = [];
foreach ($packages as $package) {
if (null === $pkg = $installedRepo->findPackage($package, '*')) {
$io->writeError(sprintf('<error>Package %s is not installed</>', $package));
return 1;
}
$operations[] = new InstallOperation($pkg);
}
$dotenvFile = $this->dotenvPath;
$dotenvPath = $this->rootDir.'/'.$dotenvFile;
if ($createEnvLocal = $force && file_exists($dotenvPath) && file_exists($dotenvPath.'.dist') && !file_exists($dotenvPath.'.local')) {
rename($dotenvPath, $dotenvPath.'.local');
$pipes = [];
proc_close(proc_open(sprintf('git mv %s %s > %s 2>&1 || %s %1$s %2$s', ProcessExecutor::escape($dotenvFile.'.dist'), ProcessExecutor::escape($dotenvFile), $win ? 'NUL' : '/dev/null', $win ? 'rename' : 'mv'), $pipes, $pipes, $this->rootDir));
if (file_exists($this->rootDir.'/phpunit.xml.dist')) {
touch($dotenvPath.'.test');
}
}
$this->flex->update(new UpdateEvent($force, (bool) $input->getOption('reset')), $operations);
if ($force) {
$output = [
'',
'<bg=blue;fg=white> </>',
'<bg=blue;fg=white> Files have been reset to the latest version of the recipe. </>',
'<bg=blue;fg=white> </>',
'',
' * Use <comment>git diff</> to inspect the changes.',
'',
' Not all of the changes will be relevant to your app: you now',
' need to selectively add or revert them using e.g. a combination',
' of <comment>git add -p</> and <comment>git checkout -p</>',
'',
];
if ($createEnvLocal) {
$output[] = ' Dotenv files have been renamed: .env -> .env.local and .env.dist -> .env';
$output[] = ' See https://symfony.com/doc/current/configuration/dot-env-changes.html';
$output[] = '';
}
$output[] = ' * Use <comment>git checkout .</> to revert the changes.';
$output[] = '';
if ($createEnvLocal) {
$root = '.' !== $this->rootDir ? $this->rootDir.'/' : '';
$output[] = ' To revert the changes made to .env files, run';
$output[] = sprintf(' <comment>git mv %s %s</> && <comment>%s %s %1$s</>', ProcessExecutor::escape($root.$dotenvFile), ProcessExecutor::escape($root.$dotenvFile.'.dist'), $win ? 'rename' : 'mv', ProcessExecutor::escape($root.$dotenvFile.'.local'));
$output[] = '';
}
$output[] = ' New (untracked) files can be inspected using <comment>git clean --dry-run</>';
$output[] = ' Add the new files you want to keep using <comment>git add</>';
$output[] = ' then delete the rest using <comment>git clean --force</>';
$output[] = '';
$io->write($output);
}
return 0;
}
}

View File

@ -0,0 +1,329 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Command;
use Composer\Command\BaseCommand;
use Composer\Downloader\TransportException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Flex\GithubApi;
use Symfony\Flex\InformationOperation;
use Symfony\Flex\Lock;
use Symfony\Flex\Recipe;
/**
* @author Maxime Hélias <maximehelias16@gmail.com>
*/
class RecipesCommand extends BaseCommand
{
/** @var \Symfony\Flex\Flex */
private $flex;
private $symfonyLock;
private $githubApi;
public function __construct(/* cannot be type-hinted */ $flex, Lock $symfonyLock, $downloader)
{
$this->flex = $flex;
$this->symfonyLock = $symfonyLock;
$this->githubApi = new GithubApi($downloader);
parent::__construct();
}
protected function configure()
{
$this->setName('symfony:recipes')
->setAliases(['recipes'])
->setDescription('Shows information about all available recipes.')
->setDefinition([
new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect, if not provided all packages are.'),
])
->addOption('outdated', 'o', InputOption::VALUE_NONE, 'Show only recipes that are outdated')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository();
// Inspect one or all packages
$package = $input->getArgument('package');
if (null !== $package) {
$packages = [0 => ['name' => strtolower($package)]];
} else {
$locker = $this->getComposer()->getLocker();
$lockData = $locker->getLockData();
// Merge all packages installed
$packages = array_merge($lockData['packages'], $lockData['packages-dev']);
}
$operations = [];
foreach ($packages as $value) {
if (null === $pkg = $installedRepo->findPackage($value['name'], '*')) {
$this->getIO()->writeError(sprintf('<error>Package %s is not installed</error>', $value['name']));
continue;
}
$operations[] = new InformationOperation($pkg);
}
$recipes = $this->flex->fetchRecipes($operations, false);
ksort($recipes);
$nbRecipe = \count($recipes);
if ($nbRecipe <= 0) {
$this->getIO()->writeError('<error>No recipe found</error>');
return 1;
}
// Display the information about a specific recipe
if (1 === $nbRecipe) {
$this->displayPackageInformation(current($recipes));
return 0;
}
$outdated = $input->getOption('outdated');
$write = [];
$hasOutdatedRecipes = false;
/** @var Recipe $recipe */
foreach ($recipes as $name => $recipe) {
$lockRef = $this->symfonyLock->get($name)['recipe']['ref'] ?? null;
$additional = null;
if (null === $lockRef && null !== $recipe->getRef()) {
$additional = '<comment>(recipe not installed)</comment>';
} elseif ($recipe->getRef() !== $lockRef && !$recipe->isAuto()) {
$additional = '<comment>(update available)</comment>';
}
if ($outdated && null === $additional) {
continue;
}
$hasOutdatedRecipes = true;
$write[] = sprintf(' * %s %s', $name, $additional);
}
// Nothing to display
if (!$hasOutdatedRecipes) {
return 0;
}
$this->getIO()->write(array_merge([
'',
'<bg=blue;fg=white> </>',
sprintf('<bg=blue;fg=white> %s recipes. </>', $outdated ? ' Outdated' : 'Available'),
'<bg=blue;fg=white> </>',
'',
], $write, [
'',
'Run:',
' * <info>composer recipes vendor/package</info> to see details about a recipe.',
' * <info>composer recipes:update vendor/package</info> to update that recipe.',
'',
]));
if ($outdated) {
return 1;
}
return 0;
}
private function displayPackageInformation(Recipe $recipe)
{
$io = $this->getIO();
$recipeLock = $this->symfonyLock->get($recipe->getName());
$lockRef = $recipeLock['recipe']['ref'] ?? null;
$lockRepo = $recipeLock['recipe']['repo'] ?? null;
$lockFiles = $recipeLock['files'] ?? null;
$lockBranch = $recipeLock['recipe']['branch'] ?? null;
$lockVersion = $recipeLock['recipe']['version'] ?? $recipeLock['version'] ?? null;
$status = '<comment>up to date</comment>';
if ($recipe->isAuto()) {
$status = '<comment>auto-generated recipe</comment>';
} elseif (null === $lockRef && null !== $recipe->getRef()) {
$status = '<comment>recipe not installed</comment>';
} elseif ($recipe->getRef() !== $lockRef) {
$status = '<comment>update available</comment>';
}
$gitSha = null;
$commitDate = null;
if (null !== $lockRef && null !== $lockRepo) {
try {
$recipeCommitData = $this->githubApi->findRecipeCommitDataFromTreeRef(
$recipe->getName(),
$lockRepo,
$lockBranch ?? '',
$lockVersion,
$lockRef
);
$gitSha = $recipeCommitData ? $recipeCommitData['commit'] : null;
$commitDate = $recipeCommitData ? $recipeCommitData['date'] : null;
} catch (TransportException $exception) {
$io->writeError('Error downloading exact git sha for installed recipe.');
}
}
$io->write('<info>name</info> : '.$recipe->getName());
$io->write('<info>version</info> : '.($lockVersion ?? 'n/a'));
$io->write('<info>status</info> : '.$status);
if (!$recipe->isAuto() && null !== $lockVersion) {
$recipeUrl = sprintf(
'https://%s/tree/%s/%s/%s',
$lockRepo,
// if something fails, default to the branch as the closest "sha"
$gitSha ?? $lockBranch,
$recipe->getName(),
$lockVersion
);
$io->write('<info>installed recipe</info> : '.$recipeUrl);
}
if ($lockRef !== $recipe->getRef()) {
$io->write('<info>latest recipe</info> : '.$recipe->getURL());
}
if ($lockRef !== $recipe->getRef() && null !== $lockVersion) {
$historyUrl = sprintf(
'https://%s/commits/%s/%s',
$lockRepo,
$lockBranch,
$recipe->getName()
);
// show commits since one second after the currently-installed recipe
if (null !== $commitDate) {
$historyUrl .= '?since='.(new \DateTime($commitDate))->modify('+1 seconds')->format('c\Z');
}
$io->write('<info>recipe history</info> : '.$historyUrl);
}
if (null !== $lockFiles) {
$io->write('<info>files</info> : ');
$io->write('');
$tree = $this->generateFilesTree($lockFiles);
$this->displayFilesTree($tree);
}
if ($lockRef !== $recipe->getRef()) {
$io->write([
'',
'Update this recipe by running:',
sprintf('<info>composer recipes:update %s</info>', $recipe->getName()),
]);
}
}
private function generateFilesTree(array $files): array
{
$tree = [];
foreach ($files as $file) {
$path = explode('/', $file);
$tree = array_merge_recursive($tree, $this->addNode($path));
}
return $tree;
}
private function addNode(array $node): array
{
$current = array_shift($node);
$subTree = [];
if (null !== $current) {
$subTree[$current] = $this->addNode($node);
}
return $subTree;
}
/**
* Note : We do not display file modification information with Configurator like ComposerScripts, Container, DockerComposer, Dockerfile, Env, Gitignore and Makefile.
*/
private function displayFilesTree(array $tree)
{
end($tree);
$endKey = key($tree);
foreach ($tree as $dir => $files) {
$treeBar = '├';
$total = \count($files);
if (0 === $total || $endKey === $dir) {
$treeBar = '└';
}
$info = sprintf(
'%s──%s',
$treeBar,
$dir
);
$this->writeTreeLine($info);
$treeBar = str_replace('└', ' ', $treeBar);
$this->displayTree($files, $treeBar);
}
}
private function displayTree(array $tree, $previousTreeBar = '├', $level = 1)
{
$previousTreeBar = str_replace('├', '│', $previousTreeBar);
$treeBar = $previousTreeBar.' ├';
$i = 0;
$total = \count($tree);
foreach ($tree as $dir => $files) {
++$i;
if ($i === $total) {
$treeBar = $previousTreeBar.' └';
}
$info = sprintf(
'%s──%s',
$treeBar,
$dir
);
$this->writeTreeLine($info);
$treeBar = str_replace('└', ' ', $treeBar);
$this->displayTree($files, $treeBar, $level + 1);
}
}
private function writeTreeLine($line)
{
$io = $this->getIO();
if (!$io->isDecorated()) {
$line = str_replace(['└', '├', '──', '│'], ['`-', '|-', '-', '|'], $line);
}
$io->write($line);
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Command;
use Composer\Command\RemoveCommand as BaseRemoveCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Flex\PackageResolver;
class RemoveCommand extends BaseRemoveCommand
{
private $resolver;
public function __construct(PackageResolver $resolver)
{
$this->resolver = $resolver;
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$input->setArgument('packages', $this->resolver->resolve($input->getArgument('packages')));
return parent::execute($input, $output);
}
}

View File

@ -0,0 +1,90 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Command;
use Composer\Command\RequireCommand as BaseRequireCommand;
use Composer\Factory;
use Composer\Json\JsonFile;
use Composer\Json\JsonManipulator;
use Composer\Plugin\PluginInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Flex\PackageResolver;
class RequireCommand extends BaseRequireCommand
{
private $resolver;
private $updateComposerLock;
public function __construct(PackageResolver $resolver, \Closure $updateComposerLock = null)
{
$this->resolver = $resolver;
$this->updateComposerLock = $updateComposerLock;
parent::__construct();
}
protected function configure()
{
parent::configure();
$this->addOption('no-unpack', null, InputOption::VALUE_NONE, '[DEPRECATED] Disable unpacking Symfony packs in composer.json.');
$this->addOption('unpack', null, InputOption::VALUE_NONE, '[DEPRECATED] Unpacking is now enabled by default.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
if ($input->getOption('no-unpack')) {
$this->getIO()->writeError('<warning>The "--unpack" command line option is deprecated; unpacking is now enabled by default.</warning>');
}
if ($input->getOption('unpack')) {
$this->getIO()->writeError('<warning>The "--unpack" command line option is deprecated; unpacking is now enabled by default.</warning>');
}
$packages = $this->resolver->resolve($input->getArgument('packages'), true);
if ($packages) {
$input->setArgument('packages', $this->resolver->resolve($input->getArgument('packages'), true));
}
if (version_compare('2.0.0', PluginInterface::PLUGIN_API_VERSION, '>') && $input->hasOption('no-suggest')) {
$input->setOption('no-suggest', true);
}
$file = Factory::getComposerFile();
$contents = file_get_contents($file);
$json = JsonFile::parseJson($contents);
if (\array_key_exists('require-dev', $json) && !$json['require-dev'] && (new JsonManipulator($contents))->removeMainKey('require-dev')) {
$manipulator = new JsonManipulator($contents);
$manipulator->addLink('require-dev', 'php', '*');
file_put_contents($file, $manipulator->getContents());
} else {
$file = null;
}
unset($contents, $json, $manipulator);
try {
return parent::execute($input, $output) ?? 0;
} finally {
if (null !== $file) {
$manipulator = new JsonManipulator(file_get_contents($file));
$manipulator->removeSubNode('require-dev', 'php');
file_put_contents($file, $manipulator->getContents());
if ($this->updateComposerLock) {
($this->updateComposerLock)();
}
}
}
}
}

View File

@ -0,0 +1,128 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Command;
use Composer\Command\BaseCommand;
use Composer\Factory;
use Composer\Installer;
use Composer\Package\Version\VersionParser;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Flex\PackageResolver;
use Symfony\Flex\Unpack\Operation;
use Symfony\Flex\Unpacker;
/**
* @deprecated since Flex 1.4
*/
class UnpackCommand extends BaseCommand
{
private $resolver;
public function __construct(PackageResolver $resolver)
{
$this->resolver = $resolver;
parent::__construct();
}
protected function configure()
{
$this->setName('symfony:unpack')
->setAliases(['unpack'])
->setDescription('[DEPRECATED] Unpacks a Symfony pack.')
->setDefinition([
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Installed packages to unpack.'),
new InputOption('sort-packages', null, InputOption::VALUE_NONE, 'Sorts packages'),
])
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$composer = $this->getComposer();
$packages = $this->resolver->resolve($input->getArgument('packages'), true);
$io = $this->getIO();
$lockData = $composer->getLocker()->getLockData();
$installedRepo = $composer->getRepositoryManager()->getLocalRepository();
$versionParser = new VersionParser();
$dryRun = $input->hasOption('dry-run') && $input->getOption('dry-run');
$io->writeError('<warning>Command "symfony:unpack" is deprecated, Symfony packs are always unpacked now.</>');
$op = new Operation(true, $input->getOption('sort-packages') || $composer->getConfig()->get('sort-packages'));
foreach ($versionParser->parseNameVersionPairs($packages) as $package) {
if (null === $pkg = $installedRepo->findPackage($package['name'], '*')) {
$io->writeError(sprintf('<error>Package %s is not installed</>', $package['name']));
return 1;
}
$dev = false;
foreach ($lockData['packages-dev'] as $p) {
if ($package['name'] === $p['name']) {
$dev = true;
break;
}
}
$op->addPackage($pkg->getName(), $pkg->getVersion(), $dev);
}
$unpacker = new Unpacker($composer, $this->resolver, $dryRun);
$result = $unpacker->unpack($op);
// remove the packages themselves
if (!$result->getUnpacked()) {
$io->writeError('<info>Nothing to unpack</>');
return 0;
}
$io->writeError('<info>Unpacking Symfony packs</>');
foreach ($result->getUnpacked() as $pkg) {
$io->writeError(sprintf(' - Unpacked <info>%s</>', $pkg->getName()));
}
$unpacker->updateLock($result, $io);
if ($input->hasOption('no-install') && $input->getOption('no-install')) {
return 0;
}
$composer = Factory::create($io, null, true);
$installer = Installer::create($io, $composer);
$installer
->setDryRun($dryRun)
->setDevMode(true)
->setDumpAutoloader(false)
->setIgnorePlatformRequirements(true)
->setUpdate(true)
->setUpdateAllowList(['php'])
;
if (method_exists($composer->getEventDispatcher(), 'setRunScripts')) {
$composer->getEventDispatcher()->setRunScripts(false);
} else {
$installer->setRunScripts(false);
}
if (method_exists($installer, 'setSkipSuggest')) {
$installer->setSkipSuggest(true);
}
return $installer->run();
}
}

View File

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Command;
use Composer\Command\UpdateCommand as BaseUpdateCommand;
use Composer\Plugin\PluginInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Flex\PackageResolver;
class UpdateCommand extends BaseUpdateCommand
{
private $resolver;
public function __construct(PackageResolver $resolver)
{
$this->resolver = $resolver;
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$input->setArgument('packages', $this->resolver->resolve($input->getArgument('packages')));
if (version_compare('2.0.0', PluginInterface::PLUGIN_API_VERSION, '>') && $input->hasOption('no-suggest')) {
$input->setOption('no-suggest', true);
}
return parent::execute($input, $output);
}
}

View File

@ -0,0 +1,423 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Command;
use Composer\Command\BaseCommand;
use Composer\IO\IOInterface;
use Composer\Util\ProcessExecutor;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Flex\Configurator;
use Symfony\Flex\Downloader;
use Symfony\Flex\Flex;
use Symfony\Flex\GithubApi;
use Symfony\Flex\InformationOperation;
use Symfony\Flex\Lock;
use Symfony\Flex\Recipe;
use Symfony\Flex\Update\RecipePatcher;
use Symfony\Flex\Update\RecipeUpdate;
class UpdateRecipesCommand extends BaseCommand
{
/** @var Flex */
private $flex;
private $downloader;
private $configurator;
private $rootDir;
private $githubApi;
private $processExecutor;
public function __construct(/* cannot be type-hinted */ $flex, Downloader $downloader, $httpDownloader, Configurator $configurator, string $rootDir)
{
$this->flex = $flex;
$this->downloader = $downloader;
$this->configurator = $configurator;
$this->rootDir = $rootDir;
$this->githubApi = new GithubApi($httpDownloader);
parent::__construct();
}
protected function configure()
{
$this->setName('symfony:recipes:update')
->setAliases(['recipes:update'])
->setDescription('Updates an already-installed recipe to the latest version.')
->addArgument('package', InputArgument::OPTIONAL, 'Recipe that should be updated.')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$win = '\\' === \DIRECTORY_SEPARATOR;
$runtimeExceptionClass = class_exists(RuntimeException::class) ? RuntimeException::class : \RuntimeException::class;
if (!@is_executable(strtok(exec($win ? 'where git' : 'command -v git'), \PHP_EOL))) {
throw new $runtimeExceptionClass('Cannot run "recipes:update": git not found.');
}
$io = $this->getIO();
if (!$this->isIndexClean($io)) {
$io->write([
' Cannot run <comment>recipes:update</comment>: Your git index contains uncommitted changes.',
' Please commit or stash them and try again!',
]);
return 1;
}
$packageName = $input->getArgument('package');
$symfonyLock = $this->flex->getLock();
if (!$packageName) {
$packageName = $this->askForPackage($io, $symfonyLock);
if (null === $packageName) {
$io->writeError('All packages appear to be up-to-date!');
return 0;
}
}
if (!$symfonyLock->has($packageName)) {
$io->writeError([
'Package not found inside symfony.lock. It looks like it\'s not installed?',
sprintf('Try running <info>composer recipes:install %s --force -v</info> to re-install the recipe.', $packageName),
]);
return 1;
}
$packageLockData = $symfonyLock->get($packageName);
if (!isset($packageLockData['recipe'])) {
$io->writeError([
'It doesn\'t look like this package had a recipe when it was originally installed.',
'To install the latest version of the recipe, if there is one, run:',
sprintf(' <info>composer recipes:install %s --force -v</info>', $packageName),
]);
return 1;
}
$recipeRef = $packageLockData['recipe']['ref'] ?? null;
$recipeVersion = $packageLockData['recipe']['version'] ?? null;
if (!$recipeRef || !$recipeVersion) {
$io->writeError([
'The version of the installed recipe was not saved into symfony.lock.',
'This is possible if it was installed by an old version of Symfony Flex.',
'Update the recipe by re-installing the latest version with:',
sprintf(' <info>composer recipes:install %s --force -v</info>', $packageName),
]);
return 1;
}
$originalRecipe = $this->getRecipe($packageName, $recipeRef, $recipeVersion);
if (null === $originalRecipe) {
$io->writeError([
'The original recipe version you have installed could not be found, it may be too old.',
'Update the recipe by re-installing the latest version with:',
sprintf(' <info>composer recipes:install %s --force -v</info>', $packageName),
]);
return 1;
}
$newRecipe = $this->getRecipe($packageName);
if ($newRecipe->getRef() === $originalRecipe->getRef()) {
$io->write(sprintf('This recipe for <info>%s</info> is already at the latest version.', $packageName));
return 0;
}
$io->write([
sprintf(' Updating recipe for <info>%s</info>...', $packageName),
'',
]);
$recipeUpdate = new RecipeUpdate($originalRecipe, $newRecipe, $symfonyLock, $this->rootDir);
$this->configurator->populateUpdate($recipeUpdate);
$originalComposerJsonHash = $this->flex->getComposerJsonHash();
$patcher = new RecipePatcher($this->rootDir, $io);
try {
$patch = $patcher->generatePatch($recipeUpdate->getOriginalFiles(), $recipeUpdate->getNewFiles());
$hasConflicts = !$patcher->applyPatch($patch);
} catch (\Throwable $throwable) {
$io->writeError([
'<bg=red;fg=white>There was an error applying the recipe update patch</>',
$throwable->getMessage(),
'',
'Update the recipe by re-installing the latest version with:',
sprintf(' <info>composer recipes:install %s --force -v</info>', $packageName),
]);
return 1;
}
$symfonyLock->add($packageName, $newRecipe->getLock());
$this->flex->finish($this->rootDir, $originalComposerJsonHash);
// stage symfony.lock, as all patched files with already be staged
$cmdOutput = '';
$this->getProcessExecutor()->execute('git add symfony.lock', $cmdOutput, $this->rootDir);
$io->write([
' <bg=blue;fg=white> </>',
' <bg=blue;fg=white> Yes! Recipe updated! </>',
' <bg=blue;fg=white> </>',
'',
]);
if ($hasConflicts) {
$io->write([
' The recipe was updated but with <bg=red;fg=white>one or more conflicts</>.',
' Run <comment>git status</comment> to see them.',
' After resolving, commit your changes like normal.',
]);
} else {
if (!$patch->getPatch()) {
// no changes were required
$io->write([
' No files were changed as a result of the update.',
]);
} else {
$io->write([
' Run <comment>git status</comment> or <comment>git diff --cached</comment> to see the changes.',
' When you\'re ready, commit these changes like normal.',
]);
}
}
if (0 !== \count($recipeUpdate->getCopyFromPackagePaths())) {
$io->write([
'',
' <bg=red;fg=white>NOTE:</>',
' This recipe copies the following paths from the bundle into your app:',
]);
foreach ($recipeUpdate->getCopyFromPackagePaths() as $source => $target) {
$io->write(sprintf(' * %s => %s', $source, $target));
}
$io->write([
'',
' The recipe updater has no way of knowing if these files have changed since you originally installed the recipe.',
' And so, no updates were made to these paths.',
]);
}
if (0 !== \count($patch->getRemovedPatches())) {
if (1 === \count($patch->getRemovedPatches())) {
$notes = [
sprintf(' The file <comment>%s</comment> was not updated because it doesn\'t exist in your app.', array_keys($patch->getRemovedPatches())[0]),
];
} else {
$notes = [' The following files were not updated because they don\'t exist in your app:'];
foreach ($patch->getRemovedPatches() as $filename => $contents) {
$notes[] = sprintf(' * <comment>%s</comment>', $filename);
}
}
$io->write([
'',
' <bg=red;fg=white>NOTE:</>',
]);
$io->write($notes);
$io->write('');
if ($io->askConfirmation(' Would you like to save the "diff" to a file so you can review it? (Y/n) ')) {
$patchFilename = str_replace('/', '.', $packageName).'.updates-for-deleted-files.patch';
file_put_contents($this->rootDir.'/'.$patchFilename, implode("\n", $patch->getRemovedPatches()));
$io->write([
'',
sprintf(' Saved diff to <info>%s</info>', $patchFilename),
]);
}
}
if ($patch->getPatch()) {
$io->write('');
$io->write(' Calculating CHANGELOG...', false);
$changelog = $this->generateChangelog($originalRecipe);
$io->write("\r", false); // clear current line
if ($changelog) {
$io->write($changelog);
} else {
$io->write('No CHANGELOG could be calculated.');
}
}
return 0;
}
private function getRecipe(string $packageName, string $recipeRef = null, string $recipeVersion = null): ?Recipe
{
$installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository();
$package = $installedRepo->findPackage($packageName, '*');
if (null === $package) {
throw new RuntimeException(sprintf('Could not find package "%s". Try running "composer install".', $packageName));
}
$operation = new InformationOperation($package);
if (null !== $recipeRef) {
$operation->setSpecificRecipeVersion($recipeRef, $recipeVersion);
}
$recipes = $this->downloader->getRecipes([$operation]);
if (0 === \count($recipes['manifests'] ?? [])) {
return null;
}
return new Recipe(
$package,
$packageName,
$operation->getOperationType(),
$recipes['manifests'][$packageName],
$recipes['locks'][$packageName] ?? []
);
}
private function generateChangelog(Recipe $originalRecipe): ?array
{
$recipeData = $originalRecipe->getLock()['recipe'] ?? null;
if (null === $recipeData) {
return null;
}
if (!isset($recipeData['ref']) || !isset($recipeData['repo']) || !isset($recipeData['branch']) || !isset($recipeData['version'])) {
return null;
}
$currentRecipeVersionData = $this->githubApi->findRecipeCommitDataFromTreeRef(
$originalRecipe->getName(),
$recipeData['repo'],
$recipeData['branch'],
$recipeData['version'],
$recipeData['ref']
);
if (!$currentRecipeVersionData) {
return null;
}
$recipeVersions = $this->githubApi->getVersionsOfRecipe(
$recipeData['repo'],
$recipeData['branch'],
$originalRecipe->getName()
);
if (!$recipeVersions) {
return null;
}
$newerRecipeVersions = array_filter($recipeVersions, function ($version) use ($recipeData) {
return version_compare($version, $recipeData['version'], '>');
});
$newCommits = $currentRecipeVersionData['new_commits'];
foreach ($newerRecipeVersions as $newerRecipeVersion) {
$newCommits = array_merge(
$newCommits,
$this->githubApi->getCommitDataForPath($recipeData['repo'], $originalRecipe->getName().'/'.$newerRecipeVersion, $recipeData['branch'])
);
}
$newCommits = array_unique($newCommits);
asort($newCommits);
$pullRequests = [];
foreach ($newCommits as $commit => $date) {
$pr = $this->githubApi->getPullRequestForCommit($commit, $recipeData['repo']);
if ($pr) {
$pullRequests[$pr['number']] = $pr;
}
}
$lines = [];
// borrowed from symfony/console's OutputFormatterStyle
$handlesHrefGracefully = 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR')
&& (!getenv('KONSOLE_VERSION') || (int) getenv('KONSOLE_VERSION') > 201100);
foreach ($pullRequests as $number => $data) {
$url = $data['url'];
if ($handlesHrefGracefully) {
$url = "\033]8;;$url\033\\$number\033]8;;\033\\";
}
$lines[] = sprintf(' * %s (PR %s)', $data['title'], $url);
}
return $lines;
}
private function askForPackage(IOInterface $io, Lock $symfonyLock): ?string
{
$installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository();
$locker = $this->getComposer()->getLocker();
$lockData = $locker->getLockData();
// Merge all packages installed
$packages = array_merge($lockData['packages'], $lockData['packages-dev']);
$operations = [];
foreach ($packages as $value) {
if (null === $pkg = $installedRepo->findPackage($value['name'], '*')) {
continue;
}
$operations[] = new InformationOperation($pkg);
}
$recipes = $this->flex->fetchRecipes($operations, false);
ksort($recipes);
$outdatedRecipes = [];
foreach ($recipes as $name => $recipe) {
$lockRef = $symfonyLock->get($name)['recipe']['ref'] ?? null;
if (null !== $lockRef && $recipe->getRef() !== $lockRef && !$recipe->isAuto()) {
$outdatedRecipes[] = $name;
}
}
if (0 === \count($outdatedRecipes)) {
return null;
}
$question = 'Which outdated recipe would you like to update? (default: <info>0</info>)';
$choice = $io->select(
$question,
$outdatedRecipes,
0
);
return $outdatedRecipes[$choice];
}
private function isIndexClean(IOInterface $io): bool
{
$output = '';
$this->getProcessExecutor()->execute('git status --porcelain --untracked-files=no', $output, $this->rootDir);
if ('' !== trim($output)) {
return false;
}
return true;
}
private function getProcessExecutor(): ProcessExecutor
{
if (null === $this->processExecutor) {
$this->processExecutor = new ProcessExecutor($this->getIO());
}
return $this->processExecutor;
}
}

View File

@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\Repository\ComposerRepository as BaseComposerRepository;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class ComposerRepository extends BaseComposerRepository
{
private $providerFiles;
protected function loadProviderListings($data)
{
if (null !== $this->providerFiles) {
parent::loadProviderListings($data);
return;
}
$data = [$data];
while ($data) {
$this->providerFiles = [];
foreach ($data as $data) {
$this->loadProviderListings($data);
}
$loadingFiles = $this->providerFiles;
$this->providerFiles = null;
$data = [];
$this->rfs->download($loadingFiles, function (...$args) use (&$data) {
$data[] = $this->fetchFile(...$args);
});
}
}
protected function fetchFile($filename, $cacheKey = null, $sha256 = null, $storeLastModifiedTime = false)
{
if (null !== $this->providerFiles) {
$this->providerFiles[] = [$filename, $cacheKey, $sha256, $storeLastModifiedTime];
return [];
}
return parent::fetchFile($filename, $cacheKey, $sha256, $storeLastModifiedTime);
}
}

View File

@ -0,0 +1,97 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\Composer;
use Composer\IO\IOInterface;
use Symfony\Flex\Configurator\AbstractConfigurator;
use Symfony\Flex\Update\RecipeUpdate;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class Configurator
{
private $composer;
private $io;
private $options;
private $configurators;
private $cache;
public function __construct(Composer $composer, IOInterface $io, Options $options)
{
$this->composer = $composer;
$this->io = $io;
$this->options = $options;
// ordered list of configurators
$this->configurators = [
'bundles' => Configurator\BundlesConfigurator::class,
'copy-from-recipe' => Configurator\CopyFromRecipeConfigurator::class,
'copy-from-package' => Configurator\CopyFromPackageConfigurator::class,
'env' => Configurator\EnvConfigurator::class,
'container' => Configurator\ContainerConfigurator::class,
'makefile' => Configurator\MakefileConfigurator::class,
'composer-scripts' => Configurator\ComposerScriptsConfigurator::class,
'gitignore' => Configurator\GitignoreConfigurator::class,
'dockerfile' => Configurator\DockerfileConfigurator::class,
'docker-compose' => Configurator\DockerComposeConfigurator::class,
];
}
public function install(Recipe $recipe, Lock $lock, array $options = [])
{
$manifest = $recipe->getManifest();
foreach (array_keys($this->configurators) as $key) {
if (isset($manifest[$key])) {
$this->get($key)->configure($recipe, $manifest[$key], $lock, $options);
}
}
}
public function populateUpdate(RecipeUpdate $recipeUpdate): void
{
$originalManifest = $recipeUpdate->getOriginalRecipe()->getManifest();
$newManifest = $recipeUpdate->getNewRecipe()->getManifest();
foreach (array_keys($this->configurators) as $key) {
if (!isset($originalManifest[$key]) && !isset($newManifest[$key])) {
continue;
}
$this->get($key)->update($recipeUpdate, $originalManifest[$key] ?? [], $newManifest[$key] ?? []);
}
}
public function unconfigure(Recipe $recipe, Lock $lock)
{
$manifest = $recipe->getManifest();
foreach (array_keys($this->configurators) as $key) {
if (isset($manifest[$key])) {
$this->get($key)->unconfigure($recipe, $manifest[$key], $lock);
}
}
}
private function get($key): AbstractConfigurator
{
if (!isset($this->configurators[$key])) {
throw new \InvalidArgumentException(sprintf('Unknown configurator "%s".', $key));
}
if (isset($this->cache[$key])) {
return $this->cache[$key];
}
$class = $this->configurators[$key];
return $this->cache[$key] = new $class($this->composer, $this->io, $this->options);
}
}

View File

@ -0,0 +1,131 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Configurator;
use Composer\Composer;
use Composer\IO\IOInterface;
use Symfony\Flex\Lock;
use Symfony\Flex\Options;
use Symfony\Flex\Path;
use Symfony\Flex\Recipe;
use Symfony\Flex\Update\RecipeUpdate;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
abstract class AbstractConfigurator
{
protected $composer;
protected $io;
protected $options;
protected $path;
public function __construct(Composer $composer, IOInterface $io, Options $options)
{
$this->composer = $composer;
$this->io = $io;
$this->options = $options;
$this->path = new Path($options->get('root-dir'));
}
abstract public function configure(Recipe $recipe, $config, Lock $lock, array $options = []);
abstract public function unconfigure(Recipe $recipe, $config, Lock $lock);
abstract public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void;
protected function write($messages)
{
if (!\is_array($messages)) {
$messages = [$messages];
}
foreach ($messages as $i => $message) {
$messages[$i] = ' '.$message;
}
$this->io->writeError($messages, true, IOInterface::VERBOSE);
}
protected function isFileMarked(Recipe $recipe, string $file): bool
{
return is_file($file) && false !== strpos(file_get_contents($file), sprintf('###> %s ###', $recipe->getName()));
}
protected function markData(Recipe $recipe, string $data): string
{
return "\n".sprintf('###> %s ###%s%s%s###< %s ###%s', $recipe->getName(), "\n", rtrim($data, "\r\n"), "\n", $recipe->getName(), "\n");
}
protected function isFileXmlMarked(Recipe $recipe, string $file): bool
{
return is_file($file) && false !== strpos(file_get_contents($file), sprintf('###+ %s ###', $recipe->getName()));
}
protected function markXmlData(Recipe $recipe, string $data): string
{
return "\n".sprintf(' <!-- ###+ %s ### -->%s%s%s <!-- ###- %s ### -->%s', $recipe->getName(), "\n", rtrim($data, "\r\n"), "\n", $recipe->getName(), "\n");
}
/**
* @return bool True if section was found and replaced
*/
protected function updateData(string $file, string $data): bool
{
if (!file_exists($file)) {
return false;
}
$contents = file_get_contents($file);
$newContents = $this->updateDataString($contents, $data);
if (null === $newContents) {
return false;
}
file_put_contents($file, $newContents);
return true;
}
/**
* @return string|null returns the updated content if the section was found, null if not found
*/
protected function updateDataString(string $contents, string $data): ?string
{
$pieces = explode("\n", trim($data));
$startMark = trim(reset($pieces));
$endMark = trim(end($pieces));
if (false === strpos($contents, $startMark) || false === strpos($contents, $endMark)) {
return null;
}
$pattern = '/'.preg_quote($startMark, '/').'.*?'.preg_quote($endMark, '/').'/s';
return preg_replace($pattern, trim($data), $contents);
}
protected function extractSection(Recipe $recipe, string $contents): ?string
{
$section = $this->markData($recipe, '----');
$pieces = explode("\n", trim($section));
$startMark = trim(reset($pieces));
$endMark = trim(end($pieces));
$pattern = '/'.preg_quote($startMark, '/').'.*?'.preg_quote($endMark, '/').'/s';
$matches = [];
preg_match($pattern, $contents, $matches);
return $matches[0] ?? null;
}
}

View File

@ -0,0 +1,143 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Configurator;
use Symfony\Flex\Lock;
use Symfony\Flex\Recipe;
use Symfony\Flex\Update\RecipeUpdate;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class BundlesConfigurator extends AbstractConfigurator
{
public function configure(Recipe $recipe, $bundles, Lock $lock, array $options = [])
{
$this->write('Enabling the package as a Symfony bundle');
$registered = $this->configureBundles($bundles);
$this->dump($this->getConfFile(), $registered);
}
public function unconfigure(Recipe $recipe, $bundles, Lock $lock)
{
$this->write('Disabling the Symfony bundle');
$file = $this->getConfFile();
if (!file_exists($file)) {
return;
}
$registered = $this->load($file);
foreach (array_keys($this->prepareBundles($bundles)) as $class) {
unset($registered[$class]);
}
$this->dump($file, $registered);
}
public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void
{
$originalBundles = $this->configureBundles($originalConfig);
$recipeUpdate->setOriginalFile(
$this->getLocalConfFile(),
$this->buildContents($originalBundles)
);
$newBundles = $this->configureBundles($newConfig);
$recipeUpdate->setNewFile(
$this->getLocalConfFile(),
$this->buildContents($newBundles)
);
}
private function configureBundles(array $bundles): array
{
$file = $this->getConfFile();
$registered = $this->load($file);
$classes = $this->prepareBundles($bundles);
if (isset($classes[$fwb = 'Symfony\Bundle\FrameworkBundle\FrameworkBundle'])) {
foreach ($classes[$fwb] as $env) {
$registered[$fwb][$env] = true;
}
unset($classes[$fwb]);
}
foreach ($classes as $class => $envs) {
// if the class already existed, clear so we can update the envs
if (isset($registered[$class])) {
$registered[$class] = [];
}
foreach ($envs as $env) {
$registered[$class][$env] = true;
}
}
return $registered;
}
private function prepareBundles(array $bundles): array
{
foreach ($bundles as $class => $envs) {
$bundles[ltrim($class, '\\')] = $envs;
}
return $bundles;
}
private function load(string $file): array
{
$bundles = file_exists($file) ? (require $file) : [];
if (!\is_array($bundles)) {
$bundles = [];
}
return $bundles;
}
private function dump(string $file, array $bundles)
{
$contents = $this->buildContents($bundles);
if (!is_dir(\dirname($file))) {
mkdir(\dirname($file), 0777, true);
}
file_put_contents($file, $contents);
if (\function_exists('opcache_invalidate')) {
opcache_invalidate($file);
}
}
private function buildContents(array $bundles): string
{
$contents = "<?php\n\nreturn [\n";
foreach ($bundles as $class => $envs) {
$contents .= " $class::class => [";
foreach ($envs as $env => $value) {
$booleanValue = var_export($value, true);
$contents .= "'$env' => $booleanValue, ";
}
$contents = substr($contents, 0, -2)."],\n";
}
$contents .= "];\n";
return $contents;
}
private function getConfFile(): string
{
return $this->options->get('root-dir').'/'.$this->getLocalConfFile();
}
private function getLocalConfFile(): string
{
return $this->options->expandTargetDir('%CONFIG_DIR%/bundles.php');
}
}

View File

@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Configurator;
use Composer\Factory;
use Composer\Json\JsonFile;
use Composer\Json\JsonManipulator;
use Symfony\Flex\Lock;
use Symfony\Flex\Recipe;
use Symfony\Flex\Update\RecipeUpdate;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class ComposerScriptsConfigurator extends AbstractConfigurator
{
public function configure(Recipe $recipe, $scripts, Lock $lock, array $options = [])
{
$json = new JsonFile(Factory::getComposerFile());
file_put_contents($json->getPath(), $this->configureScripts($scripts, $json));
}
public function unconfigure(Recipe $recipe, $scripts, Lock $lock)
{
$json = new JsonFile(Factory::getComposerFile());
$jsonContents = $json->read();
$autoScripts = $jsonContents['scripts']['auto-scripts'] ?? [];
foreach (array_keys($scripts) as $cmd) {
unset($autoScripts[$cmd]);
}
$manipulator = new JsonManipulator(file_get_contents($json->getPath()));
$manipulator->addSubNode('scripts', 'auto-scripts', $autoScripts);
file_put_contents($json->getPath(), $manipulator->getContents());
}
public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void
{
$json = new JsonFile(Factory::getComposerFile());
$jsonPath = ltrim(str_replace($recipeUpdate->getRootDir(), '', $json->getPath()), '/\\');
$recipeUpdate->setOriginalFile(
$jsonPath,
$this->configureScripts($originalConfig, $json)
);
$recipeUpdate->setNewFile(
$jsonPath,
$this->configureScripts($newConfig, $json)
);
}
private function configureScripts(array $scripts, JsonFile $json): string
{
$jsonContents = $json->read();
$autoScripts = $jsonContents['scripts']['auto-scripts'] ?? [];
$autoScripts = array_merge($autoScripts, $scripts);
$manipulator = new JsonManipulator(file_get_contents($json->getPath()));
$manipulator->addSubNode('scripts', 'auto-scripts', $autoScripts);
return $manipulator->getContents();
}
}

View File

@ -0,0 +1,150 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Configurator;
use Symfony\Flex\Lock;
use Symfony\Flex\Recipe;
use Symfony\Flex\Update\RecipeUpdate;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class ContainerConfigurator extends AbstractConfigurator
{
public function configure(Recipe $recipe, $parameters, Lock $lock, array $options = [])
{
$this->write('Setting parameters');
$contents = $this->configureParameters($parameters);
if (null !== $contents) {
file_put_contents($this->options->get('root-dir').'/'.$this->getServicesPath(), $contents);
}
}
public function unconfigure(Recipe $recipe, $parameters, Lock $lock)
{
$this->write('Unsetting parameters');
$target = $this->options->get('root-dir').'/'.$this->getServicesPath();
$lines = [];
foreach (file($target) as $line) {
if ($this->removeParameters(1, $parameters, $line)) {
continue;
}
$lines[] = $line;
}
file_put_contents($target, implode('', $lines));
}
public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void
{
if ($originalConfig) {
$recipeUpdate->setOriginalFile(
$this->getServicesPath(),
$this->configureParameters($originalConfig, true)
);
}
if ($newConfig) {
$recipeUpdate->setNewFile(
$this->getServicesPath(),
$this->configureParameters($newConfig, true)
);
}
}
private function configureParameters(array $parameters, bool $update = false): string
{
$target = $this->options->get('root-dir').'/'.$this->getServicesPath();
$endAt = 0;
$isParameters = false;
$lines = [];
foreach (file($target) as $i => $line) {
$lines[] = $line;
if (!$isParameters && !preg_match('/^parameters:/', $line)) {
continue;
}
if (!$isParameters) {
$isParameters = true;
continue;
}
if (!preg_match('/^\s+.*/', $line) && '' !== trim($line)) {
$endAt = $i - 1;
$isParameters = false;
continue;
}
foreach ($parameters as $key => $value) {
$matches = [];
if (preg_match(sprintf('/^\s+%s\:/', preg_quote($key, '/')), $line, $matches)) {
if ($update) {
$lines[$i] = substr($line, 0, \strlen($matches[0])).' '.str_replace("'", "''", $value)."\n";
}
unset($parameters[$key]);
}
}
}
if ($parameters) {
$parametersLines = [];
if (!$endAt) {
$parametersLines[] = "parameters:\n";
}
foreach ($parameters as $key => $value) {
if (\is_array($value)) {
$parametersLines[] = sprintf(" %s:\n%s", $key, $this->dumpYaml(2, $value));
continue;
}
$parametersLines[] = sprintf(" %s: '%s'%s", $key, str_replace("'", "''", $value), "\n");
}
if (!$endAt) {
$parametersLines[] = "\n";
}
array_splice($lines, $endAt, 0, $parametersLines);
}
return implode('', $lines);
}
private function removeParameters($level, $params, $line)
{
foreach ($params as $key => $value) {
if (\is_array($value) && $this->removeParameters($level + 1, $value, $line)) {
return true;
}
if (preg_match(sprintf('/^(\s{%d}|\t{%d})+%s\:/', 4 * $level, $level, preg_quote($key, '/')), $line)) {
return true;
}
}
return false;
}
private function dumpYaml($level, $array): string
{
$line = '';
foreach ($array as $key => $value) {
$line .= str_repeat(' ', $level);
if (!\is_array($value)) {
$line .= sprintf("%s: '%s'\n", $key, str_replace("'", "''", $value));
continue;
}
$line .= sprintf("%s:\n", $key).$this->dumpYaml($level + 1, $value);
}
return $line;
}
private function getServicesPath(): string
{
return $this->options->expandTargetDir('%CONFIG_DIR%/services.yaml');
}
}

View File

@ -0,0 +1,169 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Configurator;
use Symfony\Flex\Lock;
use Symfony\Flex\Recipe;
use Symfony\Flex\Update\RecipeUpdate;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class CopyFromPackageConfigurator extends AbstractConfigurator
{
public function configure(Recipe $recipe, $config, Lock $lock, array $options = [])
{
$this->write('Copying files from package');
$packageDir = $this->composer->getInstallationManager()->getInstallPath($recipe->getPackage());
$options = array_merge($this->options->toArray(), $options);
$files = $this->getFilesToCopy($config, $packageDir);
foreach ($files as $source => $target) {
$this->copyFile($source, $target, $options);
}
}
public function unconfigure(Recipe $recipe, $config, Lock $lock)
{
$this->write('Removing files from package');
$packageDir = $this->composer->getInstallationManager()->getInstallPath($recipe->getPackage());
$this->removeFiles($config, $packageDir, $this->options->get('root-dir'));
}
public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void
{
$packageDir = $this->composer->getInstallationManager()->getInstallPath($recipeUpdate->getNewRecipe()->getPackage());
foreach ($originalConfig as $source => $target) {
if (isset($newConfig[$source])) {
// path is in both, we cannot update
$recipeUpdate->addCopyFromPackagePath(
$packageDir.'/'.$source,
$this->options->expandTargetDir($target)
);
unset($newConfig[$source]);
}
// if any paths were removed from the recipe, we'll keep them
}
// any remaining files are new, and we can copy them
foreach ($this->getFilesToCopy($newConfig, $packageDir) as $source => $target) {
if (!file_exists($source)) {
throw new \LogicException(sprintf('File "%s" does not exist!', $source));
}
$recipeUpdate->setNewFile($target, file_get_contents($source));
}
}
private function getFilesToCopy(array $manifest, string $from): array
{
$files = [];
foreach ($manifest as $source => $target) {
$target = $this->options->expandTargetDir($target);
if ('/' === substr($source, -1)) {
$files = array_merge($files, $this->getFilesForDir($this->path->concatenate([$from, $source]), $this->path->concatenate([$target])));
continue;
}
$files[$this->path->concatenate([$from, $source])] = $target;
}
return $files;
}
private function removeFiles(array $manifest, string $from, string $to)
{
foreach ($manifest as $source => $target) {
$target = $this->options->expandTargetDir($target);
if ('/' === substr($source, -1)) {
$this->removeFilesFromDir($this->path->concatenate([$from, $source]), $this->path->concatenate([$to, $target]));
} else {
$targetPath = $this->path->concatenate([$to, $target]);
if (file_exists($targetPath)) {
@unlink($targetPath);
$this->write(sprintf(' Removed <fg=green>"%s"</>', $this->path->relativize($targetPath)));
}
}
}
}
private function getFilesForDir(string $source, string $target): array
{
$iterator = $this->createSourceIterator($source, \RecursiveIteratorIterator::SELF_FIRST);
$files = [];
foreach ($iterator as $item) {
$targetPath = $this->path->concatenate([$target, $iterator->getSubPathName()]);
$files[(string) $item] = $targetPath;
}
return $files;
}
/**
* @param string $source The absolute path to the source file
* @param string $target The relative (to root dir) path to the target
*/
public function copyFile(string $source, string $target, array $options)
{
$target = $this->options->get('root-dir').'/'.$target;
if (is_dir($source)) {
// directory will be created when a file is copied to it
return;
}
$overwrite = $options['force'] ?? false;
if (!$this->options->shouldWriteFile($target, $overwrite)) {
return;
}
if (!file_exists($source)) {
throw new \LogicException(sprintf('File "%s" does not exist!', $source));
}
if (!file_exists(\dirname($target))) {
mkdir(\dirname($target), 0777, true);
$this->write(sprintf(' Created <fg=green>"%s"</>', $this->path->relativize(\dirname($target))));
}
file_put_contents($target, $this->options->expandTargetDir(file_get_contents($source)));
@chmod($target, fileperms($target) | (fileperms($source) & 0111));
$this->write(sprintf(' Created <fg=green>"%s"</>', $this->path->relativize($target)));
}
private function removeFilesFromDir(string $source, string $target)
{
if (!is_dir($source)) {
return;
}
$iterator = $this->createSourceIterator($source, \RecursiveIteratorIterator::CHILD_FIRST);
foreach ($iterator as $item) {
$targetPath = $this->path->concatenate([$target, $iterator->getSubPathName()]);
if ($item->isDir()) {
// that removes the dir only if it is empty
@rmdir($targetPath);
$this->write(sprintf(' Removed directory <fg=green>"%s"</>', $this->path->relativize($targetPath)));
} else {
@unlink($targetPath);
$this->write(sprintf(' Removed <fg=green>"%s"</>', $this->path->relativize($targetPath)));
}
}
}
private function createSourceIterator(string $source, int $mode): \RecursiveIteratorIterator
{
return new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), $mode);
}
}

View File

@ -0,0 +1,175 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Configurator;
use Symfony\Flex\Lock;
use Symfony\Flex\Recipe;
use Symfony\Flex\Update\RecipeUpdate;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class CopyFromRecipeConfigurator extends AbstractConfigurator
{
public function configure(Recipe $recipe, $config, Lock $lock, array $options = [])
{
$this->write('Copying files from recipe');
$options = array_merge($this->options->toArray(), $options);
$lock->add($recipe->getName(), ['files' => $this->copyFiles($config, $recipe->getFiles(), $options)]);
}
public function unconfigure(Recipe $recipe, $config, Lock $lock)
{
$this->write('Removing files from recipe');
$this->removeFiles($config, $this->getRemovableFilesFromRecipeAndLock($recipe, $lock), $this->options->get('root-dir'));
}
public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void
{
foreach ($recipeUpdate->getOriginalRecipe()->getFiles() as $filename => $data) {
$recipeUpdate->setOriginalFile($filename, $data['contents']);
}
$files = [];
foreach ($recipeUpdate->getNewRecipe()->getFiles() as $filename => $data) {
$recipeUpdate->setNewFile($filename, $data['contents']);
$files[] = $this->getLocalFilePath($recipeUpdate->getRootDir(), $filename);
}
$recipeUpdate->getLock()->add($recipeUpdate->getPackageName(), ['files' => $files]);
}
private function getRemovableFilesFromRecipeAndLock(Recipe $recipe, Lock $lock): array
{
$lockedFiles = array_unique(
array_reduce(
array_column($lock->all(), 'files'),
function (array $carry, array $package) {
return array_merge($carry, $package);
},
[]
)
);
$removableFiles = $recipe->getFiles();
$lockedFiles = array_map('realpath', $lockedFiles);
// Compare file paths by their real path to abstract OS differences
foreach (array_keys($removableFiles) as $file) {
if (\in_array(realpath($file), $lockedFiles)) {
unset($removableFiles[$file]);
}
}
return $removableFiles;
}
private function copyFiles(array $manifest, array $files, array $options): array
{
$copiedFiles = [];
$to = $options['root-dir'] ?? '.';
foreach ($manifest as $source => $target) {
$target = $this->options->expandTargetDir($target);
if ('/' === substr($source, -1)) {
$copiedFiles = array_merge(
$copiedFiles,
$this->copyDir($source, $this->path->concatenate([$to, $target]), $files, $options)
);
} else {
$copiedFiles[] = $this->copyFile($this->path->concatenate([$to, $target]), $files[$source]['contents'], $files[$source]['executable'], $options);
}
}
return $copiedFiles;
}
private function copyDir(string $source, string $target, array $files, array $options): array
{
$copiedFiles = [];
foreach ($files as $file => $data) {
if (0 === strpos($file, $source)) {
$file = $this->path->concatenate([$target, substr($file, \strlen($source))]);
$copiedFiles[] = $this->copyFile($file, $data['contents'], $data['executable'], $options);
}
}
return $copiedFiles;
}
private function copyFile(string $to, string $contents, bool $executable, array $options): string
{
$overwrite = $options['force'] ?? false;
$basePath = $options['root-dir'] ?? '.';
$copiedFile = $this->getLocalFilePath($basePath, $to);
if (!$this->options->shouldWriteFile($to, $overwrite)) {
return $copiedFile;
}
if (!is_dir(\dirname($to))) {
mkdir(\dirname($to), 0777, true);
}
file_put_contents($to, $this->options->expandTargetDir($contents));
if ($executable) {
@chmod($to, fileperms($to) | 0111);
}
$this->write(sprintf(' Created <fg=green>"%s"</>', $this->path->relativize($to)));
return $copiedFile;
}
private function removeFiles(array $manifest, array $files, string $to)
{
foreach ($manifest as $source => $target) {
$target = $this->options->expandTargetDir($target);
if ('.git' === $target) {
// never remove the main Git directory, even if it was created by a recipe
continue;
}
if ('/' === substr($source, -1)) {
foreach (array_keys($files) as $file) {
if (0 === strpos($file, $source)) {
$this->removeFile($this->path->concatenate([$to, $target, substr($file, \strlen($source))]));
}
}
} else {
$this->removeFile($this->path->concatenate([$to, $target]));
}
}
}
private function removeFile(string $to)
{
if (!file_exists($to)) {
return;
}
@unlink($to);
$this->write(sprintf(' Removed <fg=green>"%s"</>', $this->path->relativize($to)));
if (0 === \count(glob(\dirname($to).'/*', \GLOB_NOSORT))) {
@rmdir(\dirname($to));
}
}
private function getLocalFilePath(string $basePath, $destination): string
{
return str_replace($basePath.\DIRECTORY_SEPARATOR, '', $destination);
}
}

View File

@ -0,0 +1,384 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Configurator;
use Composer\Composer;
use Composer\Factory;
use Composer\IO\IOInterface;
use Composer\Json\JsonFile;
use Composer\Json\JsonManipulator;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Flex\Lock;
use Symfony\Flex\Options;
use Symfony\Flex\Recipe;
use Symfony\Flex\Update\RecipeUpdate;
/**
* Adds services and volumes to docker-compose.yml file.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class DockerComposeConfigurator extends AbstractConfigurator
{
private $filesystem;
public static $configureDockerRecipes = null;
public function __construct(Composer $composer, IOInterface $io, Options $options)
{
parent::__construct($composer, $io, $options);
$this->filesystem = new Filesystem();
}
public function configure(Recipe $recipe, $config, Lock $lock, array $options = [])
{
if (!self::shouldConfigureDockerRecipe($this->composer, $this->io, $recipe)) {
return;
}
$this->configureDockerCompose($recipe, $config, $options['force'] ?? false);
$this->write('Docker Compose definitions have been modified. Please run "docker-compose up --build" again to apply the changes.');
}
public function unconfigure(Recipe $recipe, $config, Lock $lock)
{
$rootDir = $this->options->get('root-dir');
foreach ($this->normalizeConfig($config) as $file => $extra) {
if (null === $dockerComposeFile = $this->findDockerComposeFile($rootDir, $file)) {
continue;
}
$name = $recipe->getName();
// Remove recipe and add break line
$contents = preg_replace(sprintf('{%s+###> %s ###.*?###< %s ###%s+}s', "\n", $name, $name, "\n"), \PHP_EOL.\PHP_EOL, file_get_contents($dockerComposeFile), -1, $count);
if (!$count) {
return;
}
foreach ($extra as $key => $value) {
if (0 === preg_match(sprintf('{^%s:[ \t\r\n]*([ \t]+\w|#)}m', $key), $contents, $matches)) {
$contents = preg_replace(sprintf('{\n?^%s:[ \t\r\n]*}sm', $key), '', $contents, -1, $count);
}
}
$this->write(sprintf('Removing Docker Compose entries from "%s"', $dockerComposeFile));
file_put_contents($dockerComposeFile, ltrim($contents, "\n"));
}
$this->write('Docker Compose definitions have been modified. Please run "docker-compose up" again to apply the changes.');
}
public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void
{
if (!self::shouldConfigureDockerRecipe($this->composer, $this->io, $recipeUpdate->getNewRecipe())) {
return;
}
$recipeUpdate->addOriginalFiles(
$this->getContentsAfterApplyingRecipe($recipeUpdate->getRootDir(), $recipeUpdate->getOriginalRecipe(), $originalConfig)
);
$recipeUpdate->addNewFiles(
$this->getContentsAfterApplyingRecipe($recipeUpdate->getRootDir(), $recipeUpdate->getNewRecipe(), $newConfig)
);
}
public static function shouldConfigureDockerRecipe(Composer $composer, IOInterface $io, Recipe $recipe): bool
{
if (null !== self::$configureDockerRecipes) {
return self::$configureDockerRecipes;
}
if (null !== $dockerPreference = $composer->getPackage()->getExtra()['symfony']['docker'] ?? null) {
self::$configureDockerRecipes = $dockerPreference;
return self::$configureDockerRecipes;
}
if ('install' !== $recipe->getJob()) {
// default to not configuring
return false;
}
if (!isset($_SERVER['SYMFONY_DOCKER'])) {
$answer = self::askDockerSupport($io, $recipe);
} elseif (filter_var($_SERVER['SYMFONY_DOCKER'], \FILTER_VALIDATE_BOOLEAN)) {
$answer = 'p';
} else {
$answer = 'x';
}
if ('n' === $answer) {
self::$configureDockerRecipes = false;
return self::$configureDockerRecipes;
}
if ('y' === $answer) {
self::$configureDockerRecipes = true;
return self::$configureDockerRecipes;
}
// yes or no permanently
self::$configureDockerRecipes = 'p' === $answer;
$json = new JsonFile(Factory::getComposerFile());
$manipulator = new JsonManipulator(file_get_contents($json->getPath()));
$manipulator->addSubNode('extra', 'symfony.docker', self::$configureDockerRecipes);
file_put_contents($json->getPath(), $manipulator->getContents());
return self::$configureDockerRecipes;
}
/**
* Normalizes the config and return the name of the main Docker Compose file if applicable.
*/
private function normalizeConfig(array $config): array
{
foreach ($config as $val) {
// Support for the short syntax recipe syntax that modifies docker-compose.yml only
return isset($val[0]) ? ['docker-compose.yml' => $config] : $config;
}
return $config;
}
/**
* Finds the Docker Compose file according to these rules: https://docs.docker.com/compose/reference/envvars/#compose_file.
*/
private function findDockerComposeFile(string $rootDir, string $file): ?string
{
if (isset($_SERVER['COMPOSE_FILE'])) {
$separator = $_SERVER['COMPOSE_PATH_SEPARATOR'] ?? ('\\' === \DIRECTORY_SEPARATOR ? ';' : ':');
$files = explode($separator, $_SERVER['COMPOSE_FILE']);
foreach ($files as $f) {
if ($file !== basename($f)) {
continue;
}
if (!$this->filesystem->isAbsolutePath($f)) {
$f = realpath(sprintf('%s/%s', $rootDir, $f));
}
if ($this->filesystem->exists($f)) {
return $f;
}
}
}
// COMPOSE_FILE not set, or doesn't contain the file we're looking for
$dir = $rootDir;
do {
// Test with the ".yaml" extension if the file doesn't end up with ".yml".
if (
$this->filesystem->exists($dockerComposeFile = sprintf('%s/%s', $dir, $file)) ||
$this->filesystem->exists($dockerComposeFile = substr($dockerComposeFile, 0, -2).'aml')
) {
return $dockerComposeFile;
}
$previousDir = $dir;
$dir = \dirname($dir);
} while ($dir !== $previousDir);
return null;
}
private function parse($level, $indent, $services): string
{
$line = '';
foreach ($services as $key => $value) {
$line .= str_repeat(' ', $indent * $level);
if (!\is_array($value)) {
if (\is_string($key)) {
$line .= sprintf('%s:', $key);
}
$line .= sprintf("%s\n", $value);
continue;
}
$line .= sprintf("%s:\n", $key).$this->parse($level + 1, $indent, $value);
}
return $line;
}
private function configureDockerCompose(Recipe $recipe, array $config, bool $update): void
{
$rootDir = $this->options->get('root-dir');
foreach ($this->normalizeConfig($config) as $file => $extra) {
$dockerComposeFile = $this->findDockerComposeFile($rootDir, $file);
if (null === $dockerComposeFile) {
$dockerComposeFile = $rootDir.'/'.$file;
file_put_contents($dockerComposeFile, "version: '3'\n");
$this->write(sprintf(' Created <fg=green>"%s"</>', $file));
}
if (!$update && $this->isFileMarked($recipe, $dockerComposeFile)) {
continue;
}
$this->write(sprintf('Adding Docker Compose definitions to "%s"', $dockerComposeFile));
$offset = 2;
$node = null;
$endAt = [];
$startAt = [];
$lines = [];
$nodesLines = [];
foreach (file($dockerComposeFile) as $i => $line) {
$lines[] = $line;
$ltrimedLine = ltrim($line, ' ');
if (null !== $node) {
$nodesLines[$node][$i] = $line;
}
// Skip blank lines and comments
if (('' !== $ltrimedLine && 0 === strpos($ltrimedLine, '#')) || '' === trim($line)) {
continue;
}
// Extract Docker Compose keys (usually "services" and "volumes")
if (!preg_match('/^[\'"]?([a-zA-Z0-9]+)[\'"]?:\s*$/', $line, $matches)) {
// Detect indentation to use
$offestLine = \strlen($line) - \strlen($ltrimedLine);
if ($offset > $offestLine && 0 !== $offestLine) {
$offset = $offestLine;
}
continue;
}
// Keep end in memory (check break line on previous line)
$endAt[$node] = '' !== trim($lines[$i - 1]) ? $i : $i - 1;
$node = $matches[1];
if (!isset($nodesLines[$node])) {
$nodesLines[$node] = [];
}
if (!isset($startAt[$node])) {
// the section contents starts at the next line
$startAt[$node] = $i + 1;
}
}
$endAt[$node] = \count($lines) + 1;
foreach ($extra as $key => $value) {
if (isset($endAt[$key])) {
$data = $this->markData($recipe, $this->parse(1, $offset, $value));
$updatedContents = $this->updateDataString(implode('', $nodesLines[$key]), $data);
if (null === $updatedContents) {
// not an update: just add to section
array_splice($lines, $endAt[$key], 0, $data);
continue;
}
$originalEndAt = $endAt[$key];
$length = $endAt[$key] - $startAt[$key];
array_splice($lines, $startAt[$key], $length, ltrim($updatedContents, "\n"));
// reset any start/end positions after this to the new positions
foreach ($startAt as $sectionKey => $at) {
if ($at > $originalEndAt) {
$startAt[$sectionKey] = $at - $length - 1;
}
}
foreach ($endAt as $sectionKey => $at) {
if ($at > $originalEndAt) {
$endAt[$sectionKey] = $at - $length;
}
}
continue;
}
$lines[] = sprintf("\n%s:", $key);
$lines[] = $this->markData($recipe, $this->parse(1, $offset, $value));
}
file_put_contents($dockerComposeFile, implode('', $lines));
}
}
private function getContentsAfterApplyingRecipe(string $rootDir, Recipe $recipe, array $config): array
{
if (0 === \count($config)) {
return [];
}
$files = array_map(function ($file) use ($rootDir) {
return $this->findDockerComposeFile($rootDir, $file);
}, array_keys($config));
$originalContents = [];
foreach ($files as $file) {
$originalContents[$file] = file_exists($file) ? file_get_contents($file) : null;
}
$this->configureDockerCompose(
$recipe,
$config,
true
);
$updatedContents = [];
foreach ($files as $file) {
$localPath = ltrim(str_replace($rootDir, '', $file), '/\\');
$updatedContents[$localPath] = file_exists($file) ? file_get_contents($file) : null;
}
foreach ($originalContents as $file => $contents) {
if (null === $contents) {
if (file_exists($file)) {
unlink($file);
}
} else {
file_put_contents($file, $contents);
}
}
return $updatedContents;
}
private static function askDockerSupport(IOInterface $io, Recipe $recipe): string
{
$warning = $io->isInteractive() ? 'WARNING' : 'IGNORING';
$io->writeError(sprintf(' - <warning> %s </> %s', $warning, $recipe->getFormattedOrigin()));
$question = ' The recipe for this package contains some Docker configuration.
This may create/update <comment>docker-compose.yml</comment> or update <comment>Dockerfile</comment> (if it exists).
Do you want to include Docker configuration from recipes?
[<comment>y</>] Yes
[<comment>n</>] No
[<comment>p</>] Yes permanently, never ask again for this project
[<comment>x</>] No permanently, never ask again for this project
(defaults to <comment>y</>): ';
return $io->askAndValidate(
$question,
function ($value) {
if (null === $value) {
return 'y';
}
$value = strtolower($value[0]);
if (!\in_array($value, ['y', 'n', 'p', 'x'], true)) {
throw new \InvalidArgumentException('Invalid choice.');
}
return $value;
},
null,
'y'
);
}
}

View File

@ -0,0 +1,125 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Configurator;
use Symfony\Flex\Lock;
use Symfony\Flex\Recipe;
use Symfony\Flex\Update\RecipeUpdate;
/**
* Adds commands to a Dockerfile.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class DockerfileConfigurator extends AbstractConfigurator
{
public function configure(Recipe $recipe, $config, Lock $lock, array $options = [])
{
if (!DockerComposeConfigurator::shouldConfigureDockerRecipe($this->composer, $this->io, $recipe)) {
return;
}
$this->configureDockerfile($recipe, $config, $options['force'] ?? false);
}
public function unconfigure(Recipe $recipe, $config, Lock $lock)
{
if (!file_exists($dockerfile = $this->options->get('root-dir').'/Dockerfile')) {
return;
}
$name = $recipe->getName();
$contents = preg_replace(sprintf('{%s+###> %s ###.*?###< %s ###%s+}s', "\n", $name, $name, "\n"), "\n", file_get_contents($dockerfile), -1, $count);
if (!$count) {
return;
}
$this->write('Removing Dockerfile entries');
file_put_contents($dockerfile, ltrim($contents, "\n"));
}
public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void
{
if (!DockerComposeConfigurator::shouldConfigureDockerRecipe($this->composer, $this->io, $recipeUpdate->getNewRecipe())) {
return;
}
$recipeUpdate->setOriginalFile(
'Dockerfile',
$this->getContentsAfterApplyingRecipe($recipeUpdate->getOriginalRecipe(), $originalConfig)
);
$recipeUpdate->setNewFile(
'Dockerfile',
$this->getContentsAfterApplyingRecipe($recipeUpdate->getNewRecipe(), $newConfig)
);
}
private function configureDockerfile(Recipe $recipe, array $config, bool $update, bool $writeOutput = true): void
{
$dockerfile = $this->options->get('root-dir').'/Dockerfile';
if (!file_exists($dockerfile) || (!$update && $this->isFileMarked($recipe, $dockerfile))) {
return;
}
if ($writeOutput) {
$this->write('Adding Dockerfile entries');
}
$data = ltrim($this->markData($recipe, implode("\n", $config)), "\n");
if ($this->updateData($dockerfile, $data)) {
// done! Existing spot updated
return;
}
$lines = [];
foreach (file($dockerfile) as $line) {
$lines[] = $line;
if (!preg_match('/^###> recipes ###$/', $line)) {
continue;
}
$lines[] = $data;
}
file_put_contents($dockerfile, implode('', $lines));
}
private function getContentsAfterApplyingRecipe(Recipe $recipe, array $config): ?string
{
if (0 === \count($config)) {
return null;
}
$dockerfile = $this->options->get('root-dir').'/Dockerfile';
$originalContents = file_exists($dockerfile) ? file_get_contents($dockerfile) : null;
$this->configureDockerfile(
$recipe,
$config,
true,
false
);
$updatedContents = file_exists($dockerfile) ? file_get_contents($dockerfile) : null;
if (null === $originalContents) {
if (file_exists($dockerfile)) {
unlink($dockerfile);
}
} else {
file_put_contents($dockerfile, $originalContents);
}
return $updatedContents;
}
}

View File

@ -0,0 +1,277 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Configurator;
use Symfony\Flex\Lock;
use Symfony\Flex\Recipe;
use Symfony\Flex\Update\RecipeUpdate;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class EnvConfigurator extends AbstractConfigurator
{
public function configure(Recipe $recipe, $vars, Lock $lock, array $options = [])
{
$this->write('Adding environment variable defaults');
$this->configureEnvDist($recipe, $vars, $options['force'] ?? false);
if (!file_exists($this->options->get('root-dir').'/'.($this->options->get('runtime')['dotenv_path'] ?? '.env').'.test')) {
$this->configurePhpUnit($recipe, $vars, $options['force'] ?? false);
}
}
public function unconfigure(Recipe $recipe, $vars, Lock $lock)
{
$this->unconfigureEnvFiles($recipe, $vars);
$this->unconfigurePhpUnit($recipe, $vars);
}
public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void
{
$recipeUpdate->addOriginalFiles(
$this->getContentsAfterApplyingRecipe($recipeUpdate->getRootDir(), $recipeUpdate->getOriginalRecipe(), $originalConfig)
);
$recipeUpdate->addNewFiles(
$this->getContentsAfterApplyingRecipe($recipeUpdate->getRootDir(), $recipeUpdate->getNewRecipe(), $newConfig)
);
}
private function configureEnvDist(Recipe $recipe, $vars, bool $update)
{
$dotenvPath = $this->options->get('runtime')['dotenv_path'] ?? '.env';
foreach ([$dotenvPath.'.dist', $dotenvPath] as $file) {
$env = $this->options->get('root-dir').'/'.$file;
if (!is_file($env)) {
continue;
}
if (!$update && $this->isFileMarked($recipe, $env)) {
continue;
}
$data = '';
foreach ($vars as $key => $value) {
$existingValue = $update ? $this->findExistingValue($key, $env, $recipe) : null;
$value = $this->evaluateValue($value, $existingValue);
if ('#' === $key[0] && is_numeric(substr($key, 1))) {
if ('' === $value) {
$data .= "#\n";
} else {
$data .= '# '.$value."\n";
}
continue;
}
$value = $this->options->expandTargetDir($value);
if (false !== strpbrk($value, " \t\n&!\"")) {
$value = '"'.str_replace(['\\', '"', "\t", "\n"], ['\\\\', '\\"', '\t', '\n'], $value).'"';
}
$data .= "$key=$value\n";
}
$data = $this->markData($recipe, $data);
if (!$this->updateData($env, $data)) {
file_put_contents($env, $data, \FILE_APPEND);
}
}
}
private function configurePhpUnit(Recipe $recipe, $vars, bool $update)
{
foreach (['phpunit.xml.dist', 'phpunit.xml'] as $file) {
$phpunit = $this->options->get('root-dir').'/'.$file;
if (!is_file($phpunit)) {
continue;
}
if (!$update && $this->isFileXmlMarked($recipe, $phpunit)) {
continue;
}
$data = '';
foreach ($vars as $key => $value) {
$value = $this->evaluateValue($value);
if ('#' === $key[0]) {
if (is_numeric(substr($key, 1))) {
$doc = new \DOMDocument();
$data .= ' '.$doc->saveXML($doc->createComment(' '.$value.' '))."\n";
} else {
$value = $this->options->expandTargetDir($value);
$doc = new \DOMDocument();
$fragment = $doc->createElement('env');
$fragment->setAttribute('name', substr($key, 1));
$fragment->setAttribute('value', $value);
$data .= ' '.str_replace(['<', '/>'], ['<!-- ', ' -->'], $doc->saveXML($fragment))."\n";
}
} else {
$value = $this->options->expandTargetDir($value);
$doc = new \DOMDocument();
$fragment = $doc->createElement('env');
$fragment->setAttribute('name', $key);
$fragment->setAttribute('value', $value);
$data .= ' '.$doc->saveXML($fragment)."\n";
}
}
$data = $this->markXmlData($recipe, $data);
if (!$this->updateData($phpunit, $data)) {
file_put_contents($phpunit, preg_replace('{^(\s+</php>)}m', $data.'$1', file_get_contents($phpunit)));
}
}
}
private function unconfigureEnvFiles(Recipe $recipe, $vars)
{
$dotenvPath = $this->options->get('runtime')['dotenv_path'] ?? '.env';
foreach ([$dotenvPath, $dotenvPath.'.dist'] as $file) {
$env = $this->options->get('root-dir').'/'.$file;
if (!file_exists($env)) {
continue;
}
$contents = preg_replace(sprintf('{%s*###> %s ###.*###< %s ###%s+}s', "\n", $recipe->getName(), $recipe->getName(), "\n"), "\n", file_get_contents($env), -1, $count);
if (!$count) {
continue;
}
$this->write(sprintf('Removing environment variables from %s', $file));
file_put_contents($env, $contents);
}
}
private function unconfigurePhpUnit(Recipe $recipe, $vars)
{
foreach (['phpunit.xml.dist', 'phpunit.xml'] as $file) {
$phpunit = $this->options->get('root-dir').'/'.$file;
if (!is_file($phpunit)) {
continue;
}
$contents = preg_replace(sprintf('{%s*\s+<!-- ###\+ %s ### -->.*<!-- ###- %s ### -->%s+}s', "\n", $recipe->getName(), $recipe->getName(), "\n"), "\n", file_get_contents($phpunit), -1, $count);
if (!$count) {
continue;
}
$this->write(sprintf('Removing environment variables from %s', $file));
file_put_contents($phpunit, $contents);
}
}
/**
* Evaluates expressions like %generate(secret)%.
*
* If $originalValue is passed, and the value contains an expression.
* the $originalValue is used.
*/
private function evaluateValue($value, string $originalValue = null)
{
if ('%generate(secret)%' === $value) {
if (null !== $originalValue) {
return $originalValue;
}
return $this->generateRandomBytes();
}
if (preg_match('~^%generate\(secret,\s*([0-9]+)\)%$~', $value, $matches)) {
if (null !== $originalValue) {
return $originalValue;
}
return $this->generateRandomBytes($matches[1]);
}
return $value;
}
private function generateRandomBytes($length = 16)
{
return bin2hex(random_bytes($length));
}
private function getContentsAfterApplyingRecipe(string $rootDir, Recipe $recipe, array $vars): array
{
$dotenvPath = $this->options->get('runtime')['dotenv_path'] ?? '.env';
$files = [$dotenvPath, $dotenvPath.'.dist', 'phpunit.xml.dist', 'phpunit.xml'];
if (0 === \count($vars)) {
return array_fill_keys($files, null);
}
$originalContents = [];
foreach ($files as $file) {
$originalContents[$file] = file_exists($rootDir.'/'.$file) ? file_get_contents($rootDir.'/'.$file) : null;
}
$this->configureEnvDist(
$recipe,
$vars,
true
);
if (!file_exists($rootDir.'/'.$dotenvPath.'.test')) {
$this->configurePhpUnit(
$recipe,
$vars,
true
);
}
$updatedContents = [];
foreach ($files as $file) {
$updatedContents[$file] = file_exists($rootDir.'/'.$file) ? file_get_contents($rootDir.'/'.$file) : null;
}
foreach ($originalContents as $file => $contents) {
if (null === $contents) {
if (file_exists($rootDir.'/'.$file)) {
unlink($rootDir.'/'.$file);
}
} else {
file_put_contents($rootDir.'/'.$file, $contents);
}
}
return $updatedContents;
}
/**
* Attempts to find the existing value of an environment variable.
*/
private function findExistingValue(string $var, string $filename, Recipe $recipe): ?string
{
if (!file_exists($filename)) {
return null;
}
$contents = file_get_contents($filename);
$section = $this->extractSection($recipe, $contents);
if (!$section) {
return null;
}
$lines = explode("\n", $section);
foreach ($lines as $line) {
if (0 !== strpos($line, sprintf('%s=', $var))) {
continue;
}
return trim(substr($line, \strlen($var) + 1));
}
return null;
}
}

View File

@ -0,0 +1,105 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Configurator;
use Symfony\Flex\Lock;
use Symfony\Flex\Recipe;
use Symfony\Flex\Update\RecipeUpdate;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class GitignoreConfigurator extends AbstractConfigurator
{
public function configure(Recipe $recipe, $vars, Lock $lock, array $options = [])
{
$this->write('Adding entries to .gitignore');
$this->configureGitignore($recipe, $vars, $options['force'] ?? false);
}
public function unconfigure(Recipe $recipe, $vars, Lock $lock)
{
$file = $this->options->get('root-dir').'/.gitignore';
if (!file_exists($file)) {
return;
}
$contents = preg_replace(sprintf('{%s*###> %s ###.*###< %s ###%s+}s', "\n", $recipe->getName(), $recipe->getName(), "\n"), "\n", file_get_contents($file), -1, $count);
if (!$count) {
return;
}
$this->write('Removing entries in .gitignore');
file_put_contents($file, ltrim($contents, "\r\n"));
}
public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void
{
$recipeUpdate->setOriginalFile(
'.gitignore',
$this->getContentsAfterApplyingRecipe($recipeUpdate->getRootDir(), $recipeUpdate->getOriginalRecipe(), $originalConfig)
);
$recipeUpdate->setNewFile(
'.gitignore',
$this->getContentsAfterApplyingRecipe($recipeUpdate->getRootDir(), $recipeUpdate->getNewRecipe(), $newConfig)
);
}
private function configureGitignore(Recipe $recipe, array $vars, bool $update)
{
$gitignore = $this->options->get('root-dir').'/.gitignore';
if (!$update && $this->isFileMarked($recipe, $gitignore)) {
return;
}
$data = '';
foreach ($vars as $value) {
$value = $this->options->expandTargetDir($value);
$data .= "$value\n";
}
$data = "\n".ltrim($this->markData($recipe, $data), "\r\n");
if (!$this->updateData($gitignore, $data)) {
file_put_contents($gitignore, $data, \FILE_APPEND);
}
}
private function getContentsAfterApplyingRecipe(string $rootDir, Recipe $recipe, $vars): ?string
{
if (0 === \count($vars)) {
return null;
}
$file = $rootDir.'/.gitignore';
$originalContents = file_exists($file) ? file_get_contents($file) : null;
$this->configureGitignore(
$recipe,
$vars,
true
);
$updatedContents = file_exists($file) ? file_get_contents($file) : null;
if (null === $originalContents) {
if (file_exists($file)) {
unlink($file);
}
} else {
file_put_contents($file, $originalContents);
}
return $updatedContents;
}
}

View File

@ -0,0 +1,124 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Configurator;
use Symfony\Flex\Lock;
use Symfony\Flex\Recipe;
use Symfony\Flex\Update\RecipeUpdate;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class MakefileConfigurator extends AbstractConfigurator
{
public function configure(Recipe $recipe, $definitions, Lock $lock, array $options = [])
{
$this->write('Adding Makefile entries');
$this->configureMakefile($recipe, $definitions, $options['force'] ?? false);
}
public function unconfigure(Recipe $recipe, $vars, Lock $lock)
{
if (!file_exists($makefile = $this->options->get('root-dir').'/Makefile')) {
return;
}
$contents = preg_replace(sprintf('{%s*###> %s ###.*###< %s ###%s+}s', "\n", $recipe->getName(), $recipe->getName(), "\n"), "\n", file_get_contents($makefile), -1, $count);
if (!$count) {
return;
}
$this->write(sprintf('Removing Makefile entries from %s', $makefile));
if (!trim($contents)) {
@unlink($makefile);
} else {
file_put_contents($makefile, ltrim($contents, "\r\n"));
}
}
public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void
{
$recipeUpdate->setOriginalFile(
'Makefile',
$this->getContentsAfterApplyingRecipe($recipeUpdate->getRootDir(), $recipeUpdate->getOriginalRecipe(), $originalConfig)
);
$recipeUpdate->setNewFile(
'Makefile',
$this->getContentsAfterApplyingRecipe($recipeUpdate->getRootDir(), $recipeUpdate->getNewRecipe(), $newConfig)
);
}
private function configureMakefile(Recipe $recipe, array $definitions, bool $update)
{
$makefile = $this->options->get('root-dir').'/Makefile';
if (!$update && $this->isFileMarked($recipe, $makefile)) {
return;
}
$data = $this->options->expandTargetDir(implode("\n", $definitions));
$data = $this->markData($recipe, $data);
$data = "\n".ltrim($data, "\r\n");
if (!file_exists($makefile)) {
$envKey = $this->options->get('runtime')['env_var_name'] ?? 'APP_ENV';
$dotenvPath = $this->options->get('runtime')['dotenv_path'] ?? '.env';
file_put_contents(
$this->options->get('root-dir').'/Makefile',
<<<EOF
ifndef {$envKey}
include {$dotenvPath}
endif
.DEFAULT_GOAL := help
.PHONY: help
help:
@awk 'BEGIN {FS = ":.*?## "}; /^[a-zA-Z-]+:.*?## .*$$/ {printf "\033[32m%-15s\033[0m %s\\n", $$1, $$2}' Makefile | sort
EOF
);
}
if (!$this->updateData($makefile, $data)) {
file_put_contents($makefile, $data, \FILE_APPEND);
}
}
private function getContentsAfterApplyingRecipe(string $rootDir, Recipe $recipe, array $definitions): ?string
{
if (0 === \count($definitions)) {
return null;
}
$file = $rootDir.'/Makefile';
$originalContents = file_exists($file) ? file_get_contents($file) : null;
$this->configureMakefile(
$recipe,
$definitions,
true
);
$updatedContents = file_exists($file) ? file_get_contents($file) : null;
if (null === $originalContents) {
if (file_exists($file)) {
unlink($file);
}
} else {
file_put_contents($file, $originalContents);
}
return $updatedContents;
}
}

View File

@ -0,0 +1,216 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\Downloader\TransportException;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class CurlDownloader
{
private $multiHandle;
private $shareHandle;
private $jobs = [];
private $exceptions = [];
private static $options = [
'http' => [
'method' => \CURLOPT_CUSTOMREQUEST,
'content' => \CURLOPT_POSTFIELDS,
],
'ssl' => [
'cafile' => \CURLOPT_CAINFO,
'capath' => \CURLOPT_CAPATH,
],
];
private static $timeInfo = [
'total_time' => true,
'namelookup_time' => true,
'connect_time' => true,
'pretransfer_time' => true,
'starttransfer_time' => true,
'redirect_time' => true,
];
public function __construct()
{
$this->multiHandle = $mh = curl_multi_init();
curl_multi_setopt($mh, \CURLMOPT_PIPELINING, /*CURLPIPE_MULTIPLEX*/ 2);
if (\defined('CURLMOPT_MAX_HOST_CONNECTIONS')) {
curl_multi_setopt($mh, \CURLMOPT_MAX_HOST_CONNECTIONS, 8);
}
$this->shareHandle = $sh = curl_share_init();
curl_share_setopt($sh, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_COOKIE);
curl_share_setopt($sh, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_DNS);
curl_share_setopt($sh, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_SSL_SESSION);
}
public function get($origin, $url, $context, $file)
{
$params = stream_context_get_params($context);
$ch = curl_init();
$hd = fopen('php://temp/maxmemory:32768', 'w+b');
if ($file && !$fd = @fopen($file.'~', 'w+b')) {
$file = null;
}
if (!$file) {
$fd = @fopen('php://temp/maxmemory:524288', 'w+b');
}
$headers = array_diff($params['options']['http']['header'], ['Connection: close']);
if (!isset($params['options']['http']['protocol_version'])) {
curl_setopt($ch, \CURLOPT_HTTP_VERSION, \CURL_HTTP_VERSION_1_0);
} else {
$headers[] = 'Connection: keep-alive';
if (0 === strpos($url, 'https://') && \defined('CURL_VERSION_HTTP2') && \defined('CURL_HTTP_VERSION_2_0') && (\CURL_VERSION_HTTP2 & curl_version()['features'])) {
curl_setopt($ch, \CURLOPT_HTTP_VERSION, \CURL_HTTP_VERSION_2_0);
}
}
curl_setopt($ch, \CURLOPT_URL, $url);
curl_setopt($ch, \CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, \CURLOPT_DNS_USE_GLOBAL_CACHE, false);
curl_setopt($ch, \CURLOPT_WRITEHEADER, $hd);
curl_setopt($ch, \CURLOPT_FILE, $fd);
curl_setopt($ch, \CURLOPT_SHARE, $this->shareHandle);
foreach (self::$options as $type => $options) {
foreach ($options as $name => $curlopt) {
if (isset($params['options'][$type][$name])) {
curl_setopt($ch, $curlopt, $params['options'][$type][$name]);
}
}
}
$progress = array_diff_key(curl_getinfo($ch), self::$timeInfo);
$this->jobs[(int) $ch] = [
'progress' => $progress,
'ch' => $ch,
'callback' => $params['notification'],
'file' => $file,
'fd' => $fd,
];
curl_multi_add_handle($this->multiHandle, $ch);
$params['notification'](\STREAM_NOTIFY_RESOLVE, \STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0, false);
$active = true;
try {
while ($active && isset($this->jobs[(int) $ch])) {
curl_multi_exec($this->multiHandle, $active);
curl_multi_select($this->multiHandle);
while ($progress = curl_multi_info_read($this->multiHandle)) {
if (!isset($this->jobs[$i = (int) $h = $progress['handle']])) {
continue;
}
$progress = array_diff_key(curl_getinfo($h), self::$timeInfo);
$job = $this->jobs[$i];
unset($this->jobs[$i]);
curl_multi_remove_handle($this->multiHandle, $h);
try {
$this->onProgress($h, $job['callback'], $progress, $job['progress']);
if ('' !== curl_error($h)) {
throw new TransportException(curl_error($h));
}
if ($job['file'] && \CURLE_OK === curl_errno($h) && !isset($this->exceptions[$i])) {
fclose($job['fd']);
rename($job['file'].'~', $job['file']);
}
} catch (TransportException $e) {
$this->exceptions[$i] = $e;
}
}
foreach ($this->jobs as $i => $h) {
if (!isset($this->jobs[$i])) {
continue;
}
$h = $this->jobs[$i]['ch'];
$progress = array_diff_key(curl_getinfo($h), self::$timeInfo);
if ($this->jobs[$i]['progress'] !== $progress) {
$previousProgress = $this->jobs[$i]['progress'];
$this->jobs[$i]['progress'] = $progress;
try {
$this->onProgress($h, $this->jobs[$i]['callback'], $progress, $previousProgress);
} catch (TransportException $e) {
unset($this->jobs[$i]);
curl_multi_remove_handle($this->multiHandle, $h);
$this->exceptions[$i] = $e;
}
}
}
}
if ('' !== curl_error($ch) || \CURLE_OK !== curl_errno($ch)) {
$this->exceptions[(int) $ch] = new TransportException(curl_error($ch), curl_getinfo($ch, \CURLINFO_HTTP_CODE) ?: 0);
}
if (isset($this->exceptions[(int) $ch])) {
throw $this->exceptions[(int) $ch];
}
} finally {
if ($file && !isset($this->exceptions[(int) $ch])) {
$fd = fopen($file, 'rb');
}
$progress = array_diff_key(curl_getinfo($ch), self::$timeInfo);
$this->finishProgress($ch, $params['notification'], $progress);
unset($this->jobs[(int) $ch], $this->exceptions[(int) $ch]);
curl_multi_remove_handle($this->multiHandle, $ch);
curl_close($ch);
rewind($hd);
$headers = explode("\r\n", rtrim(stream_get_contents($hd)));
fclose($hd);
rewind($fd);
$contents = stream_get_contents($fd);
fclose($fd);
}
return [$headers, $contents];
}
private function onProgress($ch, callable $notify, array $progress, array $previousProgress)
{
if (300 <= $progress['http_code'] && $progress['http_code'] < 400 || 0 > $progress['download_content_length']) {
return;
}
if (!$previousProgress['http_code'] && $progress['http_code'] && $progress['http_code'] < 200 || 400 <= $progress['http_code']) {
$code = 403 === $progress['http_code'] ? \STREAM_NOTIFY_AUTH_RESULT : \STREAM_NOTIFY_FAILURE;
$notify($code, \STREAM_NOTIFY_SEVERITY_ERR, curl_error($ch), $progress['http_code'], 0, 0, false);
}
if ($previousProgress['download_content_length'] < $progress['download_content_length']) {
$notify(\STREAM_NOTIFY_FILE_SIZE_IS, \STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, (int) $progress['download_content_length'], false);
}
if ($previousProgress['size_download'] < $progress['size_download']) {
$notify(\STREAM_NOTIFY_PROGRESS, \STREAM_NOTIFY_SEVERITY_INFO, '', 0, (int) $progress['size_download'], (int) $progress['download_content_length'], false);
}
}
private function finishProgress($ch, callable $notify, array $progress)
{
if ($progress['download_content_length'] < 0) {
$notify(\STREAM_NOTIFY_FILE_SIZE_IS, \STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, (int) $progress['size_download'], false);
$notify(\STREAM_NOTIFY_PROGRESS, \STREAM_NOTIFY_SEVERITY_INFO, '', 0, (int) $progress['size_download'], (int) $progress['size_download'], false);
}
}
}

494
vendor/symfony/flex/src/Downloader.php vendored Normal file
View File

@ -0,0 +1,494 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\Cache as ComposerCache;
use Composer\Composer;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\IO\IOInterface;
use Composer\Json\JsonFile;
use Composer\Util\Http\Response as ComposerResponse;
use Composer\Util\HttpDownloader;
use Composer\Util\Loop;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class Downloader
{
private const DEFAULT_ENDPOINTS = [
'https://raw.githubusercontent.com/symfony/recipes/flex/main/index.json',
'https://raw.githubusercontent.com/symfony/recipes-contrib/flex/main/index.json',
];
private const MAX_LENGTH = 1000;
private static $versions;
private static $aliases;
private $io;
private $sess;
private $cache;
/** @var HttpDownloader|ParallelDownloader */
private $rfs;
private $degradedMode = false;
private $endpoints;
private $index;
private $conflicts;
private $legacyEndpoint;
private $caFile;
private $enabled = true;
private $composer;
public function __construct(Composer $composer, IoInterface $io, $rfs)
{
if (getenv('SYMFONY_CAFILE')) {
$this->caFile = getenv('SYMFONY_CAFILE');
}
if (null === $endpoint = $composer->getPackage()->getExtra()['symfony']['endpoint'] ?? null) {
$this->endpoints = self::DEFAULT_ENDPOINTS;
} elseif (\is_array($endpoint) || false !== strpos($endpoint, '.json') || 'flex://defaults' === $endpoint) {
$this->endpoints = array_values((array) $endpoint);
if (\is_string($endpoint) && false !== strpos($endpoint, '.json')) {
$this->endpoints[] = 'flex://defaults';
}
} else {
$this->legacyEndpoint = rtrim($endpoint, '/');
}
if (false === $endpoint = getenv('SYMFONY_ENDPOINT')) {
// no-op
} elseif (false !== strpos($endpoint, '.json') || 'flex://defaults' === $endpoint) {
$this->endpoints ?? $this->endpoints = self::DEFAULT_ENDPOINTS;
array_unshift($this->endpoints, $endpoint);
$this->legacyEndpoint = null;
} else {
$this->endpoints = null;
$this->legacyEndpoint = rtrim($endpoint, '/');
}
if (null !== $this->endpoints) {
if (false !== $i = array_search('flex://defaults', $this->endpoints, true)) {
array_splice($this->endpoints, $i, 1, self::DEFAULT_ENDPOINTS);
}
$this->endpoints = array_fill_keys($this->endpoints, []);
}
$this->io = $io;
$config = $composer->getConfig();
$this->rfs = $rfs;
$this->cache = new ComposerCache($io, $config->get('cache-repo-dir').'/flex');
$this->sess = bin2hex(random_bytes(16));
$this->composer = $composer;
}
public function getSessionId(): string
{
return $this->sess;
}
public function setFlexId(string $id = null)
{
// No-op to support downgrading to v1.12.x
}
public function isEnabled()
{
return $this->enabled;
}
public function disable()
{
$this->enabled = false;
}
public function getVersions()
{
$this->initialize();
return self::$versions ?? self::$versions = current($this->get([$this->legacyEndpoint.'/versions.json']));
}
public function getAliases()
{
$this->initialize();
return self::$aliases ?? self::$aliases = current($this->get([$this->legacyEndpoint.'/aliases.json']));
}
/**
* Downloads recipes.
*
* @param OperationInterface[] $operations
*/
public function getRecipes(array $operations): array
{
$this->initialize();
if ($this->conflicts) {
$lockedRepository = $this->composer->getLocker()->getLockedRepository();
foreach ($this->conflicts as $conflicts) {
foreach ($conflicts as $package => $versions) {
foreach ($versions as $version => $conflicts) {
foreach ($conflicts as $conflictingPackage => $constraint) {
if ($lockedRepository->findPackage($conflictingPackage, $constraint)) {
unset($this->index[$package][$version]);
}
}
}
}
}
$this->conflicts = [];
}
$data = [];
$urls = [];
$chunk = '';
$recipeRef = null;
foreach ($operations as $operation) {
$o = 'i';
if ($operation instanceof UpdateOperation) {
$package = $operation->getTargetPackage();
$o = 'u';
} else {
$package = $operation->getPackage();
if ($operation instanceof UninstallOperation) {
$o = 'r';
}
if ($operation instanceof InformationOperation) {
$recipeRef = $operation->getRecipeRef();
}
}
$version = $package->getPrettyVersion();
if ($operation instanceof InformationOperation && $operation->getVersion()) {
$version = $operation->getVersion();
}
if (0 === strpos($version, 'dev-') && isset($package->getExtra()['branch-alias'])) {
$branchAliases = $package->getExtra()['branch-alias'];
if (
(isset($branchAliases[$version]) && $alias = $branchAliases[$version]) ||
(isset($branchAliases['dev-main']) && $alias = $branchAliases['dev-main']) ||
(isset($branchAliases['dev-trunk']) && $alias = $branchAliases['dev-trunk']) ||
(isset($branchAliases['dev-develop']) && $alias = $branchAliases['dev-develop']) ||
(isset($branchAliases['dev-default']) && $alias = $branchAliases['dev-default']) ||
(isset($branchAliases['dev-latest']) && $alias = $branchAliases['dev-latest']) ||
(isset($branchAliases['dev-next']) && $alias = $branchAliases['dev-next']) ||
(isset($branchAliases['dev-current']) && $alias = $branchAliases['dev-current']) ||
(isset($branchAliases['dev-support']) && $alias = $branchAliases['dev-support']) ||
(isset($branchAliases['dev-tip']) && $alias = $branchAliases['dev-tip']) ||
(isset($branchAliases['dev-master']) && $alias = $branchAliases['dev-master'])
) {
$version = $alias;
}
}
if ($recipeVersions = $this->index[$package->getName()] ?? null) {
$version = explode('.', preg_replace('/^dev-|^v|\.x-dev$|-dev$/', '', $version));
$version = $version[0].'.'.($version[1] ?? '9999999');
foreach (array_reverse($recipeVersions) as $v => $endpoint) {
if (version_compare($version, $v, '<')) {
continue;
}
$data['locks'][$package->getName()]['version'] = $version;
$data['locks'][$package->getName()]['recipe']['version'] = $v;
$links = $this->endpoints[$endpoint]['_links'];
if (null !== $recipeRef && isset($links['archived_recipes_template'])) {
if (isset($links['archived_recipes_template_relative'])) {
$links['archived_recipes_template'] = preg_replace('{[^/\?]*+(?=\?|$)}', $links['archived_recipes_template_relative'], $endpoint, 1);
}
$urls[] = strtr($links['archived_recipes_template'], [
'{package_dotted}' => str_replace('/', '.', $package->getName()),
'{ref}' => $recipeRef,
]);
break;
}
if (isset($links['recipes_template_relative'])) {
$links['recipes_template'] = preg_replace('{[^/\?]*+(?=\?|$)}', $links['recipes_template_relative'], $endpoint, 1);
}
$urls[] = strtr($links['recipe_template'], [
'{package_dotted}' => str_replace('/', '.', $package->getName()),
'{package}' => $package->getName(),
'{version}' => $v,
]);
break;
}
continue;
}
if (\is_array($recipeVersions)) {
$data['conflicts'][$package->getName()] = true;
}
if (null !== $this->endpoints) {
$data['locks'][$package->getName()]['version'] = $version;
continue;
}
// FIXME: Multi name with getNames()
$name = str_replace('/', ',', $package->getName());
$path = sprintf('%s,%s%s', $name, $o, $version);
if ($date = $package->getReleaseDate()) {
$path .= ','.$date->format('U');
}
if (\strlen($chunk) + \strlen($path) > self::MAX_LENGTH) {
$urls[] = $this->legacyEndpoint.'/p/'.$chunk;
$chunk = $path;
} elseif ($chunk) {
$chunk .= ';'.$path;
} else {
$chunk = $path;
}
}
if ($chunk) {
$urls[] = $this->legacyEndpoint.'/p/'.$chunk;
}
if (null === $this->endpoints) {
foreach ($this->get($urls, true) as $body) {
foreach ($body['manifests'] ?? [] as $name => $manifest) {
$data['manifests'][$name] = $manifest;
}
foreach ($body['locks'] ?? [] as $name => $lock) {
$data['locks'][$name] = $lock;
}
}
} else {
foreach ($this->get($urls, true) as $body) {
foreach ($body['manifests'] ?? [] as $name => $manifest) {
if (null === $version = $data['locks'][$name]['recipe']['version'] ?? null) {
continue;
}
$endpoint = $this->endpoints[$this->index[$name][$version]];
$data['locks'][$name]['recipe'] = [
'repo' => $endpoint['_links']['repository'],
'branch' => $endpoint['branch'],
'version' => $version,
'ref' => $manifest['ref'],
];
foreach ($manifest['files'] ?? [] as $i => $file) {
$manifest['files'][$i]['contents'] = \is_array($file['contents']) ? implode("\n", $file['contents']) : base64_decode($file['contents']);
}
$data['manifests'][$name] = $manifest + [
'repository' => $endpoint['_links']['repository'],
'package' => $name,
'version' => $version,
'origin' => strtr($endpoint['_links']['origin_template'], [
'{package}' => $name,
'{version}' => $version,
]),
'is_contrib' => $endpoint['is_contrib'] ?? false,
];
}
}
}
return $data;
}
/**
* Used to "hide" a recipe version so that the next most-recent will be returned.
*
* This is used when resolving "conflicts".
*/
public function removeRecipeFromIndex(string $packageName, string $version)
{
unset($this->index[$packageName][$version]);
}
/**
* Fetches and decodes JSON HTTP response bodies.
*/
private function get(array $urls, bool $isRecipe = false, int $try = 3): array
{
$responses = [];
$retries = [];
$options = [];
foreach ($urls as $url) {
$cacheKey = self::generateCacheKey($url);
$headers = [];
if (preg_match('{^https?://api\.github\.com/}', $url)) {
$headers[] = 'Accept: application/vnd.github.v3.raw';
} elseif (preg_match('{^https?://raw\.githubusercontent\.com/}', $url) && $this->io->hasAuthentication('github.com')) {
$auth = $this->io->getAuthentication('github.com');
if ('x-oauth-basic' === $auth['password']) {
$headers[] = 'Authorization: token '.$auth['username'];
}
} elseif ($this->legacyEndpoint) {
$headers[] = 'Package-Session: '.$this->sess;
}
if ($contents = $this->cache->read($cacheKey)) {
$cachedResponse = Response::fromJson(json_decode($contents, true));
if ($lastModified = $cachedResponse->getHeader('last-modified')) {
$headers[] = 'If-Modified-Since: '.$lastModified;
}
if ($eTag = $cachedResponse->getHeader('etag')) {
$headers[] = 'If-None-Match: '.$eTag;
}
$responses[$url] = $cachedResponse->getBody();
}
$options[$url] = $this->getOptions($headers);
}
if ($this->rfs instanceof HttpDownloader) {
$loop = new Loop($this->rfs);
$jobs = [];
foreach ($urls as $url) {
$jobs[] = $this->rfs->add($url, $options[$url])->then(function (ComposerResponse $response) use ($url, &$responses) {
if (200 === $response->getStatusCode()) {
$cacheKey = self::generateCacheKey($url);
$responses[$url] = $this->parseJson($response->getBody(), $url, $cacheKey, $response->getHeaders())->getBody();
}
}, function (\Exception $e) use ($url, &$retries) {
$retries[] = [$url, $e];
});
}
$loop->wait($jobs);
} else {
foreach ($urls as $i => $url) {
$urls[$i] = [$url];
}
$this->rfs->download($urls, function ($url) use ($options, &$responses, &$retries, &$error) {
try {
$cacheKey = self::generateCacheKey($url);
$origin = method_exists($this->rfs, 'getOrigin') ? $this->rfs::getOrigin($url) : parse_url($url, \PHP_URL_HOST);
$json = $this->rfs->getContents($origin, $url, false, $options[$url]);
if (200 === $this->rfs->findStatusCode($this->rfs->getLastHeaders())) {
$responses[$url] = $this->parseJson($json, $url, $cacheKey, $this->rfs->getLastHeaders())->getBody();
}
} catch (\Exception $e) {
$retries[] = [$url, $e];
}
});
}
if (!$retries) {
return $responses;
}
if (0 < --$try) {
usleep(100000);
return $this->get(array_column($retries, 0), $isRecipe, $try) + $responses;
}
foreach ($retries as [$url, $e]) {
if (isset($responses[$url])) {
$this->switchToDegradedMode($e, $url);
} elseif ($isRecipe) {
$this->io->writeError('<warning>Failed to download recipe: '.$e->getMessage().'</>');
} else {
throw $e;
}
}
return $responses;
}
private function parseJson(string $json, string $url, string $cacheKey, array $lastHeaders): Response
{
$data = JsonFile::parseJson($json, $url);
if (!empty($data['warning'])) {
$this->io->writeError('<warning>Warning from '.$url.': '.$data['warning'].'</>');
}
if (!empty($data['info'])) {
$this->io->writeError('<info>Info from '.$url.': '.$data['info'].'</>');
}
$response = new Response($data, $lastHeaders);
if ($cacheKey && ($response->getHeader('last-modified') || $response->getHeader('etag'))) {
$this->cache->write($cacheKey, json_encode($response));
}
return $response;
}
private function switchToDegradedMode(\Exception $e, string $url)
{
if (!$this->degradedMode) {
$this->io->writeError('<warning>'.$e->getMessage().'</>');
$this->io->writeError('<warning>'.$url.' could not be fully loaded, package information was loaded from the local cache and may be out of date</>');
}
$this->degradedMode = true;
}
private function getOptions(array $headers): array
{
$options = ['http' => ['header' => $headers]];
if (null !== $this->caFile) {
$options['ssl']['cafile'] = $this->caFile;
}
return $options;
}
private function initialize()
{
if (null !== $this->index || null === $this->endpoints) {
$this->index ?? $this->index = [];
return;
}
$indexes = self::$versions = self::$aliases = [];
foreach ($this->get(array_keys($this->endpoints)) as $endpoint => $index) {
$indexes[$endpoint] = $index;
}
foreach ($this->endpoints as $endpoint => $config) {
$config = $indexes[$endpoint] ?? [];
foreach ($config['recipes'] ?? [] as $package => $versions) {
$this->index[$package] = $this->index[$package] ?? array_fill_keys($versions, $endpoint);
}
$this->conflicts[] = $config['recipe-conflicts'] ?? [];
self::$versions += $config['versions'] ?? [];
self::$aliases += $config['aliases'] ?? [];
unset($config['recipes'], $config['recipe-conflicts'], $config['versions'], $config['aliases']);
$this->endpoints[$endpoint] = $config;
}
}
private static function generateCacheKey(string $url): string
{
$url = preg_replace('{^https://api.github.com/repos/([^/]++/[^/]++)/contents/}', '$1/', $url);
$url = preg_replace('{^https://raw.githubusercontent.com/([^/]++/[^/]++)/}', '$1/', $url);
$key = preg_replace('{[^a-z0-9.]}i', '-', $url);
// eCryptfs can have problems with filenames longer than around 143 chars
return \strlen($key) > 140 ? md5($url) : $key;
}
}

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Event;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
class UpdateEvent extends Event
{
private $force;
private $reset;
public function __construct(bool $force, bool $reset)
{
$this->name = ScriptEvents::POST_UPDATE_CMD;
$this->force = $force;
$this->reset = $reset;
}
public function force(): bool
{
return $this->force;
}
public function reset(): bool
{
return $this->reset;
}
}

1078
vendor/symfony/flex/src/Flex.php vendored Normal file
View File

@ -0,0 +1,1078 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\Command\GlobalCommand;
use Composer\Composer;
use Composer\Console\Application;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Transaction;
use Composer\Downloader\FileDownloader;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\Factory;
use Composer\Installer;
use Composer\Installer\InstallerEvent;
use Composer\Installer\InstallerEvents;
use Composer\Installer\PackageEvent;
use Composer\Installer\PackageEvents;
use Composer\Installer\SuggestedPackagesReporter;
use Composer\IO\IOInterface;
use Composer\IO\NullIO;
use Composer\Json\JsonFile;
use Composer\Json\JsonManipulator;
use Composer\Package\BasePackage;
use Composer\Package\Comparer\Comparer;
use Composer\Package\Locker;
use Composer\Package\Package;
use Composer\Package\PackageInterface;
use Composer\Plugin\PluginEvents;
use Composer\Plugin\PluginInterface;
use Composer\Plugin\PreFileDownloadEvent;
use Composer\Plugin\PrePoolCreateEvent;
use Composer\Repository\ComposerRepository as BaseComposerRepository;
use Composer\Repository\RepositoryFactory;
use Composer\Repository\RepositoryManager;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Composer\Semver\VersionParser;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Flex\Event\UpdateEvent;
use Symfony\Flex\Unpack\Operation;
use Symfony\Thanks\Thanks;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class Flex implements PluginInterface, EventSubscriberInterface
{
/**
* @var Composer
*/
private $composer;
/**
* @var IOInterface
*/
private $io;
private $config;
private $options;
private $configurator;
private $downloader;
/**
* @var Installer
*/
private $installer;
private $postInstallOutput = [''];
private $operations = [];
private $lock;
private $cacheDirPopulated = false;
private $displayThanksReminder = 0;
private $rfs;
private $progress = true;
private $dryRun = false;
private static $activated = true;
private static $repoReadingCommands = [
'create-project' => true,
'outdated' => true,
'require' => true,
'update' => true,
'install' => true,
];
private static $aliasResolveCommands = [
'require' => true,
'update' => false,
'remove' => false,
'unpack' => true,
];
private $filter;
public function activate(Composer $composer, IOInterface $io)
{
if (!\extension_loaded('openssl')) {
self::$activated = false;
$io->writeError('<warning>Symfony Flex has been disabled. You must enable the openssl extension in your "php.ini" file.</>');
return;
}
// to avoid issues when Flex is upgraded, we load all PHP classes now
// that way, we are sure to use all classes from the same version
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(__DIR__, \FilesystemIterator::SKIP_DOTS)) as $file) {
if ('.php' === substr($file, -4)) {
class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__), -4)));
}
}
$this->composer = $composer;
$this->io = $io;
$this->config = $composer->getConfig();
$this->options = $this->initOptions();
$symfonyRequire = preg_replace('/\.x$/', '.x-dev', getenv('SYMFONY_REQUIRE') ?: ($composer->getPackage()->getExtra()['symfony']['require'] ?? ''));
if ($composer2 = version_compare('2.0.0', PluginInterface::PLUGIN_API_VERSION, '<=')) {
$rfs = Factory::createHttpDownloader($this->io, $this->config);
$this->downloader = $downloader = new Downloader($composer, $io, $rfs);
if ($symfonyRequire) {
$this->filter = new PackageFilter($io, $symfonyRequire, $this->downloader);
}
$setRepositories = null;
} else {
$rfs = Factory::createRemoteFilesystem($this->io, $this->config);
$this->rfs = $rfs = new ParallelDownloader($this->io, $this->config, $rfs->getOptions(), $rfs->isTlsDisabled());
$this->downloader = $downloader = new Downloader($composer, $io, $this->rfs);
$rootPackage = $composer->getPackage();
$manager = RepositoryFactory::manager($this->io, $this->config, $composer->getEventDispatcher(), $this->rfs);
$setRepositories = \Closure::bind(function (RepositoryManager $manager) use (&$symfonyRequire, $rootPackage, $downloader) {
$manager->repositoryClasses = $this->repositoryClasses;
$manager->setRepositoryClass('composer', TruncatedComposerRepository::class);
$manager->repositories = $this->repositories;
$i = 0;
foreach (RepositoryFactory::defaultRepos(null, $this->config, $manager) as $repo) {
$manager->repositories[$i++] = $repo;
if ($repo instanceof TruncatedComposerRepository && $symfonyRequire) {
$repo->setSymfonyRequire($symfonyRequire, $rootPackage, $downloader, $this->io);
}
}
$manager->setLocalRepository($this->getLocalRepository());
}, $composer->getRepositoryManager(), RepositoryManager::class);
$setRepositories($manager);
$composer->setRepositoryManager($manager);
}
$this->configurator = new Configurator($composer, $io, $this->options);
$this->lock = new Lock(getenv('SYMFONY_LOCKFILE') ?: str_replace('composer.json', 'symfony.lock', Factory::getComposerFile()));
$disable = true;
foreach (array_merge($composer->getPackage()->getRequires() ?? [], $composer->getPackage()->getDevRequires() ?? []) as $link) {
// recipes apply only when symfony/flex is found in "require" or "require-dev" in the root package
if ('symfony/flex' === $link->getTarget()) {
$disable = false;
break;
}
}
if ($disable) {
$downloader->disable();
}
$populateRepoCacheDir = !$composer2 && __CLASS__ === self::class;
if (!$composer2 && $composer->getPluginManager()) {
foreach ($composer->getPluginManager()->getPlugins() as $plugin) {
if (0 === strpos(\get_class($plugin), 'Hirak\Prestissimo\Plugin')) {
if (method_exists($rfs, 'getRemoteContents')) {
$plugin->disable();
} else {
$this->cacheDirPopulated = true;
}
$populateRepoCacheDir = false;
break;
}
}
}
$backtrace = $this->configureInstaller();
foreach ($backtrace as $trace) {
if (!isset($trace['object']) || !isset($trace['args'][0])) {
continue;
}
if (!$trace['object'] instanceof Application || !$trace['args'][0] instanceof ArgvInput) {
continue;
}
// In Composer 1.0.*, $input knows about option and argument definitions
// Since Composer >=1.1, $input contains only raw values
$input = $trace['args'][0];
$app = $trace['object'];
$resolver = new PackageResolver($this->downloader);
if (version_compare('1.1.0', PluginInterface::PLUGIN_API_VERSION, '>')) {
$note = $app->has('self-update') ? sprintf('`php %s self-update`', $_SERVER['argv'][0]) : 'https://getcomposer.org/';
$io->writeError('<warning>Some Symfony Flex features may not work as expected: your version of Composer is too old</>');
$io->writeError(sprintf('<warning>Please upgrade using %s</>', $note));
}
try {
$command = $input->getFirstArgument();
$command = $command ? $app->find($command)->getName() : null;
} catch (\InvalidArgumentException $e) {
}
if ('create-project' === $command) {
// detect Composer >=1.7 (using the Composer::VERSION constant doesn't work with snapshot builds)
if (class_exists(Comparer::class)) {
if ($input->hasOption('remove-vcs')) {
$input->setOption('remove-vcs', true);
}
} else {
$input->setInteractive(false);
}
$populateRepoCacheDir = $populateRepoCacheDir && !$input->hasOption('remove-vcs');
} elseif ('update' === $command) {
$this->displayThanksReminder = 1;
} elseif ('outdated' === $command) {
$symfonyRequire = null;
if ($setRepositories) {
$setRepositories($manager);
}
}
if (isset(self::$aliasResolveCommands[$command])) {
// early resolve for BC with Composer 1.0
if ($input->hasArgument('packages')) {
$input->setArgument('packages', $resolver->resolve($input->getArgument('packages'), self::$aliasResolveCommands[$command]));
}
if (version_compare('2.0.0', PluginInterface::PLUGIN_API_VERSION, '>') && $input->hasOption('no-suggest')) {
$input->setOption('no-suggest', true);
}
}
if (!$composer2) {
if ($input->hasParameterOption('--no-progress', true)) {
$this->progress = false;
}
if ($input->hasParameterOption('--dry-run', true)) {
$this->dryRun = true;
}
}
if ($input->hasParameterOption('--prefer-lowest', true)) {
// When prefer-lowest is set and no stable version has been released,
// we consider "dev" more stable than "alpha", "beta" or "RC". This
// allows testing lowest versions with potential fixes applied.
BasePackage::$stabilities['dev'] = 1 + BasePackage::STABILITY_STABLE;
}
if ($populateRepoCacheDir && isset(self::$repoReadingCommands[$command]) && ('install' !== $command || (file_exists($composerFile = Factory::getComposerFile()) && !file_exists(substr($composerFile, 0, -4).'lock')))) {
$this->populateRepoCacheDir();
}
$app->add(new Command\RequireCommand($resolver, \Closure::fromCallable([$this, 'updateComposerLock'])));
$app->add(new Command\UpdateCommand($resolver));
$app->add(new Command\RemoveCommand($resolver));
$app->add(new Command\UnpackCommand($resolver));
$app->add(new Command\RecipesCommand($this, $this->lock, $rfs));
$app->add(new Command\InstallRecipesCommand($this, $this->options->get('root-dir'), $this->options->get('runtime')['dotenv_path'] ?? '.env'));
$app->add(new Command\UpdateRecipesCommand($this, $this->downloader, $rfs, $this->configurator, $this->options->get('root-dir')));
if (class_exists(Command\GenerateIdCommand::class)) {
$app->add(new Command\GenerateIdCommand(null));
}
$app->add(new Command\DumpEnvCommand($this->config, $this->options));
break;
}
}
public function deactivate(Composer $composer, IOInterface $io)
{
self::$activated = false;
}
public function configureInstaller()
{
$backtrace = debug_backtrace();
foreach ($backtrace as $trace) {
if (isset($trace['object']) && $trace['object'] instanceof Installer) {
$this->installer = $trace['object']->setSuggestedPackagesReporter(new SuggestedPackagesReporter(new NullIO()));
}
if (isset($trace['object']) && $trace['object'] instanceof GlobalCommand) {
$this->downloader->disable();
}
}
return $backtrace;
}
public function configureProject(Event $event)
{
if (!$this->downloader->isEnabled()) {
$this->io->writeError('<warning>Project configuration is disabled: "symfony/flex" not found in the root composer.json</>');
return;
}
// Remove LICENSE (which do not apply to the user project)
@unlink('LICENSE');
// Update composer.json (project is proprietary by default)
$file = Factory::getComposerFile();
$contents = file_get_contents($file);
$manipulator = new JsonManipulator($contents);
$json = JsonFile::parseJson($contents);
// new projects are most of the time proprietary
$manipulator->addMainKey('license', 'proprietary');
// extra.branch-alias doesn't apply to the project
$manipulator->removeSubNode('extra', 'branch-alias');
// 'name' and 'description' are only required for public packages
// don't use $manipulator->removeProperty() for BC with Composer 1.0
$contents = preg_replace(['{^\s*+"name":.*,$\n}m', '{^\s*+"description":.*,$\n}m'], '', $manipulator->getContents(), 1);
file_put_contents($file, $contents);
$this->updateComposerLock();
}
public function record(PackageEvent $event)
{
if ($this->shouldRecordOperation($event->getOperation(), $event->isDevMode(), $event->getComposer())) {
$this->operations[] = $event->getOperation();
}
}
public function recordOperations(InstallerEvent $event)
{
if (!$event->isExecutingOperations()) {
return;
}
$versionParser = new VersionParser();
$packages = [];
foreach ($this->lock->all() as $name => $info) {
$packages[] = new Package($name, $versionParser->normalize($info['version']), $info['version']);
}
$transation = \Closure::bind(function () use ($packages, $event) {
return new Transaction($packages, $event->getTransaction()->resultPackageMap);
}, null, Transaction::class)();
foreach ($transation->getOperations() as $operation) {
if ($this->shouldRecordOperation($operation, $event->isDevMode(), $event->getComposer())) {
$this->operations[] = $operation;
}
}
}
public function update(Event $event, $operations = [])
{
if ($operations) {
$this->operations = $operations;
}
$this->install($event);
$file = Factory::getComposerFile();
$contents = file_get_contents($file);
$json = JsonFile::parseJson($contents);
if (!isset($json['flex-require']) && !isset($json['flex-require-dev'])) {
$this->unpack($event);
return;
}
// merge "flex-require" with "require"
$manipulator = new JsonManipulator($contents);
$sortPackages = $this->composer->getConfig()->get('sort-packages');
$symfonyVersion = $json['extra']['symfony']['require'] ?? null;
$versions = $symfonyVersion ? $this->downloader->getVersions() : null;
foreach (['require', 'require-dev'] as $type) {
if (isset($json['flex-'.$type])) {
foreach ($json['flex-'.$type] as $package => $constraint) {
if ($symfonyVersion && '*' === $constraint && isset($versions['splits'][$package])) {
// replace unbounded constraints for symfony/* packages by extra.symfony.require
$constraint = $symfonyVersion;
}
$manipulator->addLink($type, $package, $constraint, $sortPackages);
}
$manipulator->removeMainKey('flex-'.$type);
}
}
file_put_contents($file, $manipulator->getContents());
$this->reinstall($event, true);
}
public function install(Event $event)
{
$rootDir = $this->options->get('root-dir');
$runtime = $this->options->get('runtime');
$dotenvPath = $rootDir.'/'.($runtime['dotenv_path'] ?? '.env');
if (!file_exists($dotenvPath) && !file_exists($dotenvPath.'.local') && file_exists($dotenvPath.'.dist') && false === strpos(file_get_contents($dotenvPath.'.dist'), '.env.local')) {
copy($dotenvPath.'.dist', $dotenvPath);
}
// Execute missing recipes
$recipes = ScriptEvents::POST_UPDATE_CMD === $event->getName() ? $this->fetchRecipes($this->operations, $event instanceof UpdateEvent && $event->reset()) : [];
$this->operations = []; // Reset the operation after getting recipes
if (2 === $this->displayThanksReminder) {
$love = '\\' === \DIRECTORY_SEPARATOR ? 'love' : '💖 ';
$star = '\\' === \DIRECTORY_SEPARATOR ? 'star' : '★ ';
$this->io->writeError('');
$this->io->writeError('What about running <comment>composer global require symfony/thanks && composer thanks</> now?');
$this->io->writeError(sprintf('This will spread some %s by sending a %s to the GitHub repositories of your fellow package maintainers.', $love, $star));
}
$this->io->writeError('');
if (!$recipes) {
if (ScriptEvents::POST_UPDATE_CMD === $event->getName()) {
$this->finish($rootDir);
}
if ($this->downloader->isEnabled()) {
$this->io->writeError('Run <comment>composer recipes</> at any time to see the status of your Symfony recipes.');
$this->io->writeError('');
}
return;
}
$this->io->writeError(sprintf('<info>Symfony operations: %d recipe%s (%s)</>', \count($recipes), \count($recipes) > 1 ? 's' : '', $this->downloader->getSessionId()));
$installContribs = $this->composer->getPackage()->getExtra()['symfony']['allow-contrib'] ?? false;
$manifest = null;
$originalComposerJsonHash = $this->getComposerJsonHash();
foreach ($recipes as $recipe) {
if ('install' === $recipe->getJob() && !$installContribs && $recipe->isContrib()) {
$warning = $this->io->isInteractive() ? 'WARNING' : 'IGNORING';
$this->io->writeError(sprintf(' - <warning> %s </> %s', $warning, $this->formatOrigin($recipe)));
$question = sprintf(' The recipe for this package comes from the "contrib" repository, which is open to community contributions.
Review the recipe at %s
Do you want to execute this recipe?
[<comment>y</>] Yes
[<comment>n</>] No
[<comment>a</>] Yes for all packages, only for the current installation session
[<comment>p</>] Yes permanently, never ask again for this project
(defaults to <comment>n</>): ', $recipe->getURL());
$answer = $this->io->askAndValidate(
$question,
function ($value) {
if (null === $value) {
return 'n';
}
$value = strtolower($value[0]);
if (!\in_array($value, ['y', 'n', 'a', 'p'])) {
throw new \InvalidArgumentException('Invalid choice.');
}
return $value;
},
null,
'n'
);
if ('n' === $answer) {
continue;
}
if ('a' === $answer) {
$installContribs = true;
}
if ('p' === $answer) {
$installContribs = true;
$json = new JsonFile(Factory::getComposerFile());
$manipulator = new JsonManipulator(file_get_contents($json->getPath()));
$manipulator->addSubNode('extra', 'symfony.allow-contrib', true);
file_put_contents($json->getPath(), $manipulator->getContents());
}
}
switch ($recipe->getJob()) {
case 'install':
$this->io->writeError(sprintf(' - Configuring %s', $this->formatOrigin($recipe)));
$this->configurator->install($recipe, $this->lock, [
'force' => $event instanceof UpdateEvent && $event->force(),
]);
$manifest = $recipe->getManifest();
if (isset($manifest['post-install-output'])) {
$this->postInstallOutput[] = sprintf('<bg=yellow;fg=white> %s </> instructions:', $recipe->getName());
$this->postInstallOutput[] = '';
foreach ($manifest['post-install-output'] as $line) {
$this->postInstallOutput[] = $this->options->expandTargetDir($line);
}
$this->postInstallOutput[] = '';
}
break;
case 'update':
break;
case 'uninstall':
$this->io->writeError(sprintf(' - Unconfiguring %s', $this->formatOrigin($recipe)));
$this->configurator->unconfigure($recipe, $this->lock);
break;
}
}
if (null !== $manifest) {
array_unshift(
$this->postInstallOutput,
'<bg=blue;fg=white> </>',
'<bg=blue;fg=white> What\'s next? </>',
'<bg=blue;fg=white> </>',
'',
'<info>Some files have been created and/or updated to configure your new packages.</>',
'Please <comment>review</>, <comment>edit</> and <comment>commit</> them: these files are <comment>yours</>.'
);
}
$this->finish($rootDir, $originalComposerJsonHash);
}
public function finish(string $rootDir, string $originalComposerJsonHash = null): void
{
$this->synchronizePackageJson($rootDir);
$this->lock->write();
if ($originalComposerJsonHash && $this->getComposerJsonHash() !== $originalComposerJsonHash) {
$this->updateComposerLock();
}
}
private function synchronizePackageJson(string $rootDir)
{
$rootDir = realpath($rootDir);
$vendorDir = trim((new Filesystem())->makePathRelative($this->config->get('vendor-dir'), $rootDir), '/');
$synchronizer = new PackageJsonSynchronizer($rootDir, $vendorDir);
if ($synchronizer->shouldSynchronize()) {
$lockData = $this->composer->getLocker()->getLockData();
if (method_exists($synchronizer, 'addPackageJsonLink') && 'string' === (new \ReflectionParameter([$synchronizer, 'addPackageJsonLink'], 'phpPackage'))->getType()->getName()) {
// support for smooth upgrades from older flex versions
$lockData['packages'] = array_column($lockData['packages'] ?? [], 'name');
$lockData['packages-dev'] = array_column($lockData['packages-dev'] ?? [], 'name');
}
if ($synchronizer->synchronize(array_merge($lockData['packages'] ?? [], $lockData['packages-dev'] ?? []))) {
$this->io->writeError('<info>Synchronizing package.json with PHP packages</>');
$this->io->writeError('<warning>Don\'t forget to run npm install --force or yarn install --force to refresh your JavaScript dependencies!</>');
$this->io->writeError('');
}
}
}
public function uninstall(Composer $composer, IOInterface $io)
{
$this->lock->delete();
}
public function enableThanksReminder()
{
if (1 === $this->displayThanksReminder) {
$this->displayThanksReminder = !class_exists(Thanks::class, false) && version_compare('1.1.0', PluginInterface::PLUGIN_API_VERSION, '<=') ? 2 : 0;
}
}
public function executeAutoScripts(Event $event)
{
$event->stopPropagation();
// force reloading scripts as we might have added and removed during this run
$json = new JsonFile(Factory::getComposerFile());
$jsonContents = $json->read();
$executor = new ScriptExecutor($this->composer, $this->io, $this->options);
foreach ($jsonContents['scripts']['auto-scripts'] as $cmd => $type) {
$executor->execute($type, $cmd);
}
$this->io->write($this->postInstallOutput);
$this->postInstallOutput = [];
}
public function populateProvidersCacheDir(InstallerEvent $event)
{
$listed = [];
$packages = [];
$pool = $event->getPool();
$pool = \Closure::bind(function () {
foreach ($this->providerRepos as $k => $repo) {
$this->providerRepos[$k] = new class($repo) extends BaseComposerRepository {
private $repo;
public function __construct($repo)
{
$this->repo = $repo;
}
public function whatProvides(Pool $pool, $name, $bypassFilters = false)
{
$packages = [];
foreach ($this->repo->whatProvides($pool, $name, $bypassFilters) as $k => $p) {
$packages[$k] = clone $p;
}
return $packages;
}
};
}
return $this;
}, clone $pool, $pool)();
foreach ($event->getRequest()->getJobs() as $job) {
if ('install' !== $job['cmd'] || false === strpos($job['packageName'], '/')) {
continue;
}
$listed[$job['packageName']] = true;
$packages[] = [$job['packageName'], $job['constraint']];
}
$loadExtraRepos = !(new \ReflectionMethod(Pool::class, 'match'))->isPublic(); // Detect Composer < 1.7.3
$this->rfs->download($packages, function ($packageName, $constraint) use (&$listed, &$packages, $pool, $loadExtraRepos) {
foreach ($pool->whatProvides($packageName, $constraint, true) as $package) {
$links = $loadExtraRepos ? array_merge($package->getRequires(), $package->getConflicts(), $package->getReplaces()) : $package->getRequires();
foreach ($links as $link) {
if (isset($listed[$link->getTarget()]) || false === strpos($link->getTarget(), '/')) {
continue;
}
$listed[$link->getTarget()] = true;
$packages[] = [$link->getTarget(), $link->getConstraint()];
}
}
});
}
public function populateFilesCacheDir(InstallerEvent $event)
{
if ($this->cacheDirPopulated || $this->dryRun) {
return;
}
$this->cacheDirPopulated = true;
$downloads = [];
$cacheDir = rtrim($this->config->get('cache-files-dir'), '\/').\DIRECTORY_SEPARATOR;
$getCacheKey = function (PackageInterface $package, $processedUrl) {
return $this->getCacheKey($package, $processedUrl);
};
$getCacheKey = \Closure::bind($getCacheKey, new FileDownloader($this->io, $this->config), FileDownloader::class);
foreach ($event->getOperations() as $op) {
if ('install' === $op->getJobType()) {
$package = $op->getPackage();
} elseif ('update' === $op->getJobType()) {
$package = $op->getTargetPackage();
} else {
continue;
}
if (!$fileUrl = $package->getDistUrl()) {
continue;
}
if ($package->getDistMirrors()) {
$fileUrl = current($package->getDistUrls());
}
if (!preg_match('/^https?:/', $fileUrl) || !$originUrl = parse_url($fileUrl, \PHP_URL_HOST)) {
continue;
}
if (file_exists($file = $cacheDir.$getCacheKey($package, $fileUrl))) {
continue;
}
@mkdir(\dirname($file), 0775, true);
if (!is_dir(\dirname($file))) {
continue;
}
if (preg_match('#^https://github\.com/#', $package->getSourceUrl()) && preg_match('#^https://api\.github\.com/repos(/[^/]++/[^/]++/)zipball(.++)$#', $fileUrl, $m)) {
$fileUrl = sprintf('https://codeload.github.com%slegacy.zip%s', $m[1], $m[2]);
}
$downloads[] = [$originUrl, $fileUrl, [], $file, false];
}
if (1 < \count($downloads)) {
$this->rfs->download($downloads, [$this->rfs, 'get'], false, $this->progress);
}
}
public function onFileDownload(PreFileDownloadEvent $event)
{
if ($event->getRemoteFilesystem() !== $this->rfs) {
$event->setRemoteFilesystem($this->rfs->setNextOptions($event->getRemoteFilesystem()->getOptions()));
}
}
/**
* @return Recipe[]
*/
public function fetchRecipes(array $operations, bool $reset): array
{
if (!$this->downloader->isEnabled()) {
$this->io->writeError('<warning>Symfony recipes are disabled: "symfony/flex" not found in the root composer.json</>');
return [];
}
$devPackages = null;
$data = $this->downloader->getRecipes($operations);
$manifests = $data['manifests'] ?? [];
$locks = $data['locks'] ?? [];
// symfony/flex recipes should always be applied first
$flexRecipe = [];
// symfony/framework-bundle recipe should always be applied first after the metapackages
$recipes = [
'symfony/framework-bundle' => null,
];
$metaRecipes = [];
foreach ($operations as $operation) {
if ($operation instanceof UpdateOperation) {
$package = $operation->getTargetPackage();
} else {
$package = $operation->getPackage();
}
// FIXME: Multi name with getNames()
$name = $package->getName();
$job = method_exists($operation, 'getOperationType') ? $operation->getOperationType() : $operation->getJobType();
if (!isset($manifests[$name]) && isset($data['conflicts'][$name])) {
$this->io->writeError(sprintf(' - Skipping recipe for %s: all versions of the recipe conflict with your package versions.', $name), true, IOInterface::VERBOSE);
continue;
}
while ($this->doesRecipeConflict($manifests[$name] ?? [], $operation)) {
$this->downloader->removeRecipeFromIndex($name, $manifests[$name]['version']);
$newData = $this->downloader->getRecipes([$operation]);
$newManifests = $newData['manifests'] ?? [];
if (!isset($newManifests[$name])) {
// no older recipe found
$this->io->writeError(sprintf(' - Skipping recipe for %s: all versions of the recipe conflict with your package versions.', $name), true, IOInterface::VERBOSE);
continue 2;
}
// push the "old" recipe into the $manifests
$manifests[$name] = $newManifests[$name];
$locks[$name] = $newData['locks'][$name];
}
if ($operation instanceof InstallOperation && isset($locks[$name])) {
$ref = $this->lock->get($name)['recipe']['ref'] ?? null;
if (!$reset && $ref && ($locks[$name]['recipe']['ref'] ?? null) === $ref) {
continue;
}
$this->lock->set($name, $locks[$name]);
} elseif ($operation instanceof UninstallOperation) {
if (!$this->lock->has($name)) {
continue;
}
$this->lock->remove($name);
}
if (isset($manifests[$name])) {
if ('metapackage' === $package->getType()) {
$metaRecipes[$name] = new Recipe($package, $name, $job, $manifests[$name], $locks[$name] ?? []);
} elseif ('symfony/flex' === $name) {
$flexRecipe = [$name => new Recipe($package, $name, $job, $manifests[$name], $locks[$name] ?? [])];
} else {
$recipes[$name] = new Recipe($package, $name, $job, $manifests[$name], $locks[$name] ?? []);
}
}
if (!isset($manifests[$name])) {
$bundles = [];
if (null === $devPackages) {
$devPackages = array_column($this->composer->getLocker()->getLockData()['packages-dev'], 'name');
}
$envs = \in_array($name, $devPackages) ? ['dev', 'test'] : ['all'];
$bundle = new SymfonyBundle($this->composer, $package, $job);
foreach ($bundle->getClassNames() as $bundleClass) {
$bundles[$bundleClass] = $envs;
}
if ($bundles) {
$manifest = [
'origin' => sprintf('%s:%s@auto-generated recipe', $name, $package->getPrettyVersion()),
'manifest' => ['bundles' => $bundles],
];
$recipes[$name] = new Recipe($package, $name, $job, $manifest);
}
}
}
return array_merge($flexRecipe, $metaRecipes, array_filter($recipes));
}
public function truncatePackages(PrePoolCreateEvent $event)
{
if (!$this->filter) {
return;
}
$rootPackage = $this->composer->getPackage();
$lockedPackages = $event->getRequest()->getFixedOrLockedPackages();
$event->setPackages($this->filter->removeLegacyPackages($event->getPackages(), $rootPackage, $lockedPackages));
}
public function getComposerJsonHash(): string
{
return md5_file(Factory::getComposerFile());
}
public function getLock(): Lock
{
if (null === $this->lock) {
throw new \Exception('Cannot access lock before calling activate().');
}
return $this->lock;
}
private function initOptions(): Options
{
$extra = $this->composer->getPackage()->getExtra();
$options = array_merge([
'bin-dir' => 'bin',
'conf-dir' => 'conf',
'config-dir' => 'config',
'src-dir' => 'src',
'var-dir' => 'var',
'public-dir' => 'public',
'root-dir' => $extra['symfony']['root-dir'] ?? '.',
'runtime' => $extra['runtime'] ?? [],
], $extra);
return new Options($options, $this->io);
}
private function formatOrigin(Recipe $recipe): string
{
if (method_exists($recipe, 'getFormattedOrigin')) {
return $recipe->getFormattedOrigin();
}
// BC with upgrading from flex < 1.18
$origin = $recipe->getOrigin();
// symfony/translation:3.3@github.com/symfony/recipes:branch
if (!preg_match('/^([^:]++):([^@]++)@(.+)$/', $origin, $matches)) {
return $origin;
}
return sprintf('<info>%s</> (<comment>>=%s</>): From %s', $matches[1], $matches[2], 'auto-generated recipe' === $matches[3] ? '<comment>'.$matches[3].'</>' : $matches[3]);
}
private function shouldRecordOperation(OperationInterface $operation, bool $isDevMode, Composer $composer = null): bool
{
if ($this->dryRun) {
return false;
}
if ($operation instanceof UpdateOperation) {
$package = $operation->getTargetPackage();
} else {
$package = $operation->getPackage();
}
// when Composer runs with --no-dev, ignore uninstall operations on packages from require-dev
if (!$isDevMode && $operation instanceof UninstallOperation) {
foreach (($composer ?? $this->composer)->getLocker()->getLockData()['packages-dev'] as $p) {
if ($package->getName() === $p['name']) {
return false;
}
}
}
// FIXME: Multi name with getNames()
$name = $package->getName();
if ($operation instanceof InstallOperation) {
if (!$this->lock->has($name)) {
return true;
}
} elseif ($operation instanceof UninstallOperation) {
return true;
}
return false;
}
private function populateRepoCacheDir()
{
$repos = [];
foreach ($this->composer->getPackage()->getRepositories() as $name => $repo) {
if (!isset($repo['type']) || 'composer' !== $repo['type'] || !empty($repo['force-lazy-providers'])) {
continue;
}
if (!preg_match('#^http(s\??)?://#', $repo['url'])) {
continue;
}
$repo = new ComposerRepository($repo, $this->io, $this->config, null, $this->rfs);
$repos[] = [$repo];
}
$this->rfs->download($repos, function ($repo) {
ParallelDownloader::$cacheNext = true;
$repo->getProviderNames();
});
}
private function updateComposerLock()
{
$lock = substr(Factory::getComposerFile(), 0, -4).'lock';
$composerJson = file_get_contents(Factory::getComposerFile());
$lockFile = new JsonFile($lock, null, $this->io);
if (version_compare('2.0.0', PluginInterface::PLUGIN_API_VERSION, '>')) {
$locker = new Locker($this->io, $lockFile, $this->composer->getRepositoryManager(), $this->composer->getInstallationManager(), $composerJson);
} else {
$locker = new Locker($this->io, $lockFile, $this->composer->getInstallationManager(), $composerJson);
}
$lockData = $locker->getLockData();
$lockData['content-hash'] = Locker::getContentHash($composerJson);
$lockFile->write($lockData);
}
private function unpack(Event $event)
{
$jsonPath = Factory::getComposerFile();
$json = JsonFile::parseJson(file_get_contents($jsonPath));
$sortPackages = $this->composer->getConfig()->get('sort-packages');
$unpackOp = new Operation(true, $sortPackages);
foreach (['require', 'require-dev'] as $type) {
foreach ($json[$type] ?? [] as $package => $constraint) {
$unpackOp->addPackage($package, $constraint, 'require-dev' === $type);
}
}
$unpacker = new Unpacker($this->composer, new PackageResolver($this->downloader), $this->dryRun);
$result = $unpacker->unpack($unpackOp);
if (!$result->getUnpacked()) {
return;
}
$this->io->writeError('<info>Unpacking Symfony packs</>');
foreach ($result->getUnpacked() as $pkg) {
$this->io->writeError(sprintf(' - Unpacked <info>%s</>', $pkg->getName()));
}
$unpacker->updateLock($result, $this->io);
$this->reinstall($event, false);
}
private function reinstall(Event $event, bool $update)
{
$event->stopPropagation();
$ed = $this->composer->getEventDispatcher();
$disableScripts = !method_exists($ed, 'setRunScripts') || !((array) $ed)["\0*\0runScripts"];
$composer = Factory::create($this->io, null, false, $disableScripts);
$installer = clone $this->installer;
$installer->__construct(
$this->io,
$composer->getConfig(),
$composer->getPackage(),
$composer->getDownloadManager(),
$composer->getRepositoryManager(),
$composer->getLocker(),
$composer->getInstallationManager(),
$composer->getEventDispatcher(),
$composer->getAutoloadGenerator()
);
if (!$update) {
$installer->setUpdateAllowList(['php']);
}
if (method_exists($installer, 'setSkipSuggest')) {
$installer->setSkipSuggest(true);
}
$installer->run();
$this->io->write($this->postInstallOutput);
$this->postInstallOutput = [];
}
public static function getSubscribedEvents(): array
{
if (!self::$activated) {
return [];
}
$events = [
ScriptEvents::POST_CREATE_PROJECT_CMD => 'configureProject',
ScriptEvents::POST_INSTALL_CMD => 'install',
ScriptEvents::PRE_UPDATE_CMD => 'configureInstaller',
ScriptEvents::POST_UPDATE_CMD => 'update',
'auto-scripts' => 'executeAutoScripts',
];
if (version_compare('2.0.0', PluginInterface::PLUGIN_API_VERSION, '>')) {
$events += [
PackageEvents::POST_PACKAGE_INSTALL => 'record',
PackageEvents::POST_PACKAGE_UPDATE => [['record'], ['enableThanksReminder']],
PackageEvents::POST_PACKAGE_UNINSTALL => 'record',
InstallerEvents::PRE_DEPENDENCIES_SOLVING => [['populateProvidersCacheDir', \PHP_INT_MAX]],
InstallerEvents::POST_DEPENDENCIES_SOLVING => [['populateFilesCacheDir', \PHP_INT_MAX]],
PackageEvents::PRE_PACKAGE_INSTALL => [['populateFilesCacheDir', ~\PHP_INT_MAX]],
PackageEvents::PRE_PACKAGE_UPDATE => [['populateFilesCacheDir', ~\PHP_INT_MAX]],
PluginEvents::PRE_FILE_DOWNLOAD => 'onFileDownload',
];
} else {
$events += [
PackageEvents::POST_PACKAGE_UPDATE => 'enableThanksReminder',
InstallerEvents::PRE_OPERATIONS_EXEC => 'recordOperations',
PluginEvents::PRE_POOL_CREATE => 'truncatePackages',
];
}
return $events;
}
private function doesRecipeConflict(array $recipeData, OperationInterface $operation): bool
{
if (empty($recipeData['manifest']['conflict']) || $operation instanceof UninstallOperation) {
return false;
}
$lockedRepository = $this->composer->getLocker()->getLockedRepository();
foreach ($recipeData['manifest']['conflict'] as $conflictingPackage => $constraint) {
if ($lockedRepository->findPackage($conflictingPackage, $constraint)) {
return true;
}
}
return false;
}
}

204
vendor/symfony/flex/src/GithubApi.php vendored Normal file
View File

@ -0,0 +1,204 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\Util\HttpDownloader;
use Composer\Util\RemoteFilesystem;
class GithubApi
{
/** @var HttpDownloader|RemoteFilesystem */
private $downloader;
public function __construct($downloader)
{
$this->downloader = $downloader;
}
/**
* Attempts to find data about when the recipe was installed.
*
* Returns an array containing:
* commit: The git sha of the last commit of the recipe
* date: The date of the commit
* new_commits: An array of commit sha's in this recipe's directory+version since the commit
* The key is the sha & the value is the date
*/
public function findRecipeCommitDataFromTreeRef(string $package, string $repo, string $branch, string $version, string $lockRef): ?array
{
$repositoryName = $this->getRepositoryName($repo);
if (!$repositoryName) {
return null;
}
$recipePath = sprintf('%s/%s', $package, $version);
$commitsData = $this->requestGitHubApi(sprintf(
'https://api.github.com/repos/%s/commits?path=%s&sha=%s',
$repositoryName,
$recipePath,
$branch
));
$commitShas = [];
foreach ($commitsData as $commitData) {
$commitShas[$commitData['sha']] = $commitData['commit']['committer']['date'];
// go back the commits one-by-one
$treeUrl = $commitData['commit']['tree']['url'].'?recursive=true';
// fetch the full tree, then look for the tree for the package path
$treeData = $this->requestGitHubApi($treeUrl);
foreach ($treeData['tree'] as $treeItem) {
if ($treeItem['path'] !== $recipePath) {
continue;
}
if ($treeItem['sha'] === $lockRef) {
// remove *this* commit from the new commits list
array_pop($commitShas);
return [
// shorten for brevity
'commit' => substr($commitData['sha'], 0, 7),
'date' => $commitData['commit']['committer']['date'],
'new_commits' => $commitShas,
];
}
}
}
return null;
}
public function getVersionsOfRecipe(string $repo, string $branch, string $recipePath): ?array
{
$repositoryName = $this->getRepositoryName($repo);
if (!$repositoryName) {
return null;
}
$url = sprintf(
'https://api.github.com/repos/%s/contents/%s?ref=%s',
$repositoryName,
$recipePath,
$branch
);
$contents = $this->requestGitHubApi($url);
$versions = [];
foreach ($contents as $fileData) {
if ('dir' !== $fileData['type']) {
continue;
}
$versions[] = $fileData['name'];
}
return $versions;
}
public function getCommitDataForPath(string $repo, string $path, string $branch): array
{
$repositoryName = $this->getRepositoryName($repo);
if (!$repositoryName) {
return [];
}
$commitsData = $this->requestGitHubApi(sprintf(
'https://api.github.com/repos/%s/commits?path=%s&sha=%s',
$repositoryName,
$path,
$branch
));
$data = [];
foreach ($commitsData as $commitData) {
$data[$commitData['sha']] = $commitData['commit']['committer']['date'];
}
return $data;
}
public function getPullRequestForCommit(string $commit, string $repo): ?array
{
$data = $this->requestGitHubApi('https://api.github.com/search/issues?q='.$commit);
if (0 === \count($data['items'])) {
return null;
}
$repositoryName = $this->getRepositoryName($repo);
if (!$repositoryName) {
return null;
}
$bestItem = null;
foreach ($data['items'] as $item) {
// make sure the PR referenced isn't from a different repository
if (false === strpos($item['html_url'], sprintf('%s/pull', $repositoryName))) {
continue;
}
if (null === $bestItem) {
$bestItem = $item;
continue;
}
// find the first PR to reference - avoids rare cases where an invalid
// PR that references *many* commits is first
// e.g. https://api.github.com/search/issues?q=a1a70353f64f405cfbacfc4ce860af623442d6e5
if ($item['number'] < $bestItem['number']) {
$bestItem = $item;
}
}
if (!$bestItem) {
return null;
}
return [
'number' => $bestItem['number'],
'url' => $bestItem['html_url'],
'title' => $bestItem['title'],
];
}
private function requestGitHubApi(string $path)
{
if ($this->downloader instanceof HttpDownloader) {
$contents = $this->downloader->get($path)->getBody();
} else {
$contents = $this->downloader->getContents('api.github.com', $path, false);
}
return json_decode($contents, true);
}
/**
* Converts the "repo" stored in symfony.lock to a repository name.
*
* For example: "github.com/symfony/recipes" => "symfony/recipes"
*/
private function getRepositoryName(string $repo): ?string
{
// only supports public repository placement
if (0 !== strpos($repo, 'github.com')) {
return null;
}
$parts = explode('/', $repo);
if (3 !== \count($parts)) {
return null;
}
return implode('/', [$parts[1], $parts[2]]);
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace Symfony\Flex;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\Package\PackageInterface;
/**
* @author Maxime Hélias <maximehelias16@gmail.com>
*/
class InformationOperation implements OperationInterface
{
private $package;
private $recipeRef = null;
private $version = null;
public function __construct(PackageInterface $package)
{
$this->package = $package;
}
/**
* Call to get information about a specific version of a recipe.
*
* Both $recipeRef and $version would normally come from the symfony.lock file.
*/
public function setSpecificRecipeVersion(string $recipeRef, string $version)
{
$this->recipeRef = $recipeRef;
$this->version = $version;
}
/**
* Returns package instance.
*
* @return PackageInterface
*/
public function getPackage()
{
return $this->package;
}
public function getRecipeRef(): ?string
{
return $this->recipeRef;
}
public function getVersion(): ?string
{
return $this->version;
}
public function getJobType()
{
return 'information';
}
/**
* {@inheritdoc}
*/
public function getOperationType()
{
return 'information';
}
/**
* {@inheritdoc}
*/
public function show($lock)
{
$pretty = method_exists($this->package, 'getFullPrettyVersion') ? $this->package->getFullPrettyVersion() : $this->formatVersion($this->package);
return 'Information '.$this->package->getPrettyName().' ('.$pretty.')';
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return $this->show(false);
}
/**
* Compatibility for Composer 1.x, not needed in Composer 2.
*/
public function getReason()
{
return null;
}
}

89
vendor/symfony/flex/src/Lock.php vendored Normal file
View File

@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\Json\JsonFile;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class Lock
{
private $json;
private $lock = [];
private $changed = false;
public function __construct($lockFile)
{
$this->json = new JsonFile($lockFile);
if ($this->json->exists()) {
$this->lock = $this->json->read();
}
}
public function has($name): bool
{
return \array_key_exists($name, $this->lock);
}
public function add($name, $data)
{
$current = $this->lock[$name] ?? [];
$this->lock[$name] = array_merge($current, $data);
$this->changed = true;
}
public function get($name)
{
return $this->lock[$name] ?? null;
}
public function set($name, $data)
{
if (!\array_key_exists($name, $this->lock) || $data !== $this->lock[$name]) {
$this->lock[$name] = $data;
$this->changed = true;
}
}
public function remove($name)
{
if (\array_key_exists($name, $this->lock)) {
unset($this->lock[$name]);
$this->changed = true;
}
}
public function write()
{
if (!$this->changed) {
return;
}
if ($this->lock) {
ksort($this->lock);
$this->json->write($this->lock);
} elseif ($this->json->exists()) {
@unlink($this->json->getPath());
}
}
public function delete()
{
@unlink($this->json->getPath());
}
public function all(): array
{
return $this->lock;
}
}

88
vendor/symfony/flex/src/Options.php vendored Normal file
View File

@ -0,0 +1,88 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\IO\IOInterface;
use Composer\Util\ProcessExecutor;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class Options
{
private $options;
private $writtenFiles = [];
private $io;
public function __construct(array $options = [], IOInterface $io = null)
{
$this->options = $options;
$this->io = $io;
}
public function get(string $name)
{
return $this->options[$name] ?? null;
}
public function expandTargetDir(string $target): string
{
return preg_replace_callback('{%(.+?)%}', function ($matches) {
$option = str_replace('_', '-', strtolower($matches[1]));
if (!isset($this->options[$option])) {
return $matches[0];
}
return rtrim($this->options[$option], '/');
}, $target);
}
public function shouldWriteFile(string $file, bool $overwrite): bool
{
if (isset($this->writtenFiles[$file])) {
return false;
}
$this->writtenFiles[$file] = true;
if (!file_exists($file)) {
return true;
}
if (!$overwrite) {
return false;
}
if (!filesize($file)) {
return true;
}
exec('git status --short --ignored --untracked-files=all -- '.ProcessExecutor::escape($file).' 2>&1', $output, $status);
if (0 !== $status) {
return $this->io && $this->io->askConfirmation(sprintf('Cannot determine the state of the "%s" file, overwrite anyway? [y/N] ', $file), false);
}
if (empty($output[0]) || preg_match('/^[ AMDRCU][ D][ \t]/', $output[0])) {
return true;
}
$name = basename($file);
$name = \strlen($output[0]) - \strlen($name) === strrpos($output[0], $name) ? substr($output[0], 3) : $name;
return $this->io && $this->io->askConfirmation(sprintf('File "%s" has uncommitted changes, overwrite? [y/N] ', $name), false);
}
public function toArray(): array
{
return $this->options;
}
}

View File

@ -0,0 +1,154 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\IO\IOInterface;
use Composer\Package\AliasPackage;
use Composer\Package\PackageInterface;
use Composer\Package\RootPackageInterface;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\Intervals;
use Composer\Semver\VersionParser;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class PackageFilter
{
private $versions;
private $versionParser;
private $symfonyRequire;
private $symfonyConstraints;
private $downloader;
private $io;
public function __construct(IOInterface $io, string $symfonyRequire, Downloader $downloader)
{
$this->versionParser = new VersionParser();
$this->symfonyRequire = $symfonyRequire;
$this->symfonyConstraints = $this->versionParser->parseConstraints($symfonyRequire);
$this->downloader = $downloader;
$this->io = $io;
}
/**
* @param PackageInterface[] $data
* @param PackageInterface[] $lockedPackages
*
* @return PackageInterface[]
*/
public function removeLegacyPackages(array $data, RootPackageInterface $rootPackage, array $lockedPackages): array
{
if (!$this->symfonyConstraints || !$data) {
return $data;
}
$lockedVersions = [];
foreach ($lockedPackages as $package) {
$lockedVersions[$package->getName()] = [$package->getVersion()];
if ($package instanceof AliasPackage) {
$lockedVersions[$package->getName()][] = $package->getAliasOf()->getVersion();
}
}
$rootConstraints = [];
foreach ($rootPackage->getRequires() + $rootPackage->getDevRequires() as $name => $link) {
$rootConstraints[$name] = $link->getConstraint();
}
$knownVersions = $this->getVersions();
$filteredPackages = [];
$symfonyPackages = [];
$oneSymfony = false;
foreach ($data as $package) {
$name = $package->getName();
$versions = [$package->getVersion()];
if ($package instanceof AliasPackage) {
$versions[] = $package->getAliasOf()->getVersion();
}
if ('symfony/symfony' !== $name && (
!isset($knownVersions['splits'][$name])
|| array_intersect($versions, $lockedVersions[$name] ?? [])
|| (isset($rootConstraints[$name]) && !Intervals::haveIntersections($this->symfonyConstraints, $rootConstraints[$name]))
)) {
$filteredPackages[] = $package;
continue;
}
if (null !== $alias = $package->getExtra()['branch-alias'][$package->getVersion()] ?? null) {
$versions[] = $this->versionParser->normalize($alias);
}
foreach ($versions as $version) {
if ($this->symfonyConstraints->matches(new Constraint('==', $version))) {
$filteredPackages[] = $package;
$oneSymfony = $oneSymfony || 'symfony/symfony' === $name;
continue 2;
}
}
if ('symfony/symfony' === $name) {
$symfonyPackages[] = $package;
} elseif (null !== $this->io) {
$this->io->writeError(sprintf('<info>Restricting packages listed in "symfony/symfony" to "%s"</>', $this->symfonyRequire));
$this->io = null;
}
}
if ($symfonyPackages && !$oneSymfony) {
$filteredPackages = array_merge($filteredPackages, $symfonyPackages);
}
return $filteredPackages;
}
private function getVersions(): array
{
if (null !== $this->versions) {
return $this->versions;
}
$versions = $this->downloader->getVersions();
$this->downloader = null;
$okVersions = [];
if (!isset($versions['splits'])) {
throw new \LogicException('The Flex index is missing a "splits" entry. Did you forget to add "flex://defaults" in the "extra.symfony.endpoint" array of your composer.json?');
}
foreach ($versions['splits'] as $name => $vers) {
foreach ($vers as $i => $v) {
if (!isset($okVersions[$v])) {
$okVersions[$v] = false;
$w = '.x' === substr($v, -2) ? $versions['next'] : $v;
for ($j = 0; $j < 60; ++$j) {
if ($this->symfonyConstraints->matches(new Constraint('==', $w.'.'.$j.'.0'))) {
$okVersions[$v] = true;
break;
}
}
}
if (!$okVersions[$v]) {
unset($vers[$i]);
}
}
if (!$vers || $vers === $versions['splits'][$name]) {
unset($versions['splits'][$name]);
}
}
return $this->versions = $versions;
}
}

View File

@ -0,0 +1,249 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\Json\JsonFile;
use Composer\Json\JsonManipulator;
use Seld\JsonLint\ParsingException;
/**
* Synchronize package.json files detected in installed PHP packages with
* the current application.
*/
class PackageJsonSynchronizer
{
private $rootDir;
private $vendorDir;
public function __construct(string $rootDir, string $vendorDir = 'vendor')
{
$this->rootDir = $rootDir;
$this->vendorDir = $vendorDir;
}
public function shouldSynchronize(): bool
{
return $this->rootDir && file_exists($this->rootDir.'/package.json');
}
public function synchronize(array $phpPackages): bool
{
try {
JsonFile::parseJson(file_get_contents($this->rootDir.'/package.json'));
} catch (ParsingException $e) {
// if package.json is invalid (possible during a recipe upgrade), we can't update the file
return false;
}
$didChangePackageJson = $this->removeObsoletePackageJsonLinks();
$dependencies = [];
foreach ($phpPackages as $k => $phpPackage) {
if (\is_string($phpPackage)) {
// support for smooth upgrades from older flex versions
$phpPackages[$k] = $phpPackage = [
'name' => $phpPackage,
'keywords' => ['symfony-ux'],
];
}
foreach ($this->resolvePackageDependencies($phpPackage) as $dependency => $constraint) {
$dependencies[$dependency][$phpPackage['name']] = $constraint;
}
}
$didChangePackageJson = $this->registerDependencies($dependencies) || $didChangePackageJson;
// Register controllers and entrypoints in controllers.json
$this->registerWebpackResources($phpPackages);
return $didChangePackageJson;
}
private function removeObsoletePackageJsonLinks(): bool
{
$didChangePackageJson = false;
$manipulator = new JsonManipulator(file_get_contents($this->rootDir.'/package.json'));
$content = json_decode($manipulator->getContents(), true);
$jsDependencies = $content['dependencies'] ?? [];
$jsDevDependencies = $content['devDependencies'] ?? [];
foreach (['dependencies' => $jsDependencies, 'devDependencies' => $jsDevDependencies] as $key => $packages) {
foreach ($packages as $name => $version) {
if ('@' !== $name[0] || 0 !== strpos($version, 'file:'.$this->vendorDir.'/') || false === strpos($version, '/assets')) {
continue;
}
if (file_exists($this->rootDir.'/'.substr($version, 5).'/package.json')) {
continue;
}
$manipulator->removeSubNode($key, $name);
$didChangePackageJson = true;
}
}
file_put_contents($this->rootDir.'/package.json', $manipulator->getContents());
return $didChangePackageJson;
}
private function resolvePackageDependencies($phpPackage): array
{
$dependencies = [];
if (!$packageJson = $this->resolvePackageJson($phpPackage)) {
return $dependencies;
}
$dependencies['@'.$phpPackage['name']] = 'file:'.substr($packageJson->getPath(), 1 + \strlen($this->rootDir), -13);
foreach ($packageJson->read()['peerDependencies'] ?? [] as $peerDependency => $constraint) {
$dependencies[$peerDependency] = $constraint;
}
return $dependencies;
}
private function registerDependencies(array $flexDependencies): bool
{
$didChangePackageJson = false;
$manipulator = new JsonManipulator(file_get_contents($this->rootDir.'/package.json'));
$content = json_decode($manipulator->getContents(), true);
foreach ($flexDependencies as $dependency => $constraints) {
if (1 !== \count($constraints) && 1 !== \count(array_count_values($constraints))) {
// If the flex packages have a colliding peer dependency, leave the resolution to the user
continue;
}
$constraint = array_shift($constraints);
$parentNode = isset($content['dependencies'][$dependency]) ? 'dependencies' : 'devDependencies';
if (!isset($content[$parentNode][$dependency])) {
$content['devDependencies'][$dependency] = $constraint;
$didChangePackageJson = true;
} elseif ($constraint !== $content[$parentNode][$dependency]) {
$content[$parentNode][$dependency] = $constraint;
$didChangePackageJson = true;
}
}
if ($didChangePackageJson) {
if (isset($content['dependencies'])) {
$manipulator->addMainKey('dependencies', $content['dependencies']);
}
if (isset($content['devDependencies'])) {
$devDependencies = $content['devDependencies'];
uksort($devDependencies, 'strnatcmp');
$manipulator->addMainKey('devDependencies', $devDependencies);
}
$newContents = $manipulator->getContents();
if ($newContents === file_get_contents($this->rootDir.'/package.json')) {
return false;
}
file_put_contents($this->rootDir.'/package.json', $manipulator->getContents());
}
return $didChangePackageJson;
}
private function registerWebpackResources(array $phpPackages)
{
if (!file_exists($controllersJsonPath = $this->rootDir.'/assets/controllers.json')) {
return;
}
$previousControllersJson = (new JsonFile($controllersJsonPath))->read();
$newControllersJson = [
'controllers' => [],
'entrypoints' => $previousControllersJson['entrypoints'],
];
foreach ($phpPackages as $phpPackage) {
if (!$packageJson = $this->resolvePackageJson($phpPackage)) {
continue;
}
$name = '@'.$phpPackage['name'];
foreach ($packageJson->read()['symfony']['controllers'] ?? [] as $controllerName => $defaultConfig) {
// If the package has just been added (no config), add the default config provided by the package
if (!isset($previousControllersJson['controllers'][$name][$controllerName])) {
$config = [];
$config['enabled'] = $defaultConfig['enabled'];
$config['fetch'] = $defaultConfig['fetch'] ?? 'eager';
if (isset($defaultConfig['autoimport'])) {
$config['autoimport'] = $defaultConfig['autoimport'];
}
$newControllersJson['controllers'][$name][$controllerName] = $config;
continue;
}
// Otherwise, the package exists: merge new config with user config
$previousConfig = $previousControllersJson['controllers'][$name][$controllerName];
$config = [];
$config['enabled'] = $previousConfig['enabled'];
$config['fetch'] = $previousConfig['fetch'] ?? 'eager';
if (isset($defaultConfig['autoimport'])) {
$config['autoimport'] = [];
// Use for each autoimport either the previous config if one existed or the default config otherwise
foreach ($defaultConfig['autoimport'] as $autoimport => $enabled) {
$config['autoimport'][$autoimport] = $previousConfig['autoimport'][$autoimport] ?? $enabled;
}
}
$newControllersJson['controllers'][$name][$controllerName] = $config;
}
foreach ($packageJson->read()['symfony']['entrypoints'] ?? [] as $entrypoint => $filename) {
if (!isset($newControllersJson['entrypoints'][$entrypoint])) {
$newControllersJson['entrypoints'][$entrypoint] = $filename;
}
}
}
file_put_contents($controllersJsonPath, json_encode($newControllersJson, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)."\n");
}
private function resolvePackageJson(array $phpPackage): ?JsonFile
{
$packageDir = $this->rootDir.'/'.$this->vendorDir.'/'.$phpPackage['name'];
if (!\in_array('symfony-ux', $phpPackage['keywords'] ?? [], true)) {
return null;
}
foreach (['/assets', '/Resources/assets', '/src/Resources/assets'] as $subdir) {
$packageJsonPath = $packageDir.$subdir.'/package.json';
if (!file_exists($packageJsonPath)) {
continue;
}
return new JsonFile($packageJsonPath);
}
return null;
}
}

View File

@ -0,0 +1,145 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\Factory;
use Composer\Package\Version\VersionParser;
use Composer\Repository\PlatformRepository;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class PackageResolver
{
private static $SYMFONY_VERSIONS = ['lts', 'previous', 'stable', 'next', 'dev'];
private $downloader;
public function __construct(Downloader $downloader)
{
$this->downloader = $downloader;
}
public function resolve(array $arguments = [], bool $isRequire = false): array
{
// first pass split on : and = to resolve package names
$packages = [];
foreach ($arguments as $i => $argument) {
if ((false !== $pos = strpos($argument, ':')) || (false !== $pos = strpos($argument, '='))) {
$package = $this->resolvePackageName(substr($argument, 0, $pos), $i);
$version = substr($argument, $pos + 1);
$packages[] = $package.':'.$version;
} else {
$packages[] = $this->resolvePackageName($argument, $i);
}
}
// second pass to resolve versions
$versionParser = new VersionParser();
$requires = [];
foreach ($versionParser->parseNameVersionPairs($packages) as $package) {
$requires[] = $package['name'].$this->parseVersion($package['name'], $package['version'] ?? '', $isRequire);
}
return array_unique($requires);
}
public function parseVersion(string $package, string $version, bool $isRequire): string
{
if (0 !== strpos($package, 'symfony/')) {
return $version ? ':'.$version : '';
}
$versions = $this->downloader->getVersions();
if (!isset($versions['splits'][$package])) {
return $version ? ':'.$version : '';
}
if (!$version || '*' === $version) {
try {
$config = @json_decode(file_get_contents(Factory::getComposerFile()), true);
} finally {
if (!$isRequire || !(isset($config['extra']['symfony']['require']) || isset($config['require']['symfony/framework-bundle']))) {
return '';
}
}
$version = $config['extra']['symfony']['require'] ?? $config['require']['symfony/framework-bundle'];
} elseif ('dev' === $version) {
$version = '^'.$versions['dev-name'].'@dev';
} elseif ('next' === $version) {
$version = '^'.$versions[$version].'@dev';
} elseif (\in_array($version, self::$SYMFONY_VERSIONS, true)) {
$version = '^'.$versions[$version];
}
return ':'.$version;
}
private function resolvePackageName(string $argument, int $position): string
{
if (false !== strpos($argument, '/') || preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $argument) || preg_match('{(?<=[a-z0-9_/-])\*|\*(?=[a-z0-9_/-])}i', $argument) || \in_array($argument, ['lock', 'mirrors', 'nothing', ''])) {
return $argument;
}
$aliases = $this->downloader->getAliases();
if (isset($aliases[$argument])) {
$argument = $aliases[$argument];
} else {
// is it a version or an alias that does not exist?
try {
$versionParser = new VersionParser();
$versionParser->parseConstraints($argument);
} catch (\UnexpectedValueException $e) {
// is it a special Symfony version?
if (!\in_array($argument, self::$SYMFONY_VERSIONS, true)) {
$this->throwAlternatives($argument, $position);
}
}
}
return $argument;
}
/**
* @throws \UnexpectedValueException
*/
private function throwAlternatives(string $argument, int $position)
{
$alternatives = [];
foreach ($this->downloader->getAliases() as $alias => $package) {
$lev = levenshtein($argument, $alias);
if ($lev <= \strlen($argument) / 3 || ('' !== $argument && false !== strpos($alias, $argument))) {
$alternatives[$package][] = $alias;
}
}
// First position can only be a package name, not a version
if ($alternatives || 0 === $position) {
$message = sprintf('"%s" is not a valid alias.', $argument);
if ($alternatives) {
if (1 === \count($alternatives)) {
$message .= " Did you mean this:\n";
} else {
$message .= " Did you mean one of these:\n";
}
foreach ($alternatives as $package => $aliases) {
$message .= sprintf(" \"%s\", supported aliases: \"%s\"\n", $package, implode('", "', $aliases));
}
}
} else {
$message = sprintf('Could not parse version constraint "%s".', $argument);
}
throw new \UnexpectedValueException($message);
}
}

View File

@ -0,0 +1,287 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\Config;
use Composer\Downloader\TransportException;
use Composer\IO\IOInterface;
use Composer\Util\RemoteFilesystem;
/**
* Speedup Composer by downloading packages in parallel.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class ParallelDownloader extends RemoteFilesystem
{
private $io;
private $downloader;
private $quiet = true;
private $progress = true;
private $nextCallback;
private $downloadCount;
private $nextOptions = [];
private $sharedState;
private $fileName;
private $lastHeaders;
public static $cacheNext = false;
protected static $cache = [];
public function __construct(IOInterface $io, Config $config, array $options = [], $disableTls = false)
{
$this->io = $io;
if (!method_exists(parent::class, 'getRemoteContents')) {
$this->io->writeError('Composer >=1.7 not found, downloads will happen in sequence', true, IOInterface::DEBUG);
} elseif (!\extension_loaded('curl')) {
$this->io->writeError('ext-curl not found, downloads will happen in sequence', true, IOInterface::DEBUG);
} else {
$this->downloader = new CurlDownloader();
}
parent::__construct($io, $config, $options, $disableTls);
}
public function download(array &$nextArgs, callable $nextCallback, bool $quiet = true, bool $progress = true)
{
$previousState = [$this->quiet, $this->progress, $this->downloadCount, $this->nextCallback, $this->sharedState];
$this->quiet = $quiet;
$this->progress = $progress;
$this->downloadCount = \count($nextArgs);
$this->nextCallback = $nextCallback;
$this->sharedState = (object) [
'bytesMaxCount' => 0,
'bytesMax' => 0,
'bytesTransferred' => 0,
'nextArgs' => &$nextArgs,
'nestingLevel' => 0,
'maxNestingReached' => false,
'lastProgress' => 0,
'lastUpdate' => microtime(true),
];
if (!$this->quiet) {
if (!$this->downloader && method_exists(parent::class, 'getRemoteContents')) {
$this->io->writeError('<warning>Enable the "cURL" PHP extension for faster downloads</>');
}
$note = '';
if ($this->io->isDecorated()) {
$note = '\\' === \DIRECTORY_SEPARATOR ? '' : (false !== stripos(\PHP_OS, 'darwin') ? '🎵' : '🎶');
$note .= $this->downloader ? ('\\' !== \DIRECTORY_SEPARATOR ? ' 💨' : '') : '';
}
$this->io->writeError('');
$this->io->writeError(sprintf('<info>Prefetching %d packages</> %s', $this->downloadCount, $note));
$this->io->writeError(' - Downloading', false);
if ($this->progress) {
$this->io->writeError(' (<comment>0%</>)', false);
}
}
try {
$this->getNext();
if ($this->quiet) {
// no-op
} elseif ($this->progress) {
$this->io->overwriteError(' (<comment>100%</>)');
} else {
$this->io->writeError(' (<comment>100%</>)');
}
} finally {
if (!$this->quiet) {
$this->io->writeError('');
}
list($this->quiet, $this->progress, $this->downloadCount, $this->nextCallback, $this->sharedState) = $previousState;
}
}
public function getOptions()
{
$options = array_replace_recursive(parent::getOptions(), $this->nextOptions);
$this->nextOptions = [];
return $options;
}
public function setNextOptions(array $options)
{
$this->nextOptions = parent::getOptions() !== $options ? $options : [];
return $this;
}
/**
* {@inheritdoc}
*/
public function getLastHeaders()
{
return $this->lastHeaders ?? parent::getLastHeaders();
}
/**
* {@inheritdoc}
*/
public function copy($originUrl, $fileUrl, $fileName, $progress = true, $options = [])
{
$options = array_replace_recursive($this->nextOptions, $options);
$this->nextOptions = [];
$rfs = clone $this;
$rfs->fileName = $fileName;
$rfs->progress = $this->progress && $progress;
try {
return $rfs->get($originUrl, $fileUrl, $options, $fileName, $rfs->progress);
} finally {
$rfs->lastHeaders = null;
$this->lastHeaders = $rfs->getLastHeaders();
}
}
/**
* {@inheritdoc}
*/
public function getContents($originUrl, $fileUrl, $progress = true, $options = [])
{
return $this->copy($originUrl, $fileUrl, null, $progress, $options);
}
/**
* @internal
*/
public function callbackGet($notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax, $nativeDownload = true)
{
if (!$nativeDownload && \STREAM_NOTIFY_SEVERITY_ERR === $severity) {
throw new TransportException($message, $messageCode);
}
parent::callbackGet($notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax);
if (!$state = $this->sharedState) {
return;
}
if (\STREAM_NOTIFY_FILE_SIZE_IS === $notificationCode) {
++$state->bytesMaxCount;
$state->bytesMax += $bytesMax;
}
if (!$bytesMax || \STREAM_NOTIFY_PROGRESS !== $notificationCode) {
if ($state->nextArgs && !$nativeDownload) {
$this->getNext();
}
return;
}
if (0 < $state->bytesMax) {
$progress = $state->bytesMaxCount / $this->downloadCount;
$progress *= 100 * ($state->bytesTransferred + $bytesTransferred) / $state->bytesMax;
} else {
$progress = 0;
}
if ($bytesTransferred === $bytesMax) {
$state->bytesTransferred += $bytesMax;
}
if (null !== $state->nextArgs && !$this->quiet && $this->progress && 1 <= $progress - $state->lastProgress) {
$progressTime = microtime(true);
if (5 <= $progress - $state->lastProgress || 1 <= $progressTime - $state->lastUpdate) {
$state->lastProgress = $progress;
$this->io->overwriteError(sprintf(' (<comment>%d%%</>)', $progress), false);
$state->lastUpdate = microtime(true);
}
}
if (!$nativeDownload || !$state->nextArgs || $bytesTransferred === $bytesMax || $state->maxNestingReached) {
return;
}
if (5 < $state->nestingLevel) {
$state->maxNestingReached = true;
} else {
$this->getNext();
}
}
/**
* {@inheritdoc}
*/
protected function getRemoteContents($originUrl, $fileUrl, $context, array &$responseHeaders = null, $maxFileSize = null)
{
if (isset(self::$cache[$fileUrl])) {
self::$cacheNext = false;
$result = self::$cache[$fileUrl];
if (3 < \func_num_args()) {
list($responseHeaders, $result) = $result;
}
return $result;
}
if (self::$cacheNext) {
self::$cacheNext = false;
if (3 < \func_num_args()) {
$result = $this->getRemoteContents($originUrl, $fileUrl, $context, $responseHeaders, $maxFileSize);
self::$cache[$fileUrl] = [$responseHeaders, $result];
} else {
$result = $this->getRemoteContents($originUrl, $fileUrl, $context);
self::$cache[$fileUrl] = $result;
}
return $result;
}
if (!$this->downloader || !preg_match('/^https?:/', $fileUrl)) {
return parent::getRemoteContents($originUrl, $fileUrl, $context, $responseHeaders, $maxFileSize);
}
try {
$result = $this->downloader->get($originUrl, $fileUrl, $context, $this->fileName);
if (3 < \func_num_args()) {
list($responseHeaders, $result) = $result;
}
return $result;
} catch (TransportException $e) {
$this->io->writeError('Retrying download: '.$e->getMessage(), true, IOInterface::DEBUG);
return parent::getRemoteContents($originUrl, $fileUrl, $context, $responseHeaders, $maxFileSize);
} catch (\Throwable $e) {
$responseHeaders = [];
throw $e;
}
}
private function getNext()
{
$state = $this->sharedState;
++$state->nestingLevel;
try {
while ($state->nextArgs && (!$state->maxNestingReached || 1 === $state->nestingLevel)) {
try {
$state->maxNestingReached = false;
($this->nextCallback)(...array_shift($state->nextArgs));
} catch (TransportException $e) {
$this->io->writeError('Skipping download: '.$e->getMessage(), true, IOInterface::DEBUG);
}
}
} finally {
--$state->nestingLevel;
}
}
}

41
vendor/symfony/flex/src/Path.php vendored Normal file
View File

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
/**
* @internal
*/
class Path
{
private $workingDirectory;
public function __construct($workingDirectory)
{
$this->workingDirectory = $workingDirectory;
}
public function relativize(string $absolutePath): string
{
$relativePath = str_replace($this->workingDirectory, '.', $absolutePath);
return is_dir($absolutePath) ? rtrim($relativePath, '/').'/' : $relativePath;
}
public function concatenate(array $parts): string
{
$first = array_shift($parts);
return array_reduce($parts, function (string $initial, string $next): string {
return rtrim($initial, '/').'/'.ltrim($next, '/');
}, $first);
}
}

123
vendor/symfony/flex/src/Recipe.php vendored Normal file
View File

@ -0,0 +1,123 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\Package\PackageInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class Recipe
{
private $package;
private $name;
private $job;
private $data;
private $lock;
public function __construct(PackageInterface $package, string $name, string $job, array $data, array $lock = [])
{
$this->package = $package;
$this->name = $name;
$this->job = $job;
$this->data = $data;
$this->lock = $lock;
}
public function getPackage(): PackageInterface
{
return $this->package;
}
public function getName(): string
{
return $this->name;
}
public function getJob(): string
{
return $this->job;
}
public function getManifest(): array
{
if (!isset($this->data['manifest'])) {
throw new \LogicException(sprintf('Manifest is not available for recipe "%s".', $this->name));
}
return $this->data['manifest'];
}
public function getFiles(): array
{
return $this->data['files'] ?? [];
}
public function getOrigin(): string
{
return $this->data['origin'] ?? '';
}
public function getFormattedOrigin(): string
{
if (!$this->getOrigin()) {
return '';
}
// symfony/translation:3.3@github.com/symfony/recipes:branch
if (!preg_match('/^([^:]++):([^@]++)@(.+)$/', $this->getOrigin(), $matches)) {
return $this->getOrigin();
}
return sprintf('<info>%s</> (<comment>>=%s</>): From %s', $matches[1], $matches[2], 'auto-generated recipe' === $matches[3] ? '<comment>'.$matches[3].'</>' : $matches[3]);
}
public function getURL(): string
{
if (!$this->data['origin']) {
return '';
}
// symfony/translation:3.3@github.com/symfony/recipes:branch
if (!preg_match('/^([^:]++):([^@]++)@([^:]++):(.+)$/', $this->data['origin'], $matches)) {
// that excludes auto-generated recipes, which is what we want
return '';
}
return sprintf('https://%s/tree/%s/%s/%s', $matches[3], $matches[4], $matches[1], $matches[2]);
}
public function isContrib(): bool
{
return $this->data['is_contrib'] ?? false;
}
public function getRef()
{
return $this->lock['recipe']['ref'] ?? null;
}
public function isAuto(): bool
{
return !isset($this->lock['recipe']);
}
public function getVersion(): string
{
return $this->lock['recipe']['version'] ?? $this->lock['version'];
}
public function getLock(): array
{
return $this->lock;
}
}

87
vendor/symfony/flex/src/Response.php vendored Normal file
View File

@ -0,0 +1,87 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class Response implements \JsonSerializable
{
private $body;
private $origHeaders;
private $headers;
private $code;
/**
* @param mixed $body The response as JSON
*/
public function __construct($body, array $headers = [], int $code = 200)
{
$this->body = $body;
$this->origHeaders = $headers;
$this->headers = $this->parseHeaders($headers);
$this->code = $code;
}
public function getStatusCode(): int
{
return $this->code;
}
public function getHeader(string $name): string
{
return $this->headers[strtolower($name)][0] ?? '';
}
public function getHeaders(string $name): array
{
return $this->headers[strtolower($name)] ?? [];
}
public function getBody()
{
return $this->body;
}
public function getOrigHeaders(): array
{
return $this->origHeaders;
}
public static function fromJson(array $json): self
{
$response = new self($json['body']);
$response->headers = $json['headers'];
return $response;
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return ['body' => $this->body, 'headers' => $this->headers];
}
private function parseHeaders(array $headers): array
{
$values = [];
foreach (array_reverse($headers) as $header) {
if (preg_match('{^([^:]++):\s*(.+?)\s*$}i', $header, $match)) {
$values[strtolower($match[1])][] = $match[2];
} elseif (preg_match('{^HTTP/}i', $header)) {
break;
}
}
return $values;
}
}

View File

@ -0,0 +1,135 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\Composer;
use Composer\EventDispatcher\ScriptExecutionException;
use Composer\IO\IOInterface;
use Composer\Semver\Constraint\EmptyConstraint;
use Composer\Semver\Constraint\MatchAllConstraint;
use Composer\Util\ProcessExecutor;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\StreamOutput;
use Symfony\Component\Process\PhpExecutableFinder;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class ScriptExecutor
{
private $composer;
private $io;
private $options;
private $executor;
public function __construct(Composer $composer, IOInterface $io, Options $options, ProcessExecutor $executor = null)
{
$this->composer = $composer;
$this->io = $io;
$this->options = $options;
$this->executor = $executor ?: new ProcessExecutor();
}
/**
* @throws ScriptExecutionException if the executed command returns a non-0 exit code
*/
public function execute(string $type, string $cmd)
{
$parsedCmd = $this->options->expandTargetDir($cmd);
if (null === $expandedCmd = $this->expandCmd($type, $parsedCmd)) {
return;
}
$cmdOutput = new StreamOutput(fopen('php://temp', 'rw'), OutputInterface::VERBOSITY_VERBOSE, $this->io->isDecorated());
$outputHandler = function ($type, $buffer) use ($cmdOutput) {
$cmdOutput->write($buffer, false, OutputInterface::OUTPUT_RAW);
};
$this->io->writeError(sprintf('Executing script %s', $parsedCmd), $this->io->isVerbose());
$exitCode = $this->executor->execute($expandedCmd, $outputHandler);
$code = 0 === $exitCode ? ' <info>[OK]</>' : ' <error>[KO]</>';
if ($this->io->isVerbose()) {
$this->io->writeError(sprintf('Executed script %s %s', $cmd, $code));
} else {
$this->io->writeError($code);
}
if (0 !== $exitCode) {
$this->io->writeError(' <error>[KO]</>');
$this->io->writeError(sprintf('<error>Script %s returned with error code %s</>', $cmd, $exitCode));
fseek($cmdOutput->getStream(), 0);
foreach (explode("\n", stream_get_contents($cmdOutput->getStream())) as $line) {
$this->io->writeError('!! '.$line);
}
throw new ScriptExecutionException($cmd, $exitCode);
}
}
private function expandCmd(string $type, string $cmd)
{
switch ($type) {
case 'symfony-cmd':
return $this->expandSymfonyCmd($cmd);
case 'php-script':
return $this->expandPhpScript($cmd);
case 'script':
return $cmd;
default:
throw new \InvalidArgumentException(sprintf('Invalid symfony/flex auto-script in composer.json: "%s" is not a valid type of command.', $type));
}
}
private function expandSymfonyCmd(string $cmd)
{
$repo = $this->composer->getRepositoryManager()->getLocalRepository();
if (!$repo->findPackage('symfony/console', class_exists(MatchAllConstraint::class) ? new MatchAllConstraint() : new EmptyConstraint())) {
$this->io->writeError(sprintf('<warning>Skipping "%s" (needs symfony/console to run).</>', $cmd));
return null;
}
$console = ProcessExecutor::escape($this->options->get('root-dir').'/'.$this->options->get('bin-dir').'/console');
if ($this->io->isDecorated()) {
$console .= ' --ansi';
}
return $this->expandPhpScript($console.' '.$cmd);
}
private function expandPhpScript(string $cmd): string
{
$phpFinder = new PhpExecutableFinder();
if (!$php = $phpFinder->find(false)) {
throw new \RuntimeException('The PHP executable could not be found, add it to your PATH and try again.');
}
$arguments = $phpFinder->findArguments();
if ($env = (string) (getenv('COMPOSER_ORIGINAL_INIS'))) {
$paths = explode(\PATH_SEPARATOR, $env);
$ini = array_shift($paths);
} else {
$ini = php_ini_loaded_file();
}
if ($ini) {
$arguments[] = '--php-ini='.$ini;
}
$phpArgs = implode(' ', array_map([ProcessExecutor::class, 'escape'], $arguments));
return ProcessExecutor::escape($php).($phpArgs ? ' '.$phpArgs : '').' '.$cmd;
}
}

View File

@ -0,0 +1,112 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\Composer;
use Composer\Package\PackageInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class SymfonyBundle
{
private $package;
private $operation;
private $vendorDir;
public function __construct(Composer $composer, PackageInterface $package, string $operation)
{
$this->package = $package;
$this->operation = $operation;
$this->vendorDir = rtrim($composer->getConfig()->get('vendor-dir'), '/');
}
public function getClassNames(): array
{
$uninstall = 'uninstall' === $this->operation;
$classes = [];
$autoload = $this->package->getAutoload();
$isSyliusPlugin = 'sylius-plugin' === $this->package->getType();
foreach (['psr-4' => true, 'psr-0' => false] as $psr => $isPsr4) {
if (!isset($autoload[$psr])) {
continue;
}
foreach ($autoload[$psr] as $namespace => $paths) {
if (!\is_array($paths)) {
$paths = [$paths];
}
foreach ($paths as $path) {
foreach ($this->extractClassNames($namespace, $isSyliusPlugin) as $class) {
// we only check class existence on install as we do have the code available
// in contrast to uninstall operation
if (!$uninstall && !$this->isBundleClass($class, $path, $isPsr4)) {
continue;
}
$classes[] = $class;
}
}
}
}
return $classes;
}
private function extractClassNames(string $namespace, bool $isSyliusPlugin): array
{
$namespace = trim($namespace, '\\');
$class = $namespace.'\\';
$parts = explode('\\', $namespace);
$suffix = $parts[\count($parts) - 1];
$endOfWord = substr($suffix, -6);
if ($isSyliusPlugin) {
if ('Bundle' !== $endOfWord && 'Plugin' !== $endOfWord) {
$suffix .= 'Bundle';
}
} elseif ('Bundle' !== $endOfWord) {
$suffix .= 'Bundle';
}
$classes = [$class.$suffix];
$acc = '';
foreach (\array_slice($parts, 0, -1) as $part) {
if ('Bundle' === $part || ($isSyliusPlugin && 'Plugin' === $part)) {
continue;
}
$classes[] = $class.$part.$suffix;
$acc .= $part;
$classes[] = $class.$acc.$suffix;
}
return array_unique($classes);
}
private function isBundleClass(string $class, string $path, bool $isPsr4): bool
{
$classPath = ($this->vendorDir ? $this->vendorDir.'/' : '').$this->package->getPrettyName().'/'.$path.'/';
$parts = explode('\\', $class);
$class = $parts[\count($parts) - 1];
if (!$isPsr4) {
$classPath .= str_replace('\\', '', implode('/', \array_slice($parts, 0, -1))).'/';
}
$classPath .= str_replace('\\', '/', $class).'.php';
if (!file_exists($classPath)) {
return false;
}
// heuristic that should work in almost all cases
return false !== strpos(file_get_contents($classPath), 'Symfony\Component\HttpKernel\Bundle\Bundle');
}
}

View File

@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\Config;
use Composer\EventDispatcher\EventDispatcher;
use Composer\IO\IOInterface;
use Composer\Package\RootPackageInterface;
use Composer\Repository\ComposerRepository as BaseComposerRepository;
use Composer\Util\RemoteFilesystem;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class TruncatedComposerRepository extends BaseComposerRepository
{
public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null)
{
parent::__construct($repoConfig, $io, $config, $eventDispatcher, $rfs);
$this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$');
}
public function setSymfonyRequire(string $symfonyRequire, RootPackageInterface $rootPackage, Downloader $downloader, IOInterface $io)
{
$this->cache->setSymfonyRequire($symfonyRequire, $rootPackage, $downloader, $io);
}
protected function fetchFile($filename, $cacheKey = null, $sha256 = null, $storeLastModifiedTime = false)
{
$data = parent::fetchFile($filename, $cacheKey, $sha256, $storeLastModifiedTime);
return \is_array($data) ? $this->cache->removeLegacyTags($data) : $data;
}
}

View File

@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Unpack;
class Operation
{
private $packages = [];
private $unpack;
private $sort;
public function __construct(bool $unpack, bool $sort)
{
$this->unpack = $unpack;
$this->sort = $sort;
}
public function addPackage(string $name, string $version, bool $dev)
{
$this->packages[] = [
'name' => $name,
'version' => $version,
'dev' => $dev,
];
}
public function getPackages(): array
{
return $this->packages;
}
public function shouldUnpack(): bool
{
return $this->unpack;
}
public function shouldSort(): bool
{
return $this->sort;
}
}

View File

@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Unpack;
use Composer\Package\PackageInterface;
class Result
{
private $unpacked = [];
private $required = [];
public function addUnpacked(PackageInterface $package): bool
{
$name = $package->getName();
if (!isset($this->unpacked[$name])) {
$this->unpacked[$name] = $package;
return true;
}
return false;
}
/**
* @return PackageInterface[]
*/
public function getUnpacked(): array
{
return $this->unpacked;
}
public function addRequired(string $package)
{
$this->required[] = $package;
}
/**
* @return string[]
*/
public function getRequired(): array
{
// we need at least one package for the command to work properly
return $this->required ?: ['symfony/flex'];
}
}

215
vendor/symfony/flex/src/Unpacker.php vendored Normal file
View File

@ -0,0 +1,215 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex;
use Composer\Composer;
use Composer\Config\JsonConfigSource;
use Composer\DependencyResolver\Pool;
use Composer\Factory;
use Composer\IO\IOInterface;
use Composer\Json\JsonFile;
use Composer\Json\JsonManipulator;
use Composer\Package\Locker;
use Composer\Package\Version\VersionSelector;
use Composer\Plugin\PluginInterface;
use Composer\Repository\CompositeRepository;
use Composer\Repository\RepositorySet;
use Composer\Semver\VersionParser;
use Symfony\Flex\Unpack\Operation;
use Symfony\Flex\Unpack\Result;
class Unpacker
{
private $composer;
private $resolver;
private $dryRun;
private $versionParser;
public function __construct(Composer $composer, PackageResolver $resolver, bool $dryRun)
{
$this->composer = $composer;
$this->resolver = $resolver;
$this->dryRun = $dryRun;
$this->versionParser = new VersionParser();
}
public function unpack(Operation $op, Result $result = null, &$links = [], bool $devRequire = false): Result
{
if (null === $result) {
$result = new Result();
}
$localRepo = $this->composer->getRepositoryManager()->getLocalRepository();
foreach ($op->getPackages() as $package) {
$pkg = $localRepo->findPackage($package['name'], '*');
$pkg = $pkg ?? $this->composer->getRepositoryManager()->findPackage($package['name'], $package['version'] ?: '*');
// not unpackable or no --unpack flag or empty packs (markers)
if (
null === $pkg ||
'symfony-pack' !== $pkg->getType() ||
!$op->shouldUnpack() ||
0 === \count($pkg->getRequires()) + \count($pkg->getDevRequires())
) {
$result->addRequired($package['name'].($package['version'] ? ':'.$package['version'] : ''));
continue;
}
if (!$result->addUnpacked($pkg)) {
continue;
}
$requires = [];
foreach ($pkg->getRequires() as $link) {
$requires[$link->getTarget()] = $link;
}
$devRequires = $pkg->getDevRequires();
foreach ($devRequires as $i => $link) {
if (!isset($requires[$link->getTarget()])) {
throw new \RuntimeException(sprintf('Symfony pack "%s" must duplicate all entries from "require-dev" into "require" but entry "%s" was not found.', $package['name'], $link->getTarget()));
}
$devRequires[$i] = $requires[$link->getTarget()];
unset($requires[$link->getTarget()]);
}
$versionSelector = null;
foreach ([$requires, $devRequires] as $dev => $requires) {
$dev = $dev ?: $devRequire ?: $package['dev'];
foreach ($requires as $link) {
if ('php' === $linkName = $link->getTarget()) {
continue;
}
$constraint = $link->getPrettyConstraint();
$constraint = substr($this->resolver->parseVersion($linkName, $constraint, true), 1) ?: $constraint;
if ($subPkg = $localRepo->findPackage($linkName, '*')) {
if ('symfony-pack' === $subPkg->getType()) {
$subOp = new Operation(true, $op->shouldSort());
$subOp->addPackage($subPkg->getName(), $constraint, $dev);
$result = $this->unpack($subOp, $result, $links, $dev);
continue;
}
if ('*' === $constraint) {
if (null === $versionSelector) {
$pool = class_exists(RepositorySet::class) ? RepositorySet::class : Pool::class;
$pool = new $pool($this->composer->getPackage()->getMinimumStability(), $this->composer->getPackage()->getStabilityFlags());
$pool->addRepository(new CompositeRepository($this->composer->getRepositoryManager()->getRepositories()));
$versionSelector = new VersionSelector($pool);
}
$constraint = $versionSelector->findRecommendedRequireVersion($subPkg);
}
}
$linkType = $dev ? 'require-dev' : 'require';
$constraint = $this->versionParser->parseConstraints($constraint);
if (isset($links[$linkName])) {
$links[$linkName]['constraints'][] = $constraint;
if ('require' === $linkType) {
$links[$linkName]['type'] = 'require';
}
} else {
$links[$linkName] = [
'type' => $linkType,
'name' => $linkName,
'constraints' => [$constraint],
];
}
}
}
}
if ($this->dryRun || 1 < \func_num_args()) {
return $result;
}
$jsonPath = Factory::getComposerFile();
$jsonContent = file_get_contents($jsonPath);
$jsonStored = json_decode($jsonContent, true);
$jsonManipulator = new JsonManipulator($jsonContent);
foreach ($links as $link) {
// nothing to do, package is already present in the "require" section
if (isset($jsonStored['require'][$link['name']])) {
continue;
}
if (isset($jsonStored['require-dev'][$link['name']])) {
// nothing to do, package is already present in the "require-dev" section
if ('require-dev' === $link['type']) {
continue;
}
// removes package from "require-dev", because it will be moved to "require"
// save stored constraint
$link['constraints'][] = $this->versionParser->parseConstraints($jsonStored['require-dev'][$link['name']]);
$jsonManipulator->removeSubNode('require-dev', $link['name']);
}
$constraint = end($link['constraints']);
if (!$jsonManipulator->addLink($link['type'], $link['name'], $constraint->getPrettyString(), $op->shouldSort())) {
throw new \RuntimeException(sprintf('Unable to unpack package "%s".', $link['name']));
}
}
file_put_contents($jsonPath, $jsonManipulator->getContents());
return $result;
}
public function updateLock(Result $result, IOInterface $io): void
{
$json = new JsonFile(Factory::getComposerFile());
$manipulator = new JsonConfigSource($json);
$locker = $this->composer->getLocker();
$lockData = $locker->getLockData();
foreach ($result->getUnpacked() as $package) {
$manipulator->removeLink('require-dev', $package->getName());
foreach ($lockData['packages-dev'] as $i => $pkg) {
if ($package->getName() === $pkg['name']) {
unset($lockData['packages-dev'][$i]);
}
}
$manipulator->removeLink('require', $package->getName());
foreach ($lockData['packages'] as $i => $pkg) {
if ($package->getName() === $pkg['name']) {
unset($lockData['packages'][$i]);
}
}
}
$jsonContent = file_get_contents($json->getPath());
$lockData['packages'] = array_values($lockData['packages']);
$lockData['packages-dev'] = array_values($lockData['packages-dev']);
$lockData['content-hash'] = Locker::getContentHash($jsonContent);
$lockFile = new JsonFile(substr($json->getPath(), 0, -4).'lock', null, $io);
if (!$this->dryRun) {
$lockFile->write($lockData);
}
// force removal of files under vendor/
if (version_compare('2.0.0', PluginInterface::PLUGIN_API_VERSION, '>')) {
$locker = new Locker($io, $lockFile, $this->composer->getRepositoryManager(), $this->composer->getInstallationManager(), $jsonContent);
} else {
$locker = new Locker($io, $lockFile, $this->composer->getInstallationManager(), $jsonContent);
}
$this->composer->setLocker($locker);
}
}

View File

@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Update;
class DiffHelper
{
public static function removeFilesFromPatch(string $patch, array $files, array &$removedPatches): string
{
foreach ($files as $filename) {
$start = strpos($patch, sprintf('diff --git a/%s b/%s', $filename, $filename));
if (false === $start) {
throw new \LogicException(sprintf('Could not find file "%s" in the patch.', $filename));
}
$end = strpos($patch, 'diff --git a/', $start + 1);
$contentBefore = substr($patch, 0, $start);
if (false === $end) {
// last patch in the file
$removedPatches[$filename] = rtrim(substr($patch, $start), "\n");
$patch = rtrim($contentBefore, "\n");
continue;
}
$removedPatches[$filename] = rtrim(substr($patch, $start, $end - $start), "\n");
$patch = $contentBefore.substr($patch, $end);
}
// valid patches end with a blank line
if ($patch && "\n" !== substr($patch, \strlen($patch) - 1, 1)) {
$patch = $patch."\n";
}
return $patch;
}
}

View File

@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Update;
class RecipePatch
{
private $patch;
private $blobs;
private $removedPatches;
public function __construct(string $patch, array $blobs, array $removedPatches = [])
{
$this->patch = $patch;
$this->blobs = $blobs;
$this->removedPatches = $removedPatches;
}
public function getPatch(): string
{
return $this->patch;
}
public function getBlobs(): array
{
return $this->blobs;
}
/**
* Patches for modified files that were removed because the file
* has been deleted in the user's project.
*/
public function getRemovedPatches(): array
{
return $this->removedPatches;
}
}

View File

@ -0,0 +1,226 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Update;
use Composer\IO\IOInterface;
use Composer\Util\ProcessExecutor;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
class RecipePatcher
{
private $rootDir;
private $filesystem;
private $io;
private $processExecutor;
public function __construct(string $rootDir, IOInterface $io)
{
$this->rootDir = $rootDir;
$this->filesystem = new Filesystem();
$this->io = $io;
$this->processExecutor = new ProcessExecutor($io);
}
/**
* Applies the patch. If it fails unexpectedly, an exception will be thrown.
*
* @return bool returns true if fully successful, false if conflicts were encountered
*/
public function applyPatch(RecipePatch $patch): bool
{
if (!$patch->getPatch()) {
// nothing to do!
return true;
}
$addedBlobs = $this->addMissingBlobs($patch->getBlobs());
$patchPath = $this->rootDir.'/_flex_recipe_update.patch';
file_put_contents($patchPath, $patch->getPatch());
try {
$this->execute('git update-index --refresh', $this->rootDir);
$output = '';
$statusCode = $this->processExecutor->execute('git apply "_flex_recipe_update.patch" -3', $output, $this->rootDir);
if (0 === $statusCode) {
// successful with no conflicts
return true;
}
if (false !== strpos($this->processExecutor->getErrorOutput(), 'with conflicts')) {
// successful with conflicts
return false;
}
throw new \LogicException('Error applying the patch: '.$this->processExecutor->getErrorOutput());
} finally {
unlink($patchPath);
// clean up any temporary blobs
foreach ($addedBlobs as $filename) {
unlink($filename);
}
}
}
public function generatePatch(array $originalFiles, array $newFiles): RecipePatch
{
// null implies "file does not exist"
$originalFiles = array_filter($originalFiles, function ($file) {
return null !== $file;
});
$newFiles = array_filter($newFiles, function ($file) {
return null !== $file;
});
// find removed files and add them so they will be deleted
foreach ($originalFiles as $file => $contents) {
if (!isset($newFiles[$file])) {
$newFiles[$file] = null;
}
}
// If a file is being modified, but does not exist in the current project,
// it cannot be patched. We generate the diff for these, but then remove
// it from the patch (and optionally report this diff to the user).
$modifiedFiles = array_intersect_key(array_keys($originalFiles), array_keys($newFiles));
$deletedModifiedFiles = [];
foreach ($modifiedFiles as $modifiedFile) {
if (!file_exists($this->rootDir.'/'.$modifiedFile) && $originalFiles[$modifiedFile] !== $newFiles[$modifiedFile]) {
$deletedModifiedFiles[] = $modifiedFile;
}
}
$tmpPath = sys_get_temp_dir().'/_flex_recipe_update'.uniqid(mt_rand(), true);
$this->filesystem->mkdir($tmpPath);
try {
$this->execute('git init', $tmpPath);
$this->execute('git config commit.gpgsign false', $tmpPath);
$this->execute('git config user.name "Flex Updater"', $tmpPath);
$this->execute('git config user.email ""', $tmpPath);
$blobs = [];
if (\count($originalFiles) > 0) {
$this->writeFiles($originalFiles, $tmpPath);
$this->execute('git add -A', $tmpPath);
$this->execute('git commit -m "original files"', $tmpPath);
$blobs = $this->generateBlobs($originalFiles, $tmpPath);
}
$this->writeFiles($newFiles, $tmpPath);
$this->execute('git add -A', $tmpPath);
$patchString = $this->execute('git diff --cached', $tmpPath);
$removedPatches = [];
$patchString = DiffHelper::removeFilesFromPatch($patchString, $deletedModifiedFiles, $removedPatches);
return new RecipePatch(
$patchString,
$blobs,
$removedPatches
);
} finally {
try {
$this->filesystem->remove($tmpPath);
} catch (IOException $e) {
// this can sometimes fail due to git file permissions
// if that happens, just leave it: we're in the temp directory anyways
}
}
}
private function writeFiles(array $files, string $directory): void
{
foreach ($files as $filename => $contents) {
$path = $directory.'/'.$filename;
if (null === $contents) {
if (file_exists($path)) {
unlink($path);
}
continue;
}
if (!file_exists(\dirname($path))) {
$this->filesystem->mkdir(\dirname($path));
}
file_put_contents($path, $contents);
}
}
private function execute(string $command, string $cwd): string
{
$output = '';
$statusCode = $this->processExecutor->execute($command, $output, $cwd);
if (0 !== $statusCode) {
throw new \LogicException(sprintf('Command "%s" failed: "%s". Output: "%s".', $command, $this->processExecutor->getErrorOutput(), $output));
}
return $output;
}
/**
* Adds git blobs for each original file.
*
* For patching to work, each original file & contents needs to be
* available to git as a blob. This is because the patch contains
* the ref to the original blob, and git uses that to find the
* original file (which is needed for the 3-way merge).
*/
private function addMissingBlobs(array $blobs): array
{
$addedBlobs = [];
foreach ($blobs as $hash => $contents) {
$blobPath = $this->rootDir.'/'.$this->getBlobPath($hash);
if (file_exists($blobPath)) {
continue;
}
$addedBlobs[] = $blobPath;
if (!file_exists(\dirname($blobPath))) {
$this->filesystem->mkdir(\dirname($blobPath));
}
file_put_contents($blobPath, $contents);
}
return $addedBlobs;
}
private function generateBlobs(array $originalFiles, string $originalFilesRoot): array
{
$addedBlobs = [];
foreach ($originalFiles as $filename => $contents) {
// if the file didn't originally exist, no blob needed
if (!file_exists($originalFilesRoot.'/'.$filename)) {
continue;
}
$hash = trim($this->execute('git hash-object '.ProcessExecutor::escape($filename), $originalFilesRoot));
$addedBlobs[$hash] = file_get_contents($originalFilesRoot.'/'.$this->getBlobPath($hash));
}
return $addedBlobs;
}
private function getBlobPath(string $hash): string
{
$hashStart = substr($hash, 0, 2);
$hashEnd = substr($hash, 2);
return '.git/objects/'.$hashStart.'/'.$hashEnd;
}
}

View File

@ -0,0 +1,114 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Flex\Update;
use Symfony\Flex\Lock;
use Symfony\Flex\Recipe;
class RecipeUpdate
{
private $originalRecipe;
private $newRecipe;
private $lock;
private $rootDir;
/** @var string[] */
private $originalRecipeFiles = [];
/** @var string[] */
private $newRecipeFiles = [];
private $copyFromPackagePaths = [];
public function __construct(Recipe $originalRecipe, Recipe $newRecipe, Lock $lock, string $rootDir)
{
$this->originalRecipe = $originalRecipe;
$this->newRecipe = $newRecipe;
$this->lock = $lock;
$this->rootDir = $rootDir;
}
public function getOriginalRecipe(): Recipe
{
return $this->originalRecipe;
}
public function getNewRecipe(): Recipe
{
return $this->newRecipe;
}
public function getLock(): Lock
{
return $this->lock;
}
public function getRootDir(): string
{
return $this->rootDir;
}
public function getPackageName(): string
{
return $this->originalRecipe->getName();
}
public function setOriginalFile(string $filename, ?string $contents): void
{
$this->originalRecipeFiles[$filename] = $contents;
}
public function setNewFile(string $filename, ?string $contents): void
{
$this->newRecipeFiles[$filename] = $contents;
}
public function addOriginalFiles(array $files)
{
foreach ($files as $file => $contents) {
if (null === $contents) {
continue;
}
$this->setOriginalFile($file, $contents);
}
}
public function addNewFiles(array $files)
{
foreach ($files as $file => $contents) {
if (null === $contents) {
continue;
}
$this->setNewFile($file, $contents);
}
}
public function getOriginalFiles(): array
{
return $this->originalRecipeFiles;
}
public function getNewFiles(): array
{
return $this->newRecipeFiles;
}
public function getCopyFromPackagePaths(): array
{
return $this->copyFromPackagePaths;
}
public function addCopyFromPackagePath(string $source, string $target)
{
$this->copyFromPackagePaths[$source] = $target;
}
}