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\Repository;
14
15use Composer\Package\RootPackageInterface;
16use Composer\Semver\Constraint\ConstraintInterface;
17use Composer\Semver\Constraint\Constraint;
18use Composer\Package\Link;
19
20/**
21 * Common ancestor class for generic repository functionality.
22 *
23 * @author Niels Keurentjes <niels.keurentjes@omines.com>
24 */
25abstract class BaseRepository implements RepositoryInterface
26{
27    /**
28     * Returns a list of links causing the requested needle packages to be installed, as an associative array with the
29     * dependent's name as key, and an array containing in order the PackageInterface and Link describing the relationship
30     * as values. If recursive lookup was requested a third value is returned containing an identically formed array up
31     * to the root package. That third value will be false in case a circular recursion was detected.
32     *
33     * @param  string|string[]          $needle        The package name(s) to inspect.
34     * @param  ConstraintInterface|null $constraint    Optional constraint to filter by.
35     * @param  bool                     $invert        Whether to invert matches to discover reasons for the package *NOT* to be installed.
36     * @param  bool                     $recurse       Whether to recursively expand the requirement tree up to the root package.
37     * @param  string[]                 $packagesFound Used internally when recurring
38     * @return array                    An associative array of arrays as described above.
39     */
40    public function getDependents($needle, $constraint = null, $invert = false, $recurse = true, $packagesFound = null)
41    {
42        $needles = (array) $needle;
43        $results = array();
44
45        // initialize the array with the needles before any recursion occurs
46        if (null === $packagesFound) {
47            $packagesFound = $needles;
48        }
49
50        // locate root package for use below
51        $rootPackage = null;
52        foreach ($this->getPackages() as $package) {
53            if ($package instanceof RootPackageInterface) {
54                $rootPackage = $package;
55                break;
56            }
57        }
58
59        // Loop over all currently installed packages.
60        foreach ($this->getPackages() as $package) {
61            $links = $package->getRequires();
62
63            // each loop needs its own "tree" as we want to show the complete dependent set of every needle
64            // without warning all the time about finding circular deps
65            $packagesInTree = $packagesFound;
66
67            // Replacements are considered valid reasons for a package to be installed during forward resolution
68            if (!$invert) {
69                $links += $package->getReplaces();
70            }
71
72            // Require-dev is only relevant for the root package
73            if ($package instanceof RootPackageInterface) {
74                $links += $package->getDevRequires();
75            }
76
77            // Cross-reference all discovered links to the needles
78            foreach ($links as $link) {
79                foreach ($needles as $needle) {
80                    if ($link->getTarget() === $needle) {
81                        if (is_null($constraint) || (($link->getConstraint()->matches($constraint) === !$invert))) {
82                            // already displayed this node's dependencies, cutting short
83                            if (in_array($link->getSource(), $packagesInTree)) {
84                                $results[$link->getSource()] = array($package, $link, false);
85                                continue;
86                            }
87                            $packagesInTree[] = $link->getSource();
88                            $dependents = $recurse ? $this->getDependents($link->getSource(), null, false, true, $packagesInTree) : array();
89                            $results[$link->getSource()] = array($package, $link, $dependents);
90                        }
91                    }
92                }
93            }
94
95            // When inverting, we need to check for conflicts of the needles against installed packages
96            if ($invert && in_array($package->getName(), $needles)) {
97                foreach ($package->getConflicts() as $link) {
98                    foreach ($this->findPackages($link->getTarget()) as $pkg) {
99                        $version = new Constraint('=', $pkg->getVersion());
100                        if ($link->getConstraint()->matches($version) === $invert) {
101                            $results[] = array($package, $link, false);
102                        }
103                    }
104                }
105            }
106
107            // When inverting, we need to check for conflicts of the needles' requirements against installed packages
108            if ($invert && $constraint && in_array($package->getName(), $needles) && $constraint->matches(new Constraint('=', $package->getVersion()))) {
109                foreach ($package->getRequires() as $link) {
110                    if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $link->getTarget())) {
111                        if ($this->findPackage($link->getTarget(), $link->getConstraint())) {
112                            continue;
113                        }
114
115                        $platformPkg = $this->findPackage($link->getTarget(), '*');
116                        $description = $platformPkg ? 'but '.$platformPkg->getPrettyVersion().' is installed' : 'but it is missing';
117                        $results[] = array($package, new Link($package->getName(), $link->getTarget(), null, 'requires', $link->getPrettyConstraint().' '.$description), false);
118
119                        continue;
120                    }
121
122                    foreach ($this->getPackages() as $pkg) {
123                        if (!in_array($link->getTarget(), $pkg->getNames())) {
124                            continue;
125                        }
126
127                        $version = new Constraint('=', $pkg->getVersion());
128                        if (!$link->getConstraint()->matches($version)) {
129                            // if we have a root package (we should but can not guarantee..) we show
130                            // the root requires as well to perhaps allow to find an issue there
131                            if ($rootPackage) {
132                                foreach (array_merge($rootPackage->getRequires(), $rootPackage->getDevRequires()) as $rootReq) {
133                                    if (in_array($rootReq->getTarget(), $pkg->getNames()) && !$rootReq->getConstraint()->matches($link->getConstraint())) {
134                                        $results[] = array($package, $link, false);
135                                        $results[] = array($rootPackage, $rootReq, false);
136                                        continue 3;
137                                    }
138                                }
139                                $results[] = array($package, $link, false);
140                                $results[] = array($rootPackage, new Link($rootPackage->getName(), $link->getTarget(), null, 'does not require', 'but ' . $pkg->getPrettyVersion() . ' is installed'), false);
141                            } else {
142                                // no root so let's just print whatever we found
143                                $results[] = array($package, $link, false);
144                            }
145                        }
146
147                        continue 2;
148                    }
149                }
150            }
151        }
152
153        ksort($results);
154
155        return $results;
156    }
157}
158