1<?php
2
3/*
4 * This file is part of Composer.
5 *
6 * (c) Nils Adermann <naderman@naderman.de>
7 *     Jordi Boggiano <j.boggiano@seld.be>
8 *
9 * For the full copyright and license information, please view the LICENSE
10 * file that was distributed with this source code.
11 */
12
13namespace Composer\Package\Loader;
14
15use Composer\Package\BasePackage;
16use Composer\Package\AliasPackage;
17use Composer\Config;
18use Composer\Package\RootPackageInterface;
19use Composer\Repository\RepositoryFactory;
20use Composer\Package\Version\VersionGuesser;
21use Composer\Package\Version\VersionParser;
22use Composer\Repository\RepositoryManager;
23use Composer\Util\ProcessExecutor;
24
25/**
26 * ArrayLoader built for the sole purpose of loading the root package
27 *
28 * Sets additional defaults and loads repositories
29 *
30 * @author Jordi Boggiano <j.boggiano@seld.be>
31 */
32class RootPackageLoader extends ArrayLoader
33{
34    /**
35     * @var RepositoryManager
36     */
37    private $manager;
38
39    /**
40     * @var Config
41     */
42    private $config;
43
44    /**
45     * @var VersionGuesser
46     */
47    private $versionGuesser;
48
49    public function __construct(RepositoryManager $manager, Config $config, VersionParser $parser = null, VersionGuesser $versionGuesser = null)
50    {
51        parent::__construct($parser);
52
53        $this->manager = $manager;
54        $this->config = $config;
55        $this->versionGuesser = $versionGuesser ?: new VersionGuesser($config, new ProcessExecutor(), $this->versionParser);
56    }
57
58    /**
59     * @param  array                $config package data
60     * @param  string               $class  FQCN to be instantiated
61     * @param  string               $cwd    cwd of the root package to be used to guess the version if it is not provided
62     * @return RootPackageInterface
63     */
64    public function load(array $config, $class = 'Composer\Package\RootPackage', $cwd = null)
65    {
66        if (!isset($config['name'])) {
67            $config['name'] = '__root__';
68        }
69        $autoVersioned = false;
70        if (!isset($config['version'])) {
71            // override with env var if available
72            if (getenv('COMPOSER_ROOT_VERSION')) {
73                $version = getenv('COMPOSER_ROOT_VERSION');
74                $commit = null;
75            } else {
76                $versionData =  $this->versionGuesser->guessVersion($config, $cwd ?: getcwd());
77                $version = $versionData['version'];
78                $commit = $versionData['commit'];
79            }
80
81            if (!$version) {
82                $version = '1.0.0';
83                $autoVersioned = true;
84            }
85
86            $config['version'] = $version;
87            if ($commit) {
88                $config['source'] = array(
89                    'type' => '',
90                    'url' => '',
91                    'reference' => $commit,
92                );
93                $config['dist'] = array(
94                    'type' => '',
95                    'url' => '',
96                    'reference' => $commit,
97                );
98            }
99        }
100
101        $realPackage = $package = parent::load($config, $class);
102        if ($realPackage instanceof AliasPackage) {
103            $realPackage = $package->getAliasOf();
104        }
105
106        if ($autoVersioned) {
107            $realPackage->replaceVersion($realPackage->getVersion(), 'No version set (parsed as 1.0.0)');
108        }
109
110        if (isset($config['minimum-stability'])) {
111            $realPackage->setMinimumStability(VersionParser::normalizeStability($config['minimum-stability']));
112        }
113
114        $aliases = array();
115        $stabilityFlags = array();
116        $references = array();
117        foreach (array('require', 'require-dev') as $linkType) {
118            if (isset($config[$linkType])) {
119                $linkInfo = BasePackage::$supportedLinkTypes[$linkType];
120                $method = 'get'.ucfirst($linkInfo['method']);
121                $links = array();
122                foreach ($realPackage->$method() as $link) {
123                    $links[$link->getTarget()] = $link->getConstraint()->getPrettyString();
124                }
125                $aliases = $this->extractAliases($links, $aliases);
126                $stabilityFlags = $this->extractStabilityFlags($links, $stabilityFlags, $realPackage->getMinimumStability());
127                $references = $this->extractReferences($links, $references);
128            }
129        }
130
131        if (isset($links[$config['name']])) {
132            throw new \InvalidArgumentException(sprintf('Root package \'%s\' cannot require itself in its composer.json' . PHP_EOL .
133                        'Did you accidentally name your root package after an external package?', $config['name']));
134        }
135
136        $realPackage->setAliases($aliases);
137        $realPackage->setStabilityFlags($stabilityFlags);
138        $realPackage->setReferences($references);
139
140        if (isset($config['prefer-stable'])) {
141            $realPackage->setPreferStable((bool) $config['prefer-stable']);
142        }
143
144        if (isset($config['config'])) {
145            $realPackage->setConfig($config['config']);
146        }
147
148        $repos = RepositoryFactory::defaultRepos(null, $this->config, $this->manager);
149        foreach ($repos as $repo) {
150            $this->manager->addRepository($repo);
151        }
152        $realPackage->setRepositories($this->config->getRepositories());
153
154        return $package;
155    }
156
157    private function extractAliases(array $requires, array $aliases)
158    {
159        foreach ($requires as $reqName => $reqVersion) {
160            if (preg_match('{^([^,\s#]+)(?:#[^ ]+)? +as +([^,\s]+)$}', $reqVersion, $match)) {
161                $aliases[] = array(
162                    'package' => strtolower($reqName),
163                    'version' => $this->versionParser->normalize($match[1], $reqVersion),
164                    'alias' => $match[2],
165                    'alias_normalized' => $this->versionParser->normalize($match[2], $reqVersion),
166                );
167            }
168        }
169
170        return $aliases;
171    }
172
173    private function extractStabilityFlags(array $requires, array $stabilityFlags, $minimumStability)
174    {
175        $stabilities = BasePackage::$stabilities;
176        $minimumStability = $stabilities[$minimumStability];
177        foreach ($requires as $reqName => $reqVersion) {
178            $constraints = array();
179
180            // extract all sub-constraints in case it is an OR/AND multi-constraint
181            $orSplit = preg_split('{\s*\|\|?\s*}', trim($reqVersion));
182            foreach ($orSplit as $orConstraint) {
183                $andSplit = preg_split('{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)}', $orConstraint);
184                foreach ($andSplit as $andConstraint) {
185                    $constraints[] = $andConstraint;
186                }
187            }
188
189            // parse explicit stability flags to the most unstable
190            $match = false;
191            foreach ($constraints as $constraint) {
192                if (preg_match('{^[^@]*?@('.implode('|', array_keys($stabilities)).')$}i', $constraint, $match)) {
193                    $name = strtolower($reqName);
194                    $stability = $stabilities[VersionParser::normalizeStability($match[1])];
195
196                    if (isset($stabilityFlags[$name]) && $stabilityFlags[$name] > $stability) {
197                        continue;
198                    }
199                    $stabilityFlags[$name] = $stability;
200                    $match = true;
201                }
202            }
203
204            if ($match) {
205                continue;
206            }
207
208            foreach ($constraints as $constraint) {
209                // infer flags for requirements that have an explicit -dev or -beta version specified but only
210                // for those that are more unstable than the minimumStability or existing flags
211                $reqVersion = preg_replace('{^([^,\s@]+) as .+$}', '$1', $constraint);
212                if (preg_match('{^[^,\s@]+$}', $reqVersion) && 'stable' !== ($stabilityName = VersionParser::parseStability($reqVersion))) {
213                    $name = strtolower($reqName);
214                    $stability = $stabilities[$stabilityName];
215                    if ((isset($stabilityFlags[$name]) && $stabilityFlags[$name] > $stability) || ($minimumStability > $stability)) {
216                        continue;
217                    }
218                    $stabilityFlags[$name] = $stability;
219                }
220            }
221        }
222
223        return $stabilityFlags;
224    }
225
226    private function extractReferences(array $requires, array $references)
227    {
228        foreach ($requires as $reqName => $reqVersion) {
229            $reqVersion = preg_replace('{^([^,\s@]+) as .+$}', '$1', $reqVersion);
230            if (preg_match('{^[^,\s@]+?#([a-f0-9]+)$}', $reqVersion, $match) && 'dev' === ($stabilityName = VersionParser::parseStability($reqVersion))) {
231                $name = strtolower($reqName);
232                $references[$name] = $match[1];
233            }
234        }
235
236        return $references;
237    }
238}
239