initial commit

This commit is contained in:
2024-11-14 13:46:24 +01:00
commit 99d46ae4ae
57 changed files with 8793 additions and 0 deletions

65
composer.json Normal file
View File

@@ -0,0 +1,65 @@
{
"type": "project",
"license": "proprietary",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"symfony/console": "6.4.*",
"symfony/dotenv": "6.4.*",
"symfony/flex": "^2",
"symfony/framework-bundle": "6.4.*",
"symfony/runtime": "6.4.*",
"symfony/yaml": "6.4.*"
},
"require-dev": {
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
},
"sort-packages": true
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*"
},
"scripts": {
"auto-scripts": [
],
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "6.4.*"
}
}
}

91
composer.lock generated Normal file
View File

@@ -0,0 +1,91 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6c7e3dbdfc97c282707609d966719896",
"packages": [
{
"name": "symfony/flex",
"version": "v2.4.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/flex.git",
"reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/flex/zipball/92f4fba342161ff36072bd3b8e0b3c6c23160402",
"reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402",
"shasum": ""
},
"require": {
"composer-plugin-api": "^2.1",
"php": ">=8.0"
},
"conflict": {
"composer/semver": "<1.7.2"
},
"require-dev": {
"composer/composer": "^2.1",
"symfony/dotenv": "^5.4|^6.0",
"symfony/filesystem": "^5.4|^6.0",
"symfony/phpunit-bridge": "^5.4|^6.0",
"symfony/process": "^5.4|^6.0"
},
"type": "composer-plugin",
"extra": {
"class": "Symfony\\Flex\\Flex"
},
"autoload": {
"psr-4": {
"Symfony\\Flex\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien.potencier@gmail.com"
}
],
"description": "Composer plugin for Symfony",
"support": {
"issues": "https://github.com/symfony/flex/issues",
"source": "https://github.com/symfony/flex/tree/v2.4.7"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-10-07T08:51:54+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*"
},
"platform-dev": [],
"plugin-api-version": "2.6.0"
}

25
vendor/autoload.php vendored Normal file
View File

@@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit54c05e64f01f49eb136e9af7b3075bdd::getLoader();

579
vendor/composer/ClassLoader.php vendored Normal file
View File

@@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

359
vendor/composer/InstalledVersions.php vendored Normal file
View File

@@ -0,0 +1,359 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}

21
vendor/composer/LICENSE vendored Normal file
View File

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

10
vendor/composer/autoload_classmap.php vendored Normal file
View File

@@ -0,0 +1,10 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

View File

@@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

12
vendor/composer/autoload_psr4.php vendored Normal file
View File

@@ -0,0 +1,12 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Symfony\\Flex\\' => array($vendorDir . '/symfony/flex/src'),
'App\\Tests\\' => array($baseDir . '/tests'),
'App\\' => array($baseDir . '/src'),
);

38
vendor/composer/autoload_real.php vendored Normal file
View File

@@ -0,0 +1,38 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit54c05e64f01f49eb136e9af7b3075bdd
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit54c05e64f01f49eb136e9af7b3075bdd', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit54c05e64f01f49eb136e9af7b3075bdd', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit54c05e64f01f49eb136e9af7b3075bdd::getInitializer($loader));
$loader->register(true);
return $loader;
}
}

49
vendor/composer/autoload_static.php vendored Normal file
View File

@@ -0,0 +1,49 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit54c05e64f01f49eb136e9af7b3075bdd
{
public static $prefixLengthsPsr4 = array (
'S' =>
array (
'Symfony\\Flex\\' => 13,
),
'A' =>
array (
'App\\Tests\\' => 10,
'App\\' => 4,
),
);
public static $prefixDirsPsr4 = array (
'Symfony\\Flex\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/flex/src',
),
'App\\Tests\\' =>
array (
0 => __DIR__ . '/../..' . '/tests',
),
'App\\' =>
array (
0 => __DIR__ . '/../..' . '/src',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit54c05e64f01f49eb136e9af7b3075bdd::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit54c05e64f01f49eb136e9af7b3075bdd::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit54c05e64f01f49eb136e9af7b3075bdd::$classMap;
}, null, ClassLoader::class);
}
}

77
vendor/composer/installed.json vendored Normal file
View File

@@ -0,0 +1,77 @@
{
"packages": [
{
"name": "symfony/flex",
"version": "v2.4.7",
"version_normalized": "2.4.7.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/flex.git",
"reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/flex/zipball/92f4fba342161ff36072bd3b8e0b3c6c23160402",
"reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402",
"shasum": ""
},
"require": {
"composer-plugin-api": "^2.1",
"php": ">=8.0"
},
"conflict": {
"composer/semver": "<1.7.2"
},
"require-dev": {
"composer/composer": "^2.1",
"symfony/dotenv": "^5.4|^6.0",
"symfony/filesystem": "^5.4|^6.0",
"symfony/phpunit-bridge": "^5.4|^6.0",
"symfony/process": "^5.4|^6.0"
},
"time": "2024-10-07T08:51:54+00:00",
"type": "composer-plugin",
"extra": {
"class": "Symfony\\Flex\\Flex"
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Flex\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien.potencier@gmail.com"
}
],
"description": "Composer plugin for Symfony",
"support": {
"issues": "https://github.com/symfony/flex/issues",
"source": "https://github.com/symfony/flex/tree/v2.4.7"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/flex"
}
],
"dev": true,
"dev-package-names": []
}

74
vendor/composer/installed.php vendored Normal file
View File

