first commit

This commit is contained in:
Sampanna Rimal
2024-08-27 17:48:06 +05:45
commit 53c0140f58
10839 changed files with 1125847 additions and 0 deletions

View File

@ -0,0 +1,750 @@
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge\V2;
use Composer\Composer;
use Composer\Json\JsonFile;
use Composer\Package\BasePackage;
use Composer\Package\CompletePackage;
use Composer\Package\Link;
use Composer\Package\Loader\ArrayLoader;
use Composer\Package\RootAliasPackage;
use Composer\Package\RootPackage;
use Composer\Package\RootPackageInterface;
use Composer\Package\Version\VersionParser;
use Composer\Semver\Intervals;
use UnexpectedValueException;
/**
* Processing for a composer.json file that will be merged into
* a RootPackageInterface
*
* @author Bryan Davis <bd808@bd808.com>
*/
class ExtraPackage
{
/**
* @var Composer $composer
*/
protected $composer;
/**
* @var Logger $logger
*/
protected $logger;
/**
* @var string $path
*/
protected $path;
/**
* @var array $json
*/
protected $json;
/**
* @var CompletePackage $package
*/
protected $package;
/**
* @var array<string, bool> $mergedRequirements
*/
protected $mergedRequirements = [];
/**
* @var VersionParser $versionParser
*/
protected $versionParser;
/**
* @param string $path Path to composer.json file
* @param Composer $composer
* @param Logger $logger
*/
public function __construct($path, Composer $composer, Logger $logger)
{
$this->path = $path;
$this->composer = $composer;
$this->logger = $logger;
$this->json = $this->readPackageJson($path);
$this->package = $this->loadPackage($this->json);
$this->versionParser = new VersionParser();
}
/**
* Get list of additional packages to include if precessing recursively.
*
* @return array
*/
public function getIncludes()
{
return isset($this->json['extra']['merge-plugin']['include']) ?
$this->fixRelativePaths($this->json['extra']['merge-plugin']['include']) : [];
}
/**
* Get list of additional packages to require if precessing recursively.
*
* @return array
*/
public function getRequires()
{
return isset($this->json['extra']['merge-plugin']['require']) ?
$this->fixRelativePaths($this->json['extra']['merge-plugin']['require']) : [];
}
/**
* Get list of merged requirements from this package.
*
* @return string[]
*/
public function getMergedRequirements()
{
return array_keys($this->mergedRequirements);
}
/**
* Read the contents of a composer.json style file into an array.
*
* The package contents are fixed up to be usable to create a Package
* object by providing dummy "name" and "version" values if they have not
* been provided in the file. This is consistent with the default root
* package loading behavior of Composer.
*
* @param string $path
* @return array
*/
protected function readPackageJson($path)
{
$file = new JsonFile($path);
$json = $file->read();
if (!isset($json['name'])) {
$json['name'] = 'merge-plugin/' .
strtr($path, DIRECTORY_SEPARATOR, '-');
}
if (!isset($json['version'])) {
$json['version'] = '1.0.0';
}
return $json;
}
/**
* @param array $json
* @return CompletePackage
*/
protected function loadPackage(array $json)
{
$loader = new ArrayLoader();
$package = $loader->load($json);
// @codeCoverageIgnoreStart
if (!$package instanceof CompletePackage) {
throw new UnexpectedValueException(
'Expected instance of CompletePackage, got ' .
get_class($package)
);
}
// @codeCoverageIgnoreEnd
return $package;
}
/**
* Merge this package into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param PluginState $state
*/
public function mergeInto(RootPackageInterface $root, PluginState $state)
{
$this->prependRepositories($root);
$this->mergeRequires('require', $root, $state);
$this->mergePackageLinks('conflict', $root);
if ($state->shouldMergeReplace()) {
$this->mergePackageLinks('replace', $root);
}
$this->mergePackageLinks('provide', $root);
$this->mergeSuggests($root);
$this->mergeAutoload('autoload', $root);
$this->mergeExtra($root, $state);
$this->mergeScripts($root, $state);
if ($state->isDevMode()) {
$this->mergeDevInto($root, $state);
} else {
$this->mergeReferences($root);
$this->mergeAliases($root);
}
}
/**
* Merge just the dev portion into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param PluginState $state
*/
public function mergeDevInto(RootPackageInterface $root, PluginState $state)
{
$this->mergeRequires('require-dev', $root, $state);
$this->mergeAutoload('devAutoload', $root);
$this->mergeReferences($root);
$this->mergeAliases($root);
}
/**
* Add a collection of repositories described by the given configuration
* to the given package and the global repository manager.
*
* @param RootPackageInterface $root
*/
protected function prependRepositories(RootPackageInterface $root)
{
if (!isset($this->json['repositories'])) {
return;
}
$repoManager = $this->composer->getRepositoryManager();
$newRepos = [];
foreach ($this->json['repositories'] as $repoJson) {
if (!isset($repoJson['type'])) {
continue;
}
if ($repoJson['type'] === 'path' && isset($repoJson['url'])) {
$repoJson['url'] = $this->fixRelativePaths(array($repoJson['url']))[0];
}
$this->logger->info("Prepending {$repoJson['type']} repository");
$repo = $repoManager->createRepository(
$repoJson['type'],
$repoJson
);
$repoManager->prependRepository($repo);
$newRepos[] = $repo;
}
$unwrapped = self::unwrapIfNeeded($root, 'setRepositories');
$unwrapped->setRepositories(array_merge(
$newRepos,
$root->getRepositories()
));
}
/**
* Merge require or require-dev into a RootPackageInterface
*
* @param string $type 'require' or 'require-dev'
* @param RootPackageInterface $root
* @param PluginState $state
*/
protected function mergeRequires(
$type,
RootPackageInterface $root,
PluginState $state
) {
$linkType = BasePackage::$supportedLinkTypes[$type];
$getter = 'get' . ucfirst($linkType['method']);
$setter = 'set' . ucfirst($linkType['method']);
$requires = $this->package->{$getter}();
if (empty($requires)) {
return;
}
$this->mergeStabilityFlags($root, $requires);
$requires = $this->replaceSelfVersionDependencies(
$type,
$requires,
$root
);
$root->{$setter}($this->mergeOrDefer(
$type,
$root->{$getter}(),
$requires,
$state
));
}
/**
* Merge two collections of package links and collect duplicates for
* subsequent processing.
*
* @param string $type 'require' or 'require-dev'
* @param array $origin Primary collection
* @param array $merge Additional collection
* @param PluginState $state
* @return array Merged collection
*/
protected function mergeOrDefer(
$type,
array $origin,
array $merge,
PluginState $state
) {
if ($state->ignoreDuplicateLinks() && $state->replaceDuplicateLinks()) {
$this->logger->warning("Both replace and ignore-duplicates are true. These are mutually exclusive.");
$this->logger->warning("Duplicate packages will be ignored.");
}
foreach ($merge as $name => $link) {
if (isset($origin[$name])) {
if ($state->ignoreDuplicateLinks()) {
$this->logger->info("Ignoring duplicate <comment>{$name}</comment>");
continue;
}
if ($state->replaceDuplicateLinks()) {
$this->logger->info("Replacing <comment>{$name}</comment>");
$this->mergedRequirements[$name] = true;
$origin[$name] = $link;
} else {
$this->logger->info("Merging <comment>{$name}</comment>");
$this->mergedRequirements[$name] = true;
$origin[$name] = $this->mergeConstraints($origin[$name], $link, $state);
}
} else {
$this->logger->info("Adding <comment>{$name}</comment>");
$this->mergedRequirements[$name] = true;
$origin[$name] = $link;
}
}
if (!$state->isComposer1()) {
Intervals::clear();
}
return $origin;
}
/**
* Merge package constraints.
*
* Adapted from Composer's UpdateCommand::appendConstraintToLink
*
* @param Link $origin The base package link.
* @param Link $merge The related package link to merge.
* @param PluginState $state
* @return Link Merged link.
*/
protected function mergeConstraints(Link $origin, Link $merge, PluginState $state)
{
$oldPrettyString = $origin->getConstraint()->getPrettyString();
$newPrettyString = $merge->getConstraint()->getPrettyString();
if ($state->isComposer1()) {
$constraintClass = MultiConstraint::class;
} else {
$constraintClass = \Composer\Semver\Constraint\MultiConstraint::class;
if (Intervals::isSubsetOf($origin->getConstraint(), $merge->getConstraint())) {
return $origin;
}
if (Intervals::isSubsetOf($merge->getConstraint(), $origin->getConstraint())) {
return $merge;
}
}
$newConstraint = $constraintClass::create([
$origin->getConstraint(),
$merge->getConstraint()
], true);
$newConstraint->setPrettyString($oldPrettyString.', '.$newPrettyString);
return new Link(
$origin->getSource(),
$origin->getTarget(),
$newConstraint,
$origin->getDescription(),
$origin->getPrettyConstraint() . ', ' . $newPrettyString
);
}
/**
* Merge autoload or autoload-dev into a RootPackageInterface
*
* @param string $type 'autoload' or 'devAutoload'
* @param RootPackageInterface $root
*/
protected function mergeAutoload($type, RootPackageInterface $root)
{
$getter = 'get' . ucfirst($type);
$setter = 'set' . ucfirst($type);
$autoload = $this->package->{$getter}();
if (empty($autoload)) {
return;
}
$unwrapped = self::unwrapIfNeeded($root, $setter);
$unwrapped->{$setter}(array_merge_recursive(
$root->{$getter}(),
$this->fixRelativePaths($autoload)
));
}
/**
* Fix a collection of paths that are relative to this package to be
* relative to the base package.
*
* @param array $paths
* @return array
*/
protected function fixRelativePaths(array $paths)
{
$base = dirname($this->path);
$base = ($base === '.') ? '' : "{$base}/";
array_walk_recursive(
$paths,
function (&$path) use ($base) {
$path = "{$base}{$path}";
}
);
return $paths;
}
/**
* Extract and merge stability flags from the given collection of
* requires and merge them into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param array $requires
*/
protected function mergeStabilityFlags(
RootPackageInterface $root,
array $requires
) {
$flags = $root->getStabilityFlags();
$sf = new StabilityFlags($flags, $root->getMinimumStability());
$unwrapped = self::unwrapIfNeeded($root, 'setStabilityFlags');
$unwrapped->setStabilityFlags(array_merge(
$flags,
$sf->extractAll($requires)
));
}
/**
* Merge package links of the given type into a RootPackageInterface
*
* @param string $type 'conflict', 'replace' or 'provide'
* @param RootPackageInterface $root
*/
protected function mergePackageLinks($type, RootPackageInterface $root)
{
$linkType = BasePackage::$supportedLinkTypes[$type];
$getter = 'get' . ucfirst($linkType['method']);
$setter = 'set' . ucfirst($linkType['method']);
$links = $this->package->{$getter}();
if (!empty($links)) {
$unwrapped = self::unwrapIfNeeded($root, $setter);
// @codeCoverageIgnoreStart
if ($root !== $unwrapped) {
$this->logger->warning(
'This Composer version does not support ' .
"'{$type}' merging for aliased packages."
);
}
// @codeCoverageIgnoreEnd
$unwrapped->{$setter}(array_merge(
$root->{$getter}(),
$this->replaceSelfVersionDependencies($type, $links, $root)
));
}
}
/**
* Merge suggested packages into a RootPackageInterface
*
* @param RootPackageInterface $root
*/
protected function mergeSuggests(RootPackageInterface $root)
{
$suggests = $this->package->getSuggests();
if (!empty($suggests)) {
$unwrapped = self::unwrapIfNeeded($root, 'setSuggests');
$unwrapped->setSuggests(array_merge(
$root->getSuggests(),
$suggests
));
}
}
/**
* Merge extra config into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param PluginState $state
*/
public function mergeExtra(RootPackageInterface $root, PluginState $state)
{
$extra = $this->package->getExtra();
unset($extra['merge-plugin']);
if (!$state->shouldMergeExtra() || empty($extra)) {
return;
}
$rootExtra = $root->getExtra();
$unwrapped = self::unwrapIfNeeded($root, 'setExtra');
if ($state->replaceDuplicateLinks()) {
$unwrapped->setExtra(
self::mergeExtraArray($state->shouldMergeExtraDeep(), $rootExtra, $extra)
);
} else {
if (!$state->shouldMergeExtraDeep()) {
foreach (array_intersect(
array_keys($extra),
array_keys($rootExtra)
) as $key) {
$this->logger->info(
"Ignoring duplicate <comment>{$key}</comment> in ".
"<comment>{$this->path}</comment> extra config."
);
}
}
$unwrapped->setExtra(
self::mergeExtraArray($state->shouldMergeExtraDeep(), $extra, $rootExtra)
);
}
}
/**
* Merge scripts config into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param PluginState $state
*/
public function mergeScripts(RootPackageInterface $root, PluginState $state)
{
$scripts = $this->package->getScripts();
if (!$state->shouldMergeScripts() || empty($scripts)) {
return;
}
$rootScripts = $root->getScripts();
$unwrapped = self::unwrapIfNeeded($root, 'setScripts');
if ($state->replaceDuplicateLinks()) {
$unwrapped->setScripts(
array_merge($rootScripts, $scripts)
);
} else {
$unwrapped->setScripts(
array_merge($scripts, $rootScripts)
);
}
}
/**
* Merges two arrays either via arrayMergeDeep or via array_merge.
*
* @param bool $mergeDeep
* @param array $array1
* @param array $array2
* @return array
*/
public static function mergeExtraArray($mergeDeep, $array1, $array2)
{
if ($mergeDeep) {
return NestedArray::mergeDeep($array1, $array2);
}
return array_merge($array1, $array2);
}
/**
* Update Links with a 'self.version' constraint with the root package's
* version.
*
* @param string $type Link type
* @param array $links
* @param RootPackageInterface $root
* @return array
*/
protected function replaceSelfVersionDependencies(
$type,
array $links,
RootPackageInterface $root
) {
$linkType = BasePackage::$supportedLinkTypes[$type];
$version = $root->getVersion();
$prettyVersion = $root->getPrettyVersion();
$vp = $this->versionParser;
$method = 'get' . ucfirst($linkType['method']);
$packages = $root->$method();
return array_map(
static function ($link) use ($linkType, $version, $prettyVersion, $vp, $packages) {
if ('self.version' === $link->getPrettyConstraint()) {
if (isset($packages[$link->getSource()])) {
/** @var Link $package */
$package = $packages[$link->getSource()];
return new Link(
$link->getSource(),
$link->getTarget(),
$vp->parseConstraints($package->getConstraint()->getPrettyString()),
$linkType['description'],
$package->getPrettyConstraint()
);
}
return new Link(
$link->getSource(),
$link->getTarget(),
$vp->parseConstraints($version),
$linkType['description'],
$prettyVersion
);
}
return $link;
},
$links
);
}
/**
* Get a full featured Package from a RootPackageInterface.
*
* In Composer versions before 599ad77 the RootPackageInterface only
* defines a sub-set of operations needed by composer-merge-plugin and
* RootAliasPackage only implemented those methods defined by the
* interface. Most of the unimplemented methods in RootAliasPackage can be
* worked around because the getter methods that are implemented proxy to
* the aliased package which we can modify by unwrapping. The exception
* being modifying the 'conflicts', 'provides' and 'replaces' collections.
* We have no way to actually modify those collections unfortunately in
* older versions of Composer.
*
* @param RootPackageInterface $root
* @param string $method Method needed
* @return RootPackageInterface|RootPackage
*/
public static function unwrapIfNeeded(
RootPackageInterface $root,
$method = 'setExtra'
) {
// @codeCoverageIgnoreStart
if ($root instanceof RootAliasPackage &&
!method_exists($root, $method)
) {
// Unwrap and return the aliased RootPackage.
$root = $root->getAliasOf();
}
// @codeCoverageIgnoreEnd
return $root;
}
protected function mergeAliases(RootPackageInterface $root)
{
$aliases = [];
$unwrapped = self::unwrapIfNeeded($root, 'setAliases');
foreach (array('require', 'require-dev') as $linkType) {
$linkInfo = BasePackage::$supportedLinkTypes[$linkType];
$method = 'get'.ucfirst($linkInfo['method']);
$links = [];
foreach ($unwrapped->$method() as $link) {
$links[$link->getTarget()] = $link->getConstraint()->getPrettyString();
}
$aliases = $this->extractAliases($links, $aliases);
}
$unwrapped->setAliases($aliases);
}
/**
* Extract aliases from version constraints (dev-branch as 1.0.0).
*
* @param array $requires
* @param array $aliases
* @return array
* @see RootPackageLoader::extractAliases()
*/
protected function extractAliases(array $requires, array $aliases)
{
foreach ($requires as $reqName => $reqVersion) {
if (preg_match('{^([^,\s#]+)(?:#[^ ]+)? +as +([^,\s]+)$}', $reqVersion, $match)) {
$aliases[] = [
'package' => strtolower($reqName),
'version' => $this->versionParser->normalize($match[1], $reqVersion),
'alias' => $match[2],
'alias_normalized' => $this->versionParser->normalize($match[2], $reqVersion),
];
} elseif (strpos($reqVersion, ' as ') !== false) {
throw new UnexpectedValueException(
'Invalid alias definition in "'.$reqName.'": "'.$reqVersion.'". '
. 'Aliases should be in the form "exact-version as other-exact-version".'
);
}
}
return $aliases;
}
/**
* Update the root packages reference information.
*
* @param RootPackageInterface $root
*/
protected function mergeReferences(RootPackageInterface $root)
{
// Merge source reference information for merged packages.
// @see RootPackageLoader::load
$references = [];
$unwrapped = self::unwrapIfNeeded($root, 'setReferences');
foreach (['require', 'require-dev'] as $linkType) {
$linkInfo = BasePackage::$supportedLinkTypes[$linkType];
$method = 'get'.ucfirst($linkInfo['method']);
$links = [];
foreach ($unwrapped->$method() as $link) {
$links[$link->getTarget()] = $link->getConstraint()->getPrettyString();
}
$references = $this->extractReferences($links, $references);
}
$unwrapped->setReferences($references);
}
/**
* Extract vcs revision from version constraint (dev-master#abc123.
*
* @param array $requires
* @param array $references
* @return array
* @see RootPackageLoader::extractReferences()
*/
protected function extractReferences(array $requires, array $references)
{
foreach ($requires as $reqName => $reqVersion) {
$reqVersion = preg_replace('{^([^,\s@]+) as .+$}', '$1', $reqVersion);
$stabilityName = VersionParser::parseStability($reqVersion);
if ($stabilityName === 'dev' &&
preg_match('{^[^,\s@]+?#([a-f0-9]+)$}', $reqVersion, $match)
) {
$name = strtolower($reqName);
$references[$name] = $match[1];
}
}
return $references;
}
}
// vim:sw=4:ts=4:sts=4:et:

