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

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;
}
}