@@ -0,0 +1,74 @@
<?php return array(
'root' => array(
'name' => 'symfony/skeleton',
'pretty_version' => 'v6.4.99',
'version' => '6.4.99.0',
'reference' => null,
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'symfony/flex' => array(
'pretty_version' => 'v2.4.7',
'version' => '2.4.7.0',
'reference' => '92f4fba342161ff36072bd3b8e0b3c6c23160402',
'type' => 'composer-plugin',
'install_path' => __DIR__ . '/../symfony/flex',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-ctype' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '*',
),
),
'symfony/polyfill-iconv' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '*',
),
),
'symfony/polyfill-php72' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '*',
),
),
'symfony/polyfill-php73' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '*',
),
),
'symfony/polyfill-php74' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '*',
),
),
'symfony/polyfill-php80' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '*',
),
),
'symfony/polyfill-php81' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '*',
),
),
'symfony/skeleton' => array(
'pretty_version' => 'v6.4.99',
'version' => '6.4.99.0',
'reference' => null,
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

26
vendor/composer/platform_check.php vendored Normal file
View File

@@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 80100)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

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

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

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

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

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

@@ -0,0 +1,35 @@
{
"name": "symfony/flex",
"type": "composer-plugin",
"description": "Composer plugin for Symfony",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien.potencier@gmail.com"
}
],
"minimum-stability": "dev",
"require": {
"php": ">=8.0",
"composer-plugin-api": "^2.1"
},
"require-dev": {
"composer/composer": "^2.1",
"symfony/dotenv": "^5.4|^6.0",
"symfony/filesystem": "^5.4|^6.0",
"symfony/phpunit-bridge": "^5.4|^6.0",
"symfony/process": "^5.4|^6.0"
},
"conflict": {
"composer/semver": "<1.7.2"
},
"autoload": {
"psr-4": {
"Symfony\\Flex\\": "src"
}
},
"extra": {
"class": "Symfony\\Flex\\Flex"
}
}

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,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,344 @@
<?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 Composer\Package\Package;
use Composer\Util\HttpDownloader;
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 Lock $symfonyLock;
private GithubApi $githubApi;
public function __construct(/* cannot be type-hinted */ $flex, Lock $symfonyLock, HttpDownloader $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): int
{
$installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository();
// Inspect one or all packages
$package = $input->getArgument('package');
if (null !== $package) {
$packages = [strtolower($package)];
} else {
$locker = $this->getComposer()->getLocker();
$lockData = $locker->getLockData();
// Merge all packages installed
$packages = array_column(array_merge($lockData['packages'], $lockData['packages-dev']), 'name');
$packages = array_unique(array_merge($packages, array_keys($this->symfonyLock->all())));
}
$operations = [];
foreach ($packages as $name) {
$pkg = $installedRepo->findPackage($name, '*');
if (!$pkg && $this->symfonyLock->has($name)) {
$pkgVersion = $this->symfonyLock->get($name)['version'];
$pkg = new Package($name, $pkgVersion, $pkgVersion);
} elseif (!$pkg) {
$this->getIO()->writeError(sprintf('<error>Package %s is not installed</error>', $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;
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;
if ('master' === $lockBranch && \in_array($lockRepo, ['github.com/symfony/recipes', 'github.com/symfony/recipes-contrib'])) {
$lockBranch = 'main';
}
$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=';
$historyUrl .= (new \DateTime($commitDate))
->setTimezone(new \DateTimeZone('UTC'))
->modify('+1 seconds')
->format('Y-m-d\TH:i:s\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,415 @@
<?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\Package\Package;
use Composer\Package\PackageInterface;
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\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;
}
$installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository();
$package = $installedRepo->findPackage($packageName, '*') ?? new Package($packageName, $packageLockData['version'], $packageLockData['version']);
$originalRecipe = $this->getRecipe($package, $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($package);
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(PackageInterface $package, ?string $recipeRef = null, ?string $recipeVersion = null): ?Recipe
{
$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,
$package->getName(),
$operation->getOperationType(),
$recipes['manifests'][$package->getName()],
$recipes['locks'][$package->getName()] ?? []
);
}
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();
$operations = [];
foreach ($symfonyLock->all() as $name => $lock) {
if (isset($lock['recipe']['ref'])) {
$package = $installedRepo->findPackage($name, '*') ?? new Package($name, $lock['version'], $lock['version']);
$operations[] = new InformationOperation($package);
}
}
$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;
}
}

119
vendor/symfony/flex/src/Configurator.php vendored Normal file
View File

@@ -0,0 +1,119 @@
<?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 $postInstallConfigurators;
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,
'dotenv' => Configurator\DotenvConfigurator::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,
];
$this->postInstallConfigurators = [
'add-lines' => Configurator\AddLinesConfigurator::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);
}
}
}
/**
* Run after all recipes have been installed to run post-install configurators.
*/
public function postInstall(Recipe $recipe, Lock $lock, array $options = [])
{
$manifest = $recipe->getManifest();
foreach (array_keys($this->postInstallConfigurators) 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();
$allConfigurators = array_merge($this->configurators, $this->postInstallConfigurators);
foreach (array_keys($allConfigurators) 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();
$allConfigurators = array_merge($this->configurators, $this->postInstallConfigurators);
foreach (array_keys($allConfigurators) as $key) {
if (isset($manifest[$key])) {
$this->get($key)->unconfigure($recipe, $manifest[$key], $lock);
}
}
}
private function get($key): AbstractConfigurator
{
if (!isset($this->configurators[$key]) && !isset($this->postInstallConfigurators[$key])) {
throw new \InvalidArgumentException(\sprintf('Unknown configurator "%s".', $key));
}
if (isset($this->cache[$key])) {
return $this->cache[$key];
}
$class = isset($this->configurators[$key]) ? $this->configurators[$key] : $this->postInstallConfigurators[$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, $verbosity = IOInterface::VERBOSE)
{
if (!\is_array($messages)) {
$messages = [$messages];
}
foreach ($messages as $i => $message) {
$messages[$i] = ' '.$message;
}
$this->io->writeError($messages, true, $verbosity);
}
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,270 @@
<?php
namespace Symfony\Flex\Configurator;
use Composer\IO\IOInterface;
use Symfony\Flex\Lock;
use Symfony\Flex\Recipe;
use Symfony\Flex\Update\RecipeUpdate;
/**
* @author Kevin Bond <kevinbond@gmail.com>
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class AddLinesConfigurator extends AbstractConfigurator
{
private const POSITION_TOP = 'top';
private const POSITION_BOTTOM = 'bottom';
private const POSITION_AFTER_TARGET = 'after_target';
private const VALID_POSITIONS = [
self::POSITION_TOP,
self::POSITION_BOTTOM,
self::POSITION_AFTER_TARGET,
];
/**
* Holds file contents for files that have been loaded.
* This allows us to "change" the contents of a file multiple
* times before we actually write it out.
*
* @var string[]
*/
private $fileContents = [];
public function configure(Recipe $recipe, $config, Lock $lock, array $options = []): void
{
$this->fileContents = [];
$this->executeConfigure($recipe, $config);
foreach ($this->fileContents as $file => $contents) {
$this->write(sprintf('[add-lines] Patching file "%s"', $this->relativize($file)));
file_put_contents($file, $contents);
}
}
public function unconfigure(Recipe $recipe, $config, Lock $lock): void
{
$this->fileContents = [];
$this->executeUnconfigure($recipe, $config);
foreach ($this->fileContents as $file => $change) {
$this->write(sprintf('[add-lines] Reverting file "%s"', $this->relativize($file)));
file_put_contents($file, $change);
}
}
public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void
{
// manually check for "requires", as unconfigure ignores it
$originalConfig = array_filter($originalConfig, function ($item) {
return !isset($item['requires']) || $this->isPackageInstalled($item['requires']);
});
// reset the file content cache
$this->fileContents = [];
$this->executeUnconfigure($recipeUpdate->getOriginalRecipe(), $originalConfig);
$this->executeConfigure($recipeUpdate->getNewRecipe(), $newConfig);
$newFiles = [];
$originalFiles = [];
foreach ($this->fileContents as $file => $contents) {
// set the original file to the current contents
$originalFiles[$this->relativize($file)] = file_get_contents($file);
// and the new file where the old recipe was unconfigured, and the new configured
$newFiles[$this->relativize($file)] = $contents;
}
$recipeUpdate->addOriginalFiles($originalFiles);
$recipeUpdate->addNewFiles($newFiles);
}
public function executeConfigure(Recipe $recipe, $config): void
{
foreach ($config as $patch) {
if (!isset($patch['file'])) {
$this->write(sprintf('The "file" key is required for the "add-lines" configurator for recipe "%s". Skipping', $recipe->getName()));
continue;
}
if (isset($patch['requires']) && !$this->isPackageInstalled($patch['requires'])) {
continue;
}
if (!isset($patch['content'])) {
$this->write(sprintf('The "content" key is required for the "add-lines" configurator for recipe "%s". Skipping', $recipe->getName()));
continue;
}
$content = $patch['content'];
$file = $this->path->concatenate([$this->options->get('root-dir'), $this->options->expandTargetDir($patch['file'])]);
$warnIfMissing = isset($patch['warn_if_missing']) && $patch['warn_if_missing'];
if (!is_file($file)) {
$this->write([
sprintf('Could not add lines to file <info>%s</info> as it does not exist. Missing lines:', $patch['file']),
'<comment>"""</comment>',
$content,
'<comment>"""</comment>',
'',
], $warnIfMissing ? IOInterface::NORMAL : IOInterface::VERBOSE);
continue;
}
if (!isset($patch['position'])) {
$this->write(sprintf('The "position" key is required for the "add-lines" configurator for recipe "%s". Skipping', $recipe->getName()));
continue;
}
$position = $patch['position'];
if (!\in_array($position, self::VALID_POSITIONS, true)) {
$this->write(sprintf('The "position" key must be one of "%s" for the "add-lines" configurator for recipe "%s". Skipping', implode('", "', self::VALID_POSITIONS), $recipe->getName()));
continue;
}
if (self::POSITION_AFTER_TARGET === $position && !isset($patch['target'])) {
$this->write(sprintf('The "target" key is required when "position" is "%s" for the "add-lines" configurator for recipe "%s". Skipping', self::POSITION_AFTER_TARGET, $recipe->getName()));
continue;
}
$target = isset($patch['target']) ? $patch['target'] : null;
$newContents = $this->getPatchedContents($file, $content, $position, $target, $warnIfMissing);
$this->fileContents[$file] = $newContents;
}
}
public function executeUnconfigure(Recipe $recipe, $config): void
{
foreach ($config as $patch) {
if (!isset($patch['file'])) {
$this->write(sprintf('The "file" key is required for the "add-lines" configurator for recipe "%s". Skipping', $recipe->getName()));
continue;
}
// Ignore "requires": the target packages may have just become uninstalled.
// Checking for a "content" match is enough.
$file = $this->path->concatenate([$this->options->get('root-dir'), $this->options->expandTargetDir($patch['file'])]);
if (!is_file($file)) {
continue;
}
if (!isset($patch['content'])) {
$this->write(sprintf('The "content" key is required for the "add-lines" configurator for recipe "%s". Skipping', $recipe->getName()));
continue;
}
$value = $patch['content'];
$newContents = $this->getUnPatchedContents($file, $value);
$this->fileContents[$file] = $newContents;
}
}
private function getPatchedContents(string $file, string $value, string $position, ?string $target, bool $warnIfMissing): string
{
$fileContents = $this->readFile($file);
if (false !== strpos($fileContents, $value)) {
return $fileContents; // already includes value, skip
}
switch ($position) {
case self::POSITION_BOTTOM:
$fileContents .= "\n".$value;
break;
case self::POSITION_TOP:
$fileContents = $value."\n".$fileContents;
break;
case self::POSITION_AFTER_TARGET:
$lines = explode("\n", $fileContents);
$targetFound = false;
foreach ($lines as $key => $line) {
if (false !== strpos($line, $target)) {
array_splice($lines, $key + 1, 0, $value);
$targetFound = true;
break;
}
}
$fileContents = implode("\n", $lines);
if (!$targetFound) {
$this->write([
sprintf('Could not add lines after "%s" as no such string was found in "%s". Missing lines:', $target, $file),
'<comment>"""</comment>',
$value,
'<comment>"""</comment>',
'',
], $warnIfMissing ? IOInterface::NORMAL : IOInterface::VERBOSE);
}
break;
}
return $fileContents;
}
private function getUnPatchedContents(string $file, $value): string
{
$fileContents = $this->readFile($file);
if (false === strpos($fileContents, $value)) {
return $fileContents; // value already gone!
}
if (false !== strpos($fileContents, "\n".$value)) {
$value = "\n".$value;
} elseif (false !== strpos($fileContents, $value."\n")) {
$value = $value."\n";
}
$position = strpos($fileContents, $value);
return substr_replace($fileContents, '', $position, \strlen($value));
}
private function isPackageInstalled($packages): bool
{
if (\is_string($packages)) {
$packages = [$packages];
}
$installedRepo = $this->composer->getRepositoryManager()->getLocalRepository();
foreach ($packages as $packageName) {
if (null === $installedRepo->findPackage($packageName, '*')) {
return false;
}
}
return true;
}
private function relativize(string $path): string
{
$rootDir = $this->options->get('root-dir');
if (0 === strpos($path, $rootDir)) {
$path = substr($path, \strlen($rootDir) + 1);
}
return ltrim($path, '/\\');
}
private function readFile(string $file): string
{
if (isset($this->fileContents[$file])) {
return $this->fileContents[$file];
}
$fileContents = file_get_contents($file);
$this->fileContents[$file] = $fileContents;
return $fileContents;
}
}

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 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, true);
$recipeUpdate->setOriginalFile(
$this->getLocalConfFile(),
$this->buildContents($originalBundles)
);
$newBundles = $this->configureBundles($newConfig, true);
$recipeUpdate->setNewFile(
$this->getLocalConfFile(),
$this->buildContents($newBundles)
);
}
private function configureBundles(array $bundles, bool $resetEnvironments = false): 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) {
// do not override existing configured envs for a bundle
if (!isset($registered[$class]) || $resetEnvironments) {
if ($resetEnvironments) {
// used during calculating an "upgrade"
// here, we want to "undo" the bundle's configuration entirely
// then re-add it fresh, in case some environments have been
// removed in an updated version of the recipe
$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,164 @@
<?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 = $this->removeParametersFromLines(file($target), $parameters);
file_put_contents($target, implode('', $lines));
}
public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void
{
$recipeUpdate->setOriginalFile(
$this->getServicesPath(),
$this->configureParameters($originalConfig, true)
);
// for the new file, we need to update any values *and* remove any removed values
$removedParameters = [];
foreach ($originalConfig as $name => $value) {
if (!isset($newConfig[$name])) {
$removedParameters[$name] = $value;
}
}
$updatedFile = $this->configureParameters($newConfig, true);
$lines = $this->removeParametersFromLines(explode("\n", $updatedFile), $removedParameters);
$recipeUpdate->setNewFile(
$this->getServicesPath(),
implode("\n", $lines)
);
}
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 removeParametersFromLines(array $sourceLines, array $parameters): array
{
$lines = [];
foreach ($sourceLines as $line) {
if ($this->removeParameters(1, $parameters, $line)) {
continue;
}
$lines[] = $line;
}
return $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,404 @@
<?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 compose.yaml file.
*
* @author Kévin Dunglas <kevin@dunglas.dev>
*/
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 $key => $val) {
// Support for the short recipe syntax that modifies compose.yaml only
if (isset($val[0])) {
return ['compose.yaml' => $config];
}
if (!str_starts_with($key, 'docker-')) {
continue;
}
// If the recipe still use the legacy "docker-compose.yml" names, remove the "docker-" prefix and change the extension
$newKey = pathinfo(substr($key, 7), \PATHINFO_FILENAME).'.yaml';
$config[$newKey] = $val;
unset($config[$key]);
}
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'])) {
$filenameToFind = pathinfo($file, \PATHINFO_FILENAME);
$separator = $_SERVER['COMPOSE_PATH_SEPARATOR'] ?? ('\\' === \DIRECTORY_SEPARATOR ? ';' : ':');
$files = explode($separator, $_SERVER['COMPOSE_FILE']);
foreach ($files as $f) {
$filename = pathinfo($f, \PATHINFO_FILENAME);
if ($filename !== $filenameToFind && "docker-$filenameToFind" !== $filename) {
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 {
if (
$this->filesystem->exists($dockerComposeFile = sprintf('%s/%s', $dir, $file)) ||
// Test with the ".yml" extension if the file doesn't end up with ".yaml"
$this->filesystem->exists($dockerComposeFile = substr($dockerComposeFile, 0, -3).'ml') ||
// Test with the legacy "docker-" suffix if "compose.ya?ml" doesn't exist
$this->filesystem->exists($dockerComposeFile = sprintf('%s/docker-%s', $dir, $file)) ||
$this->filesystem->exists($dockerComposeFile = substr($dockerComposeFile, 0, -3).'ml')
) {
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, '');
$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] = !$i || '' !== 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_filter(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 = $file;
if (0 === strpos($file, $rootDir)) {
$localPath = substr($file, \strlen($rootDir) + 1);
}
$localPath = ltrim($localPath, '/\\');
$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>compose.yaml</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,50 @@
<?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;
class DotenvConfigurator extends AbstractConfigurator
{
public function configure(Recipe $recipe, $vars, Lock $lock, array $options = [])
{
foreach ($vars as $suffix => $vars) {
$configurator = new EnvConfigurator($this->composer, $this->io, $this->options, $suffix);
$configurator->configure($recipe, $vars, $lock, $options);
}
}
public function unconfigure(Recipe $recipe, $vars, Lock $lock)
{
foreach ($vars as $suffix => $vars) {
$configurator = new EnvConfigurator($this->composer, $this->io, $this->options, $suffix);
$configurator->unconfigure($recipe, $vars, $lock);
}
}
public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void
{
foreach ($originalConfig as $suffix => $vars) {
$configurator = new EnvConfigurator($this->composer, $this->io, $this->options, $suffix);
$configurator->update($recipeUpdate, $vars, $newConfig[$suffix] ?? []);
}
foreach ($newConfig as $suffix => $vars) {
if (!isset($originalConfig[$suffix])) {
$configurator = new EnvConfigurator($this->composer, $this->io, $this->options, $suffix);
$configurator->update($recipeUpdate, [], $vars);
}
}
}
}

View File

@@ -0,0 +1,295 @@
<?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\Recipe;
use Symfony\Flex\Update\RecipeUpdate;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class EnvConfigurator extends AbstractConfigurator
{
private string $suffix;
public function __construct(Composer $composer, IOInterface $io, Options $options, string $suffix = '')
{
parent::__construct($composer, $io, $options);
$this->suffix = $suffix;
}
public function configure(Recipe $recipe, $vars, Lock $lock, array $options = [])
{
$this->write('Adding environment variable defaults'.('' === $this->suffix ? '' : ' ('.$this->suffix.')'));
$this->configureEnvDist($recipe, $vars, $options['force'] ?? false);
if ('' !== $this->suffix) {
return;
}
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';
$files = '' === $this->suffix ? [$dotenvPath.'.dist', $dotenvPath] : [$dotenvPath.'.'.$this->suffix];
foreach ($files 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';
$files = '' === $this->suffix ? [$dotenvPath, $dotenvPath.'.dist'] : [$dotenvPath.'.'.$this->suffix];
foreach ($files 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 = '' === $this->suffix ? [$dotenvPath, $dotenvPath.'.dist', 'phpunit.xml.dist', 'phpunit.xml'] : [$dotenvPath.'.'.$this->suffix];
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 ('' === $this->suffix && !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 (!str_starts_with($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;
}
}

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

@@ -0,0 +1,469 @@
<?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;
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;
private HttpDownloader $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, HttpDownloader $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 Cache($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 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['recipe_template_relative'])) {
$links['recipe_template'] = preg_replace('{[^/\?]*+(?=\?|$)}', $links['recipe_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) {
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);
}
$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);
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;
}
}

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

@@ -0,0 +1,873 @@
<?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\Transaction;
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\Locker;
use Composer\Package\Package;
use Composer\Plugin\PluginEvents;
use Composer\Plugin\PluginInterface;
use Composer\Plugin\PrePoolCreateEvent;
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
{
public static $storedOperations = [];
/**
* @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 $displayThanksReminder = 0;
private $dryRun = false;
private $reinstall;
private static $activated = true;
private static $aliasResolveCommands = [
'require' => true,
'update' => false,
'remove' => false,
'unpack' => true,
];
private $filter;
/**
* @return void
*/
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();
// if Flex is being upgraded, the original operations from the original Flex
// instance are stored in the static property, so we can reuse them now.
if (property_exists(self::class, 'storedOperations') && self::$storedOperations) {
$this->operations = self::$storedOperations;
self::$storedOperations = [];
}
$symfonyRequire = preg_replace('/\.x$/', '.x-dev', getenv('SYMFONY_REQUIRE') ?: ($composer->getPackage()->getExtra()['symfony']['require'] ?? ''));
$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);
}
$composerFile = Factory::getComposerFile();
$composerLock = 'json' === pathinfo($composerFile, \PATHINFO_EXTENSION) ? substr($composerFile, 0, -4).'lock' : $composerFile.'.lock';
$symfonyLock = str_replace('composer', 'symfony', basename($composerLock));
$this->configurator = new Configurator($composer, $io, $this->options);
$this->lock = new Lock(getenv('SYMFONY_LOCKFILE') ?: \dirname($composerLock).'/'.(basename($composerLock) !== $symfonyLock ? $symfonyLock : 'symfony.lock'));
$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();
}
$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);
try {
$command = $input->getFirstArgument();
$command = $command ? $app->find($command)->getName() : null;
} catch (\InvalidArgumentException $e) {
}
if ('create-project' === $command) {
if ($input->hasOption('remove-vcs')) {
$input->setOption('remove-vcs', true);
}
} elseif ('update' === $command) {
$this->displayThanksReminder = 1;
} elseif ('outdated' === $command) {
$symfonyRequire = null;
}
if (isset(self::$aliasResolveCommands[$command])) {
if ($input->hasArgument('packages')) {
$input->setArgument('packages', $resolver->resolve($input->getArgument('packages'), self::$aliasResolveCommands[$command]));
}
}
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;
}
$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')));
$app->add(new Command\DumpEnvCommand($this->config, $this->options));
break;
}
}
/**
* @return void
*/
public function deactivate(Composer $composer, IOInterface $io)
{
// store operations in case Flex is being upgraded
self::$storedOperations = $this->operations;
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()));
$updateAllowList = \Closure::bind(function () {
return $this->updateAllowList;
}, $this->installer, $this->installer)();
if (['php' => 0] === $updateAllowList) {
$this->dryRun = true; // prevent recipes from being uninstalled when removing a pack
}
}
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 recordFlexInstall(PackageEvent $event)
{
if (null === $this->reinstall && 'symfony/flex' === $event->getOperation()->getPackage()->getName()) {
$this->reinstall = true;
}
}
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) {
if ('9999999.9999999' === $info['version']) {
// Fix invalid versions found in some lock files
$info['version'] = '99999.9999999';
}
$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 (!$operation instanceof UninstallOperation && $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 (!$this->reinstall && !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])) {
continue;
}
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();
$postInstallRecipes = [];
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':
$postInstallRecipes[] = $recipe;
$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 (method_exists($this->configurator, 'postInstall')) {
foreach ($postInstallRecipes as $recipe) {
$this->configurator->postInstall($recipe, $this->lock, [
'force' => $event instanceof UpdateEvent && $event->force(),
]);
}
}
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), '/');
$executor = new ScriptExecutor($this->composer, $this->io, $this->options);
$synchronizer = new PackageJsonSynchronizer($rootDir, $vendorDir, $executor, $this->io);
if ($synchronizer->shouldSynchronize()) {
$lockData = $this->composer->getLocker()->getLockData();
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('');
}
}
}
/**
* @return void
*/
public function uninstall(Composer $composer, IOInterface $io)
{
$this->lock->delete();
}
public function enableThanksReminder()
{
if (1 === $this->displayThanksReminder) {
$this->displayThanksReminder = !class_exists(Thanks::class, false) ? 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 = [];
}
/**
* @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,
];
$packRecipes = [];
$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));
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));
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])) {
$recipe = new Recipe($package, $name, $job, $manifests[$name], $locks[$name] ?? []);
if ('symfony-pack' === $package->getType()) {
$packRecipes[$name] = $recipe;
} elseif ('metapackage' === $package->getType()) {
$metaRecipes[$name] = $recipe;
} elseif ('symfony/flex' === $name) {
$flexRecipe = [$name => $recipe];
} else {
$recipes[$name] = $recipe;
}
} else {
$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);
if ($operation instanceof InstallOperation) {
$this->lock->set($name, ['version' => $package->getPrettyVersion()]);
}
}
}
}
return array_merge($flexRecipe, $packRecipes, $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 || $this->reinstall) {
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 updateComposerLock()
{
$lock = substr(Factory::getComposerFile(), 0, -4).'lock';
$composerJson = file_get_contents(Factory::getComposerFile());
$lockFile = new JsonFile($lock, null, $this->io);
$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)
{
$this->reinstall = false;
$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 (method_exists($installer, 'setPlatformRequirementFilter')) {
$installer->setPlatformRequirementFilter(((array) $this->installer)["\0*\0platformRequirementFilter"]);
}
if (!$update) {
$installer->setUpdateAllowList(['php']);
}
$installer->run();
$this->io->write($this->postInstallOutput);
$this->postInstallOutput = [];
}
public static function getSubscribedEvents(): array
{
if (!self::$activated) {
return [];
}
$events = [
PackageEvents::POST_PACKAGE_UPDATE => 'enableThanksReminder',
PackageEvents::POST_PACKAGE_INSTALL => 'recordFlexInstall',
PackageEvents::POST_PACKAGE_UNINSTALL => 'record',
InstallerEvents::PRE_OPERATIONS_EXEC => 'recordOperations',
PluginEvents::PRE_POOL_CREATE => 'truncatePackages',
ScriptEvents::POST_CREATE_PROJECT_CMD => 'configureProject',
ScriptEvents::POST_INSTALL_CMD => 'install',
ScriptEvents::PRE_UPDATE_CMD => 'configureInstaller',
ScriptEvents::POST_UPDATE_CMD => 'update',
'auto-scripts' => 'executeAutoScripts',
];
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;
}
}

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

@@ -0,0 +1,200 @@
<?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.'+is:pull-request');
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)
{
$contents = $this->downloader->get($path)->getBody();
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,95 @@
<?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}
*
* @return string
*/
public function getOperationType()
{
return 'information';
}
/**
* {@inheritdoc}
*
* @return string
*/
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,155 @@
<?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]))
|| ('symfony/psr-http-message-bridge' === $name && 6.4 > $versions[0])
)) {
$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,403 @@
<?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\Json\JsonFile;
use Composer\Json\JsonManipulator;
use Composer\Semver\Semver;
use Composer\Semver\VersionParser;
use Seld\JsonLint\ParsingException;
/**
* Synchronize package.json files detected in installed PHP packages with
* the current application.
*/
class PackageJsonSynchronizer
{
private $rootDir;
private $vendorDir;
private $scriptExecutor;
private $io;
private $versionParser;
public function __construct(string $rootDir, string $vendorDir, ScriptExecutor $scriptExecutor, IOInterface $io)
{
$this->rootDir = $rootDir;
$this->vendorDir = $vendorDir;
$this->scriptExecutor = $scriptExecutor;
$this->io = $io;
$this->versionParser = new VersionParser();
}
public function shouldSynchronize(): bool
{
return $this->rootDir && (file_exists($this->rootDir.'/package.json') || file_exists($this->rootDir.'/importmap.php'));
}
public function synchronize(array $phpPackages): bool
{
if (file_exists($this->rootDir.'/importmap.php')) {
$this->synchronizeForAssetMapper($phpPackages);
return false;
}
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 = [];
$phpPackages = $this->normalizePhpPackages($phpPackages);
foreach ($phpPackages as $phpPackage) {
foreach ($this->resolvePackageJsonDependencies($phpPackage) as $dependency => $constraint) {
$dependencies[$dependency][$phpPackage['name']] = $constraint;
}
}
$didChangePackageJson = $this->registerDependenciesInPackageJson($dependencies) || $didChangePackageJson;
// Register controllers and entrypoints in controllers.json
$this->updateControllersJsonFile($phpPackages);
return $didChangePackageJson;
}
private function synchronizeForAssetMapper(array $phpPackages): void
{
$importMapEntries = [];
$phpPackages = $this->normalizePhpPackages($phpPackages);
foreach ($phpPackages as $phpPackage) {
foreach ($this->resolveImportMapPackages($phpPackage) as $name => $dependencyConfig) {
$importMapEntries[$name] = $dependencyConfig;
}
}
$this->updateImportMap($importMapEntries);
$this->updateControllersJsonFile($phpPackages);
}
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 resolvePackageJsonDependencies($phpPackage): array
{
$dependencies = [];
if (!$packageJson = $this->resolvePackageJson($phpPackage)) {
return $dependencies;
}
if ($packageJson->read()['symfony']['needsPackageAsADependency'] ?? true) {
$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 resolveImportMapPackages($phpPackage): array
{
if (!$packageJson = $this->resolvePackageJson($phpPackage)) {
return [];
}
$dependencies = [];
foreach ($packageJson->read()['symfony']['importmap'] ?? [] as $importMapName => $constraintConfig) {
if (\is_array($constraintConfig)) {
$constraint = $constraintConfig['version'] ?? [];
$package = $constraintConfig['package'] ?? $importMapName;
} else {
$constraint = $constraintConfig;
$package = $importMapName;
}
if (0 === strpos($constraint, 'path:')) {
$path = substr($constraint, 5);
$path = str_replace('%PACKAGE%', \dirname($packageJson->getPath()), $path);
$dependencies[$importMapName] = [
'path' => $path,
];
continue;
}
$dependencies[$importMapName] = [
'version' => $constraint,
'package' => $package,
];
}
return $dependencies;
}
private function registerDependenciesInPackageJson(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]) {
if ($this->shouldUpdateConstraint($content[$parentNode][$dependency], $constraint)) {
$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 shouldUpdateConstraint(string $existingConstraint, string $constraint)
{
try {
$existingConstraint = $this->versionParser->parseConstraints($existingConstraint);
$constraint = $this->versionParser->parseConstraints($constraint);
return !$existingConstraint->matches($constraint);
} catch (\UnexpectedValueException $e) {
return true;
}
}
/**
* @param array<string, array{path?: string, package?: string, version?: string}> $importMapEntries
*/
private function updateImportMap(array $importMapEntries): void
{
if (!$importMapEntries) {
return;
}
$importMapData = include $this->rootDir.'/importmap.php';
foreach ($importMapEntries as $name => $importMapEntry) {
if (isset($importMapData[$name])) {
if (!isset($importMapData[$name]['version'])) {
// AssetMapper 6.3
continue;
}
$version = $importMapData[$name]['version'];
$versionConstraint = $importMapEntry['version'] ?? null;
// if the version constraint is satisfied, skip - else, update the package
if (Semver::satisfies($version, $versionConstraint)) {
continue;
}
$this->io->writeError(sprintf('Updating package <comment>%s</> from <info>%s</> to <info>%s</>.', $name, $version, $versionConstraint));
}
if (isset($importMapEntry['path'])) {
$arguments = [$name, '--path='.$importMapEntry['path']];
$this->scriptExecutor->execute(
'symfony-cmd',
'importmap:require',
$arguments
);
continue;
}
if (isset($importMapEntry['version'])) {
$packageName = $importMapEntry['package'].'@'.$importMapEntry['version'];
if ($importMapEntry['package'] !== $name) {
$packageName .= '='.$name;
}
$arguments = [$packageName];
$this->scriptExecutor->execute(
'symfony-cmd',
'importmap:require',
$arguments
);
continue;
}
throw new \InvalidArgumentException(sprintf('Invalid importmap entry: "%s".', var_export($importMapEntry, true)));
}
}
private function updateControllersJsonFile(array $phpPackages)
{
if (!file_exists($controllersJsonPath = $this->rootDir.'/assets/controllers.json')) {
return;
}
try {
$previousControllersJson = (new JsonFile($controllersJsonPath))->read();
} catch (ParsingException $e) {
// if controllers.json is invalid (possible during a recipe upgrade), we can't update the file
return;
}
$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;
}
private function normalizePhpPackages(array $phpPackages): array
{
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'],
];
}
}
return $phpPackages;
}
}

View File

@@ -0,0 +1,151 @@
<?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, $isRequire);
$version = substr($argument, $pos + 1);
$packages[] = $package.':'.$version;
} else {
$packages[] = $this->resolvePackageName($argument, $i, $isRequire);
}
}
// 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, bool $isRequire): string
{
$skippedPackages = ['mirrors', 'nothing', ''];
if (!$isRequire) {
$skippedPackages[] = 'lock';
}
if (false !== strpos($argument, '/') || preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $argument) || preg_match('{(?<=[a-z0-9_/-])\*|\*(?=[a-z0-9_/-])}i', $argument) || \in_array($argument, $skippedPackages)) {
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);
}
}

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

90
vendor/symfony/flex/src/Response.php vendored Normal file
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;
/**
* @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;
}
/**
* @return mixed
*/
#[\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,139 @@
<?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\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, array $arguments = [])
{
$parsedCmd = $this->options->expandTargetDir($cmd);
if (null === $expandedCmd = $this->expandCmd($type, $parsedCmd, $arguments)) {
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, array $arguments)
{
switch ($type) {
case 'symfony-cmd':
return $this->expandSymfonyCmd($cmd, $arguments);
case 'php-script':
return $this->expandPhpScript($cmd, $arguments);
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, array $arguments)
{
$repo = $this->composer->getRepositoryManager()->getLocalRepository();
if (!$repo->findPackage('symfony/console', new MatchAllConstraint())) {
$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, $arguments);
}
private function expandPhpScript(string $cmd, array $scriptArguments): 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;
}
if ($memoryLimit = (string) getenv('COMPOSER_MEMORY_LIMIT')) {
$arguments[] = "-d memory_limit={$memoryLimit}";
}
$phpArgs = implode(' ', array_map([ProcessExecutor::class, 'escape'], $arguments));
$scriptArgs = implode(' ', array_map([ProcessExecutor::class, 'escape'], $scriptArguments));
return ProcessExecutor::escape($php).($phpArgs ? ' '.$phpArgs : '').' '.$cmd.($scriptArgs ? ' '.$scriptArgs : '');
}
}

View File

@@ -0,0 +1,115 @@
<?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
$classContents = file_get_contents($classPath);
return (false !== strpos($classContents, 'Symfony\Component\HttpKernel\Bundle\Bundle'))
|| (false !== strpos($classContents, 'Symfony\Component\HttpKernel\Bundle\AbstractBundle'));
}
}

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

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

@@ -0,0 +1,208 @@
<?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\Factory;
use Composer\IO\IOInterface;
use Composer\Json\JsonFile;
use Composer\Json\JsonManipulator;
use Composer\Package\Locker;
use Composer\Package\Version\VersionSelector;
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 = new RepositorySet($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/
$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,52 @@
<?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 $deletedFiles;
private $removedPatches;
public function __construct(string $patch, array $blobs, array $deletedFiles, array $removedPatches = [])
{
$this->patch = $patch;
$this->blobs = $blobs;
$this->deletedFiles = $deletedFiles;
$this->removedPatches = $removedPatches;
}
public function getPatch(): string
{
return $this->patch;
}
public function getBlobs(): array
{
return $this->blobs;
}
public function getDeletedFiles(): array
{
return $this->deletedFiles;
}
/**
* 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,259 @@
<?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
{
$withConflicts = $this->_applyPatchFile($patch);
foreach ($patch->getDeletedFiles() as $deletedFile) {
if (file_exists($this->rootDir.'/'.$deletedFile)) {
$this->execute(sprintf('git rm %s', ProcessExecutor::escape($deletedFile)), $this->rootDir);
}
}
return $withConflicts;
}
public function generatePatch(array $originalFiles, array $newFiles): RecipePatch
{
$ignoredFiles = $this->getIgnoredFiles(array_keys($originalFiles) + array_keys($newFiles));
// null implies "file does not exist"
$originalFiles = array_filter($originalFiles, function ($file, $fileName) use ($ignoredFiles) {
return null !== $file && !\in_array($fileName, $ignoredFiles);
}, \ARRAY_FILTER_USE_BOTH);
$newFiles = array_filter($newFiles, function ($file, $fileName) use ($ignoredFiles) {
return null !== $file && !\in_array($fileName, $ignoredFiles);
}, \ARRAY_FILTER_USE_BOTH);
$deletedFiles = [];
// find removed files & record that they are deleted
// unset them from originalFiles to avoid unnecessary blobs being added
foreach ($originalFiles as $file => $contents) {
if (!isset($newFiles[$file])) {
$deletedFiles[] = $file;
unset($originalFiles[$file]);
}
}
// 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;
}
}
// Use git binary to get project path from repository root
$prefix = trim($this->execute('git rev-parse --show-prefix', $this->rootDir));
$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(sprintf('git diff --cached --src-prefix "a/%s" --dst-prefix "b/%s"', $prefix, $prefix), $tmpPath);
$removedPatches = [];
$patchString = DiffHelper::removeFilesFromPatch($patchString, $deletedModifiedFiles, $removedPatches);
return new RecipePatch(
$patchString,
$blobs,
$deletedFiles,
$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->getBlobPath($this->rootDir, $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($this->getBlobPath($originalFilesRoot, $hash));
}
return $addedBlobs;
}
private function getBlobPath(string $gitRoot, string $hash): string
{
$gitDir = trim($this->execute('git rev-parse --absolute-git-dir', $gitRoot));
$hashStart = substr($hash, 0, 2);
$hashEnd = substr($hash, 2);
return $gitDir.'/objects/'.$hashStart.'/'.$hashEnd;
}
private function _applyPatchFile(RecipePatch $patch)
{
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);
}
}
}
private function getIgnoredFiles(array $fileNames): array
{
$args = implode(' ', array_map([ProcessExecutor::class, 'escape'], $fileNames));
$output = '';
$this->processExecutor->execute(sprintf('git check-ignore %s', $args), $output, $this->rootDir);
return $this->processExecutor->splitLines($output);
}
}

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