View File

@ -0,0 +1,102 @@
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge\V2;
use Composer\IO\IOInterface;
/**
* Simple logging wrapper for Composer\IO\IOInterface
*
* @author Bryan Davis <bd808@bd808.com>
*/
class Logger
{
/**
* @var string $name
*/
protected $name;
/**
* @var IOInterface $inputOutput
*/
protected $inputOutput;
/**
* @param string $name
* @param IOInterface $io
*/
public function __construct($name, IOInterface $io)
{
$this->name = $name;
$this->inputOutput = $io;
}
/**
* Log a debug message
*
* Messages will be output at the "very verbose" logging level (eg `-vv`
* needed on the Composer command).
*
* @param string $message
*/
public function debug($message)
{
if ($this->inputOutput->isVeryVerbose()) {
$message = " <info>[{$this->name}]</info> {$message}";
$this->log($message);
}
}
/**
* Log an informative message
*
* Messages will be output at the "verbose" logging level (eg `-v` needed
* on the Composer command).
*
* @param string $message
*/
public function info($message)
{
if ($this->inputOutput->isVerbose()) {
$message = " <info>[{$this->name}]</info> {$message}";
$this->log($message);
}
}
/**
* Log a warning message
*
* @param string $message
*/
public function warning($message)
{
$message = " <error>[{$this->name}]</error> {$message}";
$this->log($message);
}
/**
* Write a message
*
* @param string $message
*/
public function log($message)
{
if (method_exists($this->inputOutput, 'writeError')) {
$this->inputOutput->writeError($message);
} else {
// @codeCoverageIgnoreStart
// Backwards compatibility for Composer before cb336a5
$this->inputOutput->write($message);
// @codeCoverageIgnoreEnd
}
}
}
// vim:sw=4:ts=4:sts=4:et:

View File

@ -0,0 +1,396 @@
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge\V2;
use Composer\Composer;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\EventDispatcher\Event as BaseEvent;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\Factory;
use Composer\Installer;
use Composer\Installer\PackageEvent;
use Composer\Installer\PackageEvents;
use Composer\IO\IOInterface;
use Composer\Package\RootPackageInterface;
use Composer\Plugin\PluginEvents;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event as ScriptEvent;
use Composer\Script\ScriptEvents;
/**
* Composer plugin that allows merging multiple composer.json files.
*
* When installed, this plugin will look for a "merge-plugin" key in the
* composer configuration's "extra" section. The value for this key is
* a set of options configuring the plugin.
*
* An "include" setting is required. The value of this setting can be either
* a single value or an array of values. Each value is treated as a glob()
* pattern identifying additional composer.json style configuration files to
* merge into the configuration for the current compser execution.
*
* The "autoload", "autoload-dev", "conflict", "provide", "replace",
* "repositories", "require", "require-dev", and "suggest" sections of the
* found configuration files will be merged into the root package
* configuration as though they were directly included in the top-level
* composer.json file.
*
* If included files specify conflicting package versions for "require" or
* "require-dev", the normal Composer dependency solver process will be used
* to attempt to resolve the conflict. Specifying the 'replace' key as true will
* change this default behaviour so that the last-defined version of a package
* will win, allowing for force-overrides of package defines.
*
* By default the "extra" section is not merged. This can be enabled by
* setitng the 'merge-extra' key to true. In normal mode, when the same key is
* found in both the original and the imported extra section, the version in
* the original config is used and the imported version is skipped. If
* 'replace' mode is active, this behaviour changes so the imported version of
* the key is used, replacing the version in the original config.
*
*
* @code
* {
* "require": {
* "wikimedia/composer-merge-plugin": "dev-master"
* },
* "extra": {
* "merge-plugin": {
* "include": [
* "composer.local.json"
* ]
* }
* }
* }
* @endcode
*
* @author Bryan Davis <bd808@bd808.com>
*/
class MergePlugin implements PluginInterface, EventSubscriberInterface
{
/**
* Official package name
*/
public const PACKAGE_NAME = 'wikimedia/composer-merge-plugin';
/**
* Priority that plugin uses to register callbacks.
*/
private const CALLBACK_PRIORITY = 50000;
/**
* @var Composer $composer
*/
protected $composer;
/**
* @var PluginState $state
*/
protected $state;
/**
* @var Logger $logger
*/
protected $logger;
/**
* Files that have already been fully processed
*
* @var array<string, bool> $loaded
*/
protected $loaded = [];
/**
* Files that have already been partially processed
*
* @var array<string, bool> $loadedNoDev
*/
protected $loadedNoDev = [];
/**
* Nested packages to restrict update operations.
*
* @var array<string, bool> $updateAllowList
*/
protected $updateAllowList = [];
/**
* {@inheritdoc}
*/
public function activate(Composer $composer, IOInterface $io)
{
$this->composer = $composer;
$this->state = new PluginState($this->composer);
$this->logger = new Logger('merge-plugin', $io);
}
/**
* {@inheritdoc}
*/
public function deactivate(Composer $composer, IOInterface $io)
{
}
/**
* {@inheritdoc}
*/
public function uninstall(Composer $composer, IOInterface $io)
{
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [
PluginEvents::INIT =>
['onInit', self::CALLBACK_PRIORITY],
PackageEvents::POST_PACKAGE_INSTALL =>
['onPostPackageInstall', self::CALLBACK_PRIORITY],
ScriptEvents::POST_INSTALL_CMD =>
['onPostInstallOrUpdate', self::CALLBACK_PRIORITY],
ScriptEvents::POST_UPDATE_CMD =>
['onPostInstallOrUpdate', self::CALLBACK_PRIORITY],
ScriptEvents::PRE_AUTOLOAD_DUMP =>
['onInstallUpdateOrDump', self::CALLBACK_PRIORITY],
ScriptEvents::PRE_INSTALL_CMD =>
['onInstallUpdateOrDump', self::CALLBACK_PRIORITY],
ScriptEvents::PRE_UPDATE_CMD =>
['onInstallUpdateOrDump', self::CALLBACK_PRIORITY],
];
}
/**
* Get list of packages to restrict update operations.
*
* @return string[]
* @see \Composer\Installer::setUpdateAllowList()
*/
public function getUpdateAllowList()
{
return array_keys($this->updateAllowList);
}
/**
* Handle an event callback for initialization.
*
* @param BaseEvent $event
*/
public function onInit(BaseEvent $event)
{
$this->state->loadSettings();
// It is not possible to know if the user specified --dev or --no-dev
// so assume it is false. The dev section will be merged later when
// the other events fire.
$this->state->setDevMode(false);
$this->mergeFiles($this->state->getIncludes(), false);
$this->mergeFiles($this->state->getRequires(), true);
}
/**
* Handle an event callback for an install, update or dump command by
* checking for "merge-plugin" in the "extra" data and merging package
* contents if found.
*
* @param ScriptEvent $event
*/
public function onInstallUpdateOrDump(ScriptEvent $event)
{
$this->state->loadSettings();
$this->state->setDevMode($event->isDevMode());
$this->mergeFiles($this->state->getIncludes(), false);
$this->mergeFiles($this->state->getRequires(), true);
if ($event->getName() === ScriptEvents::PRE_AUTOLOAD_DUMP) {
$this->state->setDumpAutoloader(true);
$flags = $event->getFlags();
if (isset($flags['optimize'])) {
$this->state->setOptimizeAutoloader($flags['optimize']);
}
}
}
/**
* Find configuration files matching the configured glob patterns and
* merge their contents with the master package.
*
* @param array $patterns List of files/glob patterns
* @param bool $required Are the patterns required to match files?
* @throws MissingFileException when required and a pattern returns no
* results
*/
protected function mergeFiles(array $patterns, $required = false)
{
$root = $this->composer->getPackage();
$files = array_map(
static function ($files, $pattern) use ($required) {
if ($required && !$files) {
throw new MissingFileException(
"merge-plugin: No files matched required '{$pattern}'"
);
}
return $files;
},
array_map('glob', $patterns),
$patterns
);
foreach (array_reduce($files, 'array_merge', []) as $path) {
$this->mergeFile($root, $path);
}
}
/**
* Read a JSON file and merge its contents
*
* @param RootPackageInterface $root
* @param string $path
*/
protected function mergeFile(RootPackageInterface $root, $path)
{
if (isset($this->loaded[$path]) ||
(isset($this->loadedNoDev[$path]) && !$this->state->isDevMode())
) {
$this->logger->debug(
"Already merged <comment>$path</comment> completely"
);
return;
}
$package = new ExtraPackage($path, $this->composer, $this->logger);
if (isset($this->loadedNoDev[$path])) {
$this->logger->info(
"Loading -dev sections of <comment>{$path}</comment>..."
);
$package->mergeDevInto($root, $this->state);
} else {
$this->logger->info("Loading <comment>{$path}</comment>...");
$package->mergeInto($root, $this->state);
}
$requirements = $package->getMergedRequirements();
if (!empty($requirements)) {
$this->updateAllowList = array_replace(
$this->updateAllowList,
array_fill_keys($requirements, true)
);
}
if ($this->state->isDevMode()) {
$this->loaded[$path] = true;
} else {
$this->loadedNoDev[$path] = true;
}
if ($this->state->recurseIncludes()) {
$this->mergeFiles($package->getIncludes(), false);
$this->mergeFiles($package->getRequires(), true);
}
}
/**
* Handle an event callback following installation of a new package by
* checking to see if the package that was installed was our plugin.
*
* @param PackageEvent $event
*/
public function onPostPackageInstall(PackageEvent $event)
{
$op = $event->getOperation();
if ($op instanceof InstallOperation) {
$package = $op->getPackage()->getName();
if ($package === self::PACKAGE_NAME) {
$this->logger->info('composer-merge-plugin installed');
$this->state->setFirstInstall(true);
$this->state->setLocked(
$event->getComposer()->getLocker()->isLocked()
);
}
}
}
/**
* Handle an event callback following an install or update command. If our
* plugin was installed during the run then trigger an update command to
* process any merge-patterns in the current config.
*
* @param ScriptEvent $event
*/
public function onPostInstallOrUpdate(ScriptEvent $event)
{
// @codeCoverageIgnoreStart
if ($this->state->isFirstInstall()) {
$this->state->setFirstInstall(false);
$requirements = $this->getUpdateAllowList();
if (empty($requirements)) {
return;
}
$this->logger->log("\n".'<info>Running composer update to apply merge settings</info>');
$lockBackup = null;
$lock = null;
if (!$this->state->isComposer1()) {
$file = Factory::getComposerFile();
$lock = Factory::getLockFile($file);
if (file_exists($lock)) {
$lockBackup = file_get_contents($lock);
}
}
$config = $this->composer->getConfig();
$preferSource = $config->get('preferred-install') === 'source';
$preferDist = $config->get('preferred-install') === 'dist';
$installer = Installer::create(
$event->getIO(),
// Create a new Composer instance to ensure full processing of
// the merged files.
Factory::create($event->getIO(), null, false)
);
$installer->setPreferSource($preferSource);
$installer->setPreferDist($preferDist);
$installer->setDevMode($event->isDevMode());
$installer->setDumpAutoloader($this->state->shouldDumpAutoloader());
$installer->setOptimizeAutoloader(
$this->state->shouldOptimizeAutoloader()
);
$installer->setUpdate(true);
if ($this->state->isComposer1()) {
// setUpdateWhitelist() only exists in composer 1.x. Configure as to run phan against composer 2.x
// @phan-suppress-next-line PhanUndeclaredMethod
$installer->setUpdateWhitelist($requirements);
} else {
$installer->setUpdateAllowList($requirements);
}
$status = $installer->run();
if (( $status !== 0 ) && $lockBackup && $lock && !$this->state->isComposer1()) {
$this->logger->log(
"\n".'<error>'.
'Update to apply merge settings failed, reverting '.$lock.' to its original content.'.
'</error>'
);
file_put_contents($lock, $lockBackup);
}
}
// @codeCoverageIgnoreEnd
}
}
// vim:sw=4:ts=4:sts=4:et:

