Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
DependenciesEnumerator
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 5
600
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getAllDependencies
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 recursiveGetAllDependencies
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
240
 getAllFilesAutoloaders
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 removeVirtualPackagesFilter
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * Build a list of ComposerPackage objects for all dependencies.
4 */
5
6namespace BrianHenryIE\Strauss\Pipeline;
7
8use BrianHenryIE\Strauss\Composer\ComposerPackage;
9use BrianHenryIE\Strauss\Composer\Extra\StraussConfig;
10use BrianHenryIE\Strauss\Helpers\FileSystem;
11use Composer\Factory;
12use Exception;
13use League\Flysystem\FilesystemReader;
14use Psr\Log\LoggerAwareTrait;
15use Psr\Log\LoggerInterface;
16use Psr\Log\NullLogger;
17
18class DependenciesEnumerator
19{
20    use LoggerAwareTrait;
21
22    /**
23     * @var string[]
24     */
25    protected array $requiredPackageNames;
26
27    protected FileSystem $filesystem;
28
29    /** @var string[]  */
30    protected array $virtualPackages = array(
31        'php-http/client-implementation'
32    );
33
34    /** @var array<string, ComposerPackage> */
35    protected array $flatDependencyTree = array();
36
37    /**
38     * Record the files autoloaders for later use in building our own autoloader.
39     *
40     * Package-name: [ dir1, file1, file2, ... ].
41     *
42     * @var array<string, string[]>
43     */
44    protected array $filesAutoloaders = [];
45
46    /**
47     * @var array{}|array<string, array{files?:array<string>,classmap?:array<string>,"psr-4":array<string|array<string>>}> $overrideAutoload
48     */
49    protected array $overrideAutoload = array();
50    protected StraussConfig $config;
51
52    /**
53     * Constructor.
54     *
55     * @param StraussConfig $config
56     */
57    public function __construct(
58        StraussConfig $config,
59        FileSystem $filesystem,
60        ?LoggerInterface $logger = null
61    ) {
62        $this->overrideAutoload = $config->getOverrideAutoload();
63        $this->requiredPackageNames = $config->getPackages();
64
65        $this->filesystem = $filesystem;
66        $this->config = $config;
67
68        $this->setLogger($logger ?? new NullLogger());
69    }
70
71    /**
72     * @return array<string, ComposerPackage> Packages indexed by package name.
73     * @throws Exception
74     */
75    public function getAllDependencies(): array
76    {
77        $this->recursiveGetAllDependencies($this->requiredPackageNames);
78
79        return $this->flatDependencyTree;
80    }
81
82    /**
83     * @param string[] $requiredPackageNames
84     */
85    protected function recursiveGetAllDependencies(array $requiredPackageNames): void
86    {
87        $requiredPackageNames = array_filter($requiredPackageNames, array( $this, 'removeVirtualPackagesFilter' ));
88
89        foreach ($requiredPackageNames as $requiredPackageName) {
90            // Avoid infinite recursion.
91            if (isset($this->flatDependencyTree[$requiredPackageName])) {
92                continue;
93            }
94
95            $packageComposerFile = sprintf(
96                '%s%s/composer.json',
97                $this->config->getVendorDirectory(),
98                $requiredPackageName
99            );
100            $packageComposerFile = str_replace('mem://', '/', $packageComposerFile);
101
102            $overrideAutoload = $this->overrideAutoload[ $requiredPackageName ] ?? null;
103
104            if ($this->filesystem->fileExists($packageComposerFile)) {
105                $requiredComposerPackage = ComposerPackage::fromFile($packageComposerFile, $overrideAutoload);
106            } else {
107                // Some packages download with NO `composer.json`! E.g. woocommerce/action-scheduler.
108                // Some packages download to a different directory than the package name.
109                $this->logger->debug('Could not find ' . $requiredPackageName . '\'s composer.json in vendor dir, trying composer.lock');
110
111                // TODO: These (.json, .lock) should be read once and reused.
112                $composerJsonString = $this->filesystem->read($this->config->getProjectDirectory() . Factory::getComposerFile());
113                $composerJson       = json_decode($composerJsonString, true);
114
115                if (isset($composerJson['provide']) && in_array($requiredPackageName, array_keys($composerJson['provide']))) {
116                    $this->logger->info('Skipping ' . $requiredPackageName . ' as it is in the composer.json provide list');
117                    continue;
118                }
119
120                $composerLockPath = $this->config->getProjectDirectory() . Factory::getLockFile(Factory::getComposerFile());
121                $composerLockString     = $this->filesystem->read($composerLockPath);
122                $composerLock           = json_decode($composerLockString, true);
123
124                $requiredPackageComposerJson = null;
125                foreach ($composerLock['packages'] as $packageJson) {
126                    if ($requiredPackageName === $packageJson['name']) {
127                        $requiredPackageComposerJson = $packageJson;
128                        break;
129                    }
130                }
131
132                if (is_null($requiredPackageComposerJson)) {
133                    // e.g. composer-plugin-api, composer-runtime-api
134                    $this->logger->info('Skipping ' . $requiredPackageName . ' as it is not in composer.lock');
135                    continue;
136                }
137
138                if (!isset($requiredPackageComposerJson['autoload'])
139                    && empty($requiredPackageComposerJson['require'])
140                    && $requiredPackageComposerJson['type'] != 'metapackage'
141                    && ! $this->filesystem->directoryExists(dirname($packageComposerFile))
142                ) {
143                    // e.g. symfony/polyfill-php72 when installed on PHP 7.2 or later.
144                    $this->logger->info('Skipping ' . $requiredPackageName . ' as it is has no autoload key (possibly a polyfill unnecessary for this version of PHP).');
145                    continue;
146                }
147
148                $requiredComposerPackage = ComposerPackage::fromComposerJsonArray($requiredPackageComposerJson, $overrideAutoload);
149            }
150
151            $this->logger->info('Analysing package ' . $requiredComposerPackage->getPackageName());
152            $this->flatDependencyTree[$requiredComposerPackage->getPackageName()] = $requiredComposerPackage;
153
154            $nextRequiredPackageNames = $requiredComposerPackage->getRequiresNames();
155
156            if (0 !== count($nextRequiredPackageNames)) {
157                $packageRequiresString = $requiredComposerPackage->getPackageName() . ' requires packages: ';
158                $this->logger->debug($packageRequiresString . implode(', ', $nextRequiredPackageNames));
159            } else {
160                $this->logger->debug($requiredComposerPackage->getPackageName() . ' requires no packages.');
161                continue;
162            }
163
164            $newPackages = array_diff($nextRequiredPackageNames, array_keys($this->flatDependencyTree));
165
166            $newPackagesString = implode(', ', $newPackages);
167            if (!empty($newPackagesString)) {
168                $this->logger->debug(sprintf(
169                    'New packages: %s%s',
170                    str_repeat(' ', strlen($packageRequiresString) - strlen('New packages: ')),
171                    $newPackagesString
172                ));
173            } else {
174                $this->logger->debug('No new packages.');
175                continue;
176            }
177
178            $this->recursiveGetAllDependencies($newPackages);
179        }
180    }
181
182    /**
183     * Get the recorded files autoloaders.
184     *
185     * @return array<string, array<string>>
186     */
187    public function getAllFilesAutoloaders(): array
188    {
189        $filesAutoloaders = array();
190        foreach ($this->flatDependencyTree as $packageName => $composerPackage) {
191            if (isset($composerPackage->getAutoload()['files'])) {
192                $filesAutoloaders[$packageName] = $composerPackage->getAutoload()['files'];
193            }
194        }
195        return $filesAutoloaders;
196    }
197
198    /**
199     * Unset PHP, ext-*, ...
200     *
201     * @param string $requiredPackageName
202     */
203    protected function removeVirtualPackagesFilter(string $requiredPackageName): bool
204    {
205        return ! (
206            0 === strpos($requiredPackageName, 'ext')
207            // E.g. `php`, `php-64bit`.
208            || (0 === strpos($requiredPackageName, 'php') && false === strpos($requiredPackageName, '/'))
209            || in_array($requiredPackageName, $this->virtualPackages)
210        );
211    }
212}