View File

@ -0,0 +1,20 @@
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge\V2;
use RuntimeException;
/**
* @author Bryan Davis <bd808@bd808.com>
*/
class MissingFileException extends RuntimeException
{
}

View File

@ -0,0 +1,114 @@
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2021 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge\V2;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Constraint\EmptyConstraint;
use Composer\Semver\Constraint\MultiConstraint as SemverMultiConstraint;
use function count;
/**
* Adapted from Composer's v2 MultiConstraint::create for Composer v1
* @link https://github.com/composer/semver/blob/3.2.4/src/Constraint/MultiConstraint.php
* @author Chauncey McAskill <chauncey@mcaskill.ca>
*/
class MultiConstraint extends SemverMultiConstraint
{
/**
* Tries to optimize the constraints as much as possible, meaning
* reducing/collapsing congruent constraints etc.
* Does not necessarily return a MultiConstraint instance if
* things can be reduced to a simple constraint
*
* @param ConstraintInterface[] $constraints A set of constraints
* @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive
*
* @return ConstraintInterface
*/
public static function create(array $constraints, $conjunctive = true)
{
if (count($constraints) === 0) {
// EmptyConstraint only exists in composer 1.x. Configure as to run phan against composer 2.x
// @phan-suppress-next-line PhanTypeMismatchReturn, PhanUndeclaredClassMethod
return new EmptyConstraint();
}
if (count($constraints) === 1) {
return $constraints[0];
}
$optimized = self::optimizeConstraints($constraints, $conjunctive);
if ($optimized !== null) {
list($constraints, $conjunctive) = $optimized;
if (count($constraints) === 1) {
return $constraints[0];
}
}
return new self($constraints, $conjunctive);
}
/**
* @return array|null
*/
private static function optimizeConstraints(array $constraints, $conjunctive)
{
// parse the two OR groups and if they are contiguous we collapse
// them into one constraint
// [>= 1 < 2] || [>= 2 < 3] || [>= 3 < 4] => [>= 1 < 4]
if (!$conjunctive) {
$left = $constraints[0];
$mergedConstraints = [];
$optimized = false;
for ($i = 1, $l = count($constraints); $i < $l; $i++) {
$right = $constraints[$i];
if ($left instanceof SemverMultiConstraint
&& $left->conjunctive
&& $right instanceof SemverMultiConstraint
&& $right->conjunctive
&& count($left->constraints) === 2
&& count($right->constraints) === 2
&& ($left0 = (string) $left->constraints[0])
&& $left0[0] === '>' && $left0[1] === '='
&& ($left1 = (string) $left->constraints[1])
&& $left1[0] === '<'
&& ($right0 = (string) $right->constraints[0])
&& $right0[0] === '>' && $right0[1] === '='
&& ($right1 = (string) $right->constraints[1])
&& $right1[0] === '<'
&& substr($left1, 2) === substr($right0, 3)
) {
$optimized = true;
$left = new MultiConstraint(
[
$left->constraints[0],
$right->constraints[1],
],
true
);
} else {
$mergedConstraints[] = $left;
$left = $right;
}
}
if ($optimized) {
$mergedConstraints[] = $left;
return [$mergedConstraints, false];
}
}
// TODO: Here's the place to put more optimizations
return null;
}
}
// vim:sw=4:ts=4:sts=4:et:

View File

@ -0,0 +1,114 @@
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge\V2;
/**
* Adapted from
* http://cgit.drupalcode.org/drupal/tree/core/lib/Drupal/Component/Utility/NestedArray.php
* @ f86a4d650d5af0b82a3981e09977055fa63f6f2e
*/
class NestedArray
{
/**
* Merges multiple arrays, recursively, and returns the merged array.
*
* This function is similar to PHP's array_merge_recursive() function, but
* it handles non-array values differently. When merging values that are
* not both arrays, the latter value replaces the former rather than
* merging with it.
*
* Example:
*
* @code
* $link_options_1 = ['fragment' => 'x', 'attributes' => ['title' => t('X'), 'class' => ['a', 'b']]];
* $link_options_2 = ['fragment' => 'y', 'attributes' => ['title' => t('Y'), 'class' => ['c', 'd']]];
*
* // This results in ['fragment' => ['x', 'y'], 'attributes' =>
* // ['title' => [t('X'), t('Y')], 'class' => ['a', 'b',
* // 'c', 'd']]].
* $incorrect = array_merge_recursive($link_options_1, $link_options_2);
*
* // This results in ['fragment' => 'y', 'attributes' =>
* // ['title' => t('Y'), 'class' => ['a', 'b', 'c', 'd']]].
* $correct = NestedArray::mergeDeep($link_options_1, $link_options_2);
* @endcode
*
* @param mixed ...$params Arrays to merge.
*
* @return array The merged array.
*
* @see NestedArray::mergeDeepArray()
*/
public static function mergeDeep(...$params)
{
return self::mergeDeepArray($params);
}
/**
* Merges multiple arrays, recursively, and returns the merged array.
*
* This function is equivalent to NestedArray::mergeDeep(), except the
* input arrays are passed as a single array parameter rather than
* a variable parameter list.
*
* The following are equivalent:
* - NestedArray::mergeDeep($a, $b);
* - NestedArray::mergeDeepArray([$a, $b]);
*
* The following are also equivalent:
* - call_user_func_array('NestedArray::mergeDeep', $arrays_to_merge);
* - NestedArray::mergeDeepArray($arrays_to_merge);
*
* @param array $arrays
* An arrays of arrays to merge.
* @param bool $preserveIntegerKeys
* (optional) If given, integer keys will be preserved and merged
* instead of appended. Defaults to false.
*
* @return array
* The merged array.
*
* @see NestedArray::mergeDeep()
*/
public static function mergeDeepArray(
array $arrays,
$preserveIntegerKeys = false
) {
$result = [];
foreach ($arrays as $array) {
foreach ($array as $key => $value) {
// Renumber integer keys as array_merge_recursive() does
// unless $preserveIntegerKeys is set to TRUE. Note that PHP
// automatically converts array keys that are integer strings
// (e.g., '1') to integers.
if (is_int($key) && !$preserveIntegerKeys) {
$result[] = $value;
} elseif (isset($result[$key]) &&
is_array($result[$key]) &&
is_array($value)
) {
// Recurse when both values are arrays.
$result[$key] = self::mergeDeepArray(
[$result[$key], $value],
$preserveIntegerKeys
);
} else {
// Otherwise, use the latter value, overriding any
// previous value.
$result[$key] = $value;
}
}
}
return $result;
}
}
// vim:sw=4:ts=4:sts=4:et:

View File

@ -0,0 +1,422 @@
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge\V2;
use Composer\Composer;
use Composer\Plugin\PluginInterface;
/**
* Mutable plugin state
*
* @author Bryan Davis <bd808@bd808.com>
*/
class PluginState
{
/**
* @var Composer $composer
*/
protected $composer;
/**
* @var bool $isComposer1
*/
protected $isComposer1;
/**
* @var array $includes
*/
protected $includes = [];
/**
* @var array $requires
*/
protected $requires = [];
/**
* @var bool $devMode
*/
protected $devMode = false;
/**
* @var bool $recurse
*/
protected $recurse = true;
/**
* @var bool $replace
*/
protected $replace = false;
/**
* @var bool $ignore
*/
protected $ignore = false;
/**
* Whether to merge the -dev sections.
* @var bool $mergeDev
*/
protected $mergeDev = true;
/**
* Whether to merge the extra section.
*
* By default, the extra section is not merged and there will be many
* cases where the merge of the extra section is performed too late
* to be of use to other plugins. When enabled, merging uses one of
* two strategies - either 'first wins' or 'last wins'. When enabled,
* 'first wins' is the default behaviour. If Replace mode is activated
* then 'last wins' is used.
*
* @var bool $mergeExtra
*/
protected $mergeExtra = false;
/**
* Whether to merge the extra section in a deep / recursive way.
*
* By default the extra section is merged with array_merge() and duplicate
* keys are ignored. When enabled this allows to merge the arrays recursively
* using the following rule: Integer keys are merged, while array values are
* replaced where the later values overwrite the former.
*
* This is useful especially for the extra section when plugins use larger
* structures like a 'patches' key with the packages as sub-keys and the
* patches as values.
*
* When 'replace' mode is activated the order of array merges is exchanged.
*
* @var bool $mergeExtraDeep
*/
protected $mergeExtraDeep = false;
/**
* Whether to merge the replace section.
*
* @var bool $mergeReplace
*/
protected $mergeReplace = true;
/**
* Whether to merge the scripts section.
*
* @var bool $mergeScripts
*/
protected $mergeScripts = false;
/**
* @var bool $firstInstall
*/
protected $firstInstall = false;
/**
* @var bool $locked
*/
protected $locked = false;
/**
* @var bool $dumpAutoloader
*/
protected $dumpAutoloader = false;
/**
* @var bool $optimizeAutoloader
*/
protected $optimizeAutoloader = false;
/**
* @param Composer $composer
*/
public function __construct(Composer $composer)
{
$this->composer = $composer;
$this->isComposer1 = version_compare(PluginInterface::PLUGIN_API_VERSION, '2.0.0', '<');
}
/**
* Test if this plugin runs within Composer 1.
*
* @return bool
*/
public function isComposer1()
{
return $this->isComposer1;
}
/**
* Load plugin settings
*/
public function loadSettings()
{
$extra = $this->composer->getPackage()->getExtra();
$config = array_merge(
[
'include' => [],
'require' => [],
'recurse' => true,
'replace' => false,
'ignore-duplicates' => false,
'merge-dev' => true,
'merge-extra' => false,
'merge-extra-deep' => false,
'merge-replace' => true,
'merge-scripts' => false,
],
$extra['merge-plugin'] ?? []
);
$this->includes = (is_array($config['include'])) ?
$config['include'] : [$config['include']];
$this->requires = (is_array($config['require'])) ?
$config['require'] : [$config['require']];
$this->recurse = (bool)$config['recurse'];
$this->replace = (bool)$config['replace'];
$this->ignore = (bool)$config['ignore-duplicates'];
$this->mergeDev = (bool)$config['merge-dev'];
$this->mergeExtra = (bool)$config['merge-extra'];
$this->mergeExtraDeep = (bool)$config['merge-extra-deep'];
$this->mergeReplace = (bool)$config['merge-replace'];
$this->mergeScripts = (bool)$config['merge-scripts'];
}
/**
* Get list of filenames and/or glob patterns to include
*
* @return array
*/
public function getIncludes()
{
return $this->includes;
}
/**
* Get list of filenames and/or glob patterns to require
*
* @return array
*/
public function getRequires()
{
return $this->requires;
}
/**
* Set the first install flag
*
* @param bool $flag
*/
public function setFirstInstall($flag)
{
$this->firstInstall = (bool)$flag;
}
/**
* Is this the first time that the plugin has been installed?
*
* @return bool
*/
public function isFirstInstall()
{
return $this->firstInstall;
}
/**
* Set the locked flag
*
* @param bool $flag
*/
public function setLocked($flag)
{
$this->locked = (bool)$flag;
}
/**
* Was a lockfile present when the plugin was installed?
*
* @return bool
*/
public function isLocked()
{
return $this->locked;
}
/**
* Should an update be forced?
*
* @return true If packages are not locked
*/
public function forceUpdate()
{
return !$this->locked;
}
/**
* Set the devMode flag
*
* @param bool $flag
*/
public function setDevMode($flag)
{
$this->devMode = (bool)$flag;
}
/**
* Should devMode settings be processed?
*
* @return bool
*/
public function isDevMode()
{
return $this->shouldMergeDev() && $this->devMode;
}
/**
* Should devMode settings be merged?
*
* @return bool
*/
public function shouldMergeDev()
{
return $this->mergeDev;
}
/**
* Set the dumpAutoloader flag
*
* @param bool $flag
*/
public function setDumpAutoloader($flag)
{
$this->dumpAutoloader = (bool)$flag;
}
/**
* Is the autoloader file supposed to be written out?
*
* @return bool
*/
public function shouldDumpAutoloader()
{
return $this->dumpAutoloader;
}
/**
* Set the optimizeAutoloader flag
*
* @param bool $flag
*/
public function setOptimizeAutoloader($flag)
{
$this->optimizeAutoloader = (bool)$flag;
}
/**
* Should the autoloader be optimized?
*
* @return bool
*/
public function shouldOptimizeAutoloader()
{
return $this->optimizeAutoloader;
}
/**
* Should includes be recursively processed?
*
* @return bool
*/
public function recurseIncludes()
{
return $this->recurse;
}
/**
* Should duplicate links be replaced in a 'last definition wins' order?
*
* @return bool
*/
public function replaceDuplicateLinks()
{
return $this->replace;
}
/**
* Should duplicate links be ignored?
*
* @return bool
*/
public function ignoreDuplicateLinks()
{
return $this->ignore;
}
/**
* Should the extra section be merged?
*
* By default, the extra section is not merged and there will be many
* cases where the merge of the extra section is performed too late
* to be of use to other plugins. When enabled, merging uses one of
* two strategies - either 'first wins' or 'last wins'. When enabled,
* 'first wins' is the default behaviour. If Replace mode is activated
* then 'last wins' is used.
*
* @return bool
*/
public function shouldMergeExtra()
{
return $this->mergeExtra;
}
/**
* Should the extra section be merged deep / recursively?
*
* By default the extra section is merged with array_merge() and duplicate
* keys are ignored. When enabled this allows to merge the arrays recursively
* using the following rule: Integer keys are merged, while array values are
* replaced where the later values overwrite the former.
*
* This is useful especially for the extra section when plugins use larger
* structures like a 'patches' key with the packages as sub-keys and the
* patches as values.
*
* When 'replace' mode is activated the order of array merges is exchanged.
*
* @return bool
*/
public function shouldMergeExtraDeep()
{
return $this->mergeExtraDeep;
}
/**
* Should the replace section be merged?
*
* By default, the replace section is merged.
*
* @return bool
*/
public function shouldMergeReplace()
{
return $this->mergeReplace;
}
/**
* Should the scripts section be merged?
*
* By default, the scripts section is not merged.
*
* @return bool
*/
public function shouldMergeScripts()
{
return $this->mergeScripts;
}
}
// vim:sw=4:ts=4:sts=4:et:

View File

@ -0,0 +1,173 @@
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge\V2;
use Composer\Package\BasePackage;
use Composer\Package\Version\VersionParser;
/**
* Adapted from Composer's RootPackageLoader::extractStabilityFlags
* @author Bryan Davis <bd808@bd808.com>
*/
class StabilityFlags
{
/**
* @var array Current package name => stability mappings
*/
protected $stabilityFlags;
/**
* @var int Current default minimum stability
*/
protected $minimumStability;
/**
* @var string Regex to extract an explicit stability flag (eg '@dev')
*/
protected $explicitStabilityRe;
/**
* @param array $stabilityFlags Current package name => stability mappings
* @param int|string $minimumStability Current default minimum stability
*/
public function __construct(
array $stabilityFlags = [],
$minimumStability = BasePackage::STABILITY_STABLE
) {
$this->stabilityFlags = $stabilityFlags;
$this->minimumStability = $this->getStabilityInt((string)$minimumStability);
$this->explicitStabilityRe = '/^[^@]*?@(' .
implode('|', array_keys(BasePackage::$stabilities)) .
')$/i';
}
/**
* Get the stability value for a given string.
*
* @param string $name Stability name
* @return int Stability value
*/
protected function getStabilityInt($name)
{
$name = VersionParser::normalizeStability($name);
return BasePackage::$stabilities[$name] ?? BasePackage::STABILITY_STABLE;
}
/**
* Extract and merge stability flags from the given collection of
* requires with another collection of stability flags.
*
* @param array $requires New package name => link mappings
* @return array Unified package name => stability mappings
*/
public function extractAll(array $requires)
{
$flags = [];
foreach ($requires as $name => $link) {
$name = strtolower($name);
$version = $link->getPrettyConstraint();
$stability = $this->getExplicitStability($version);
if ($stability === null) {
$stability = $this->getParsedStability($version);
}
$flags[$name] = max($stability, $this->getCurrentStability($name));
}
// Filter out null stability values
return array_filter($flags, function ($v) {
return $v !== null;
});
}
/**
* Extract the most unstable explicit stability (eg '@dev') from a version
* specification.
*
* @param string $version
* @return int|null Stability or null if no explict stability found
*/
protected function getExplicitStability($version)
{
$found = null;
$constraints = $this->splitConstraints($version);
foreach ($constraints as $constraint) {
if (preg_match($this->explicitStabilityRe, $constraint, $match)) {
$stability = $this->getStabilityInt($match[1]);
$found = max($stability, $found);
}
}
return $found;
}
/**
* Split a version specification into a list of version constraints.
*
* @param string $version
* @return array
*/
protected function splitConstraints($version)
{
$found = [];
$orConstraints = preg_split('/\s*\|\|?\s*/', trim($version));
foreach ($orConstraints as $constraints) {
$andConstraints = preg_split(
'/(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)/',
$constraints
);
foreach ($andConstraints as $constraint) {
$found[] = $constraint;
}
}
return $found;
}
/**
* Get the stability of a version
*
* @param string $version
* @return int|null Stability or null if STABLE or less than minimum
*/
protected function getParsedStability($version)
{
// Drop aliasing if used
$version = preg_replace('/^([^,\s@]+) as .+$/', '$1', $version);
$stability = $this->getStabilityInt(
VersionParser::parseStability($version)
);
if ($stability === BasePackage::STABILITY_STABLE ||
$this->minimumStability > $stability
) {
// Ignore if 'stable' or more stable than the global
// minimum
$stability = null;
}
return $stability;
}
/**
* Get the current stability of a given package.
*
* @param string $name
* @return int|null Stability of null if not set
*/
protected function getCurrentStability($name)
{
return $this->stabilityFlags[$name] ?? null;
}
}
// vim:sw=4:ts=4:sts=4:et: