Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 103
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 / 103
0.00% covered (danger)
0.00%
0 / 5
870
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 / 85
0.00% covered (danger)
0.00%
0 / 1
420
 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 JsonException;
14use League\Flysystem\FilesystemException;
15use Psr\Log\LoggerAwareTrait;
16use Psr\Log\LoggerInterface;
17use Psr\Log\NullLogger;
18
19/**
20 * @phpstan-import-type ComposerJsonArray from ComposerPackage
21 * @phpstan-import-type AutoloadKeyArray from ComposerPackage
22 */
23class DependenciesEnumerator
24{
25    use LoggerAwareTrait;
26
27    /**
28     * @var string[]
29     */
30    protected array $requiredPackageNames;
31
32    protected FileSystem $filesystem;
33
34    /** @var string[]  */
35    protected array $virtualPackages = array(
36        'php-http/client-implementation'
37    );
38
39    /** @var array<string, ComposerPackage> */
40    protected array $flatDependencyTree = array();
41
42    /**
43     * Record the files autoloaders for later use in building our own autoloader.
44     *
45     * Package-name: [ dir1, file1, file2, ... ].
46     *
47     * @var array<string, string[]>
48     */
49    protected array $filesAutoloaders = [];
50
51    /**
52     * @var array{}|array<string, array{files?:array<string>,classmap?:array<string>,"psr-4":array<string|array<string>>}> $overrideAutoload
53     */
54    protected array $overrideAutoload = array();
55    protected StraussConfig $config;
56
57    /**
58     * Constructor.
59     */
60    public function __construct(
61        StraussConfig    $config,
62        FileSystem       $filesystem,
63        ?LoggerInterface $logger = null
64    ) {
65        $this->overrideAutoload = $config->getOverrideAutoload();
66        $this->requiredPackageNames = $config->getPackages();
67
68        $this->filesystem = $filesystem;
69        $this->config = $config;
70
71        $this->setLogger($logger ?? new NullLogger());
72    }
73
74    /**
75     * @return array<string, ComposerPackage> Packages indexed by package name.
76     * @throws Exception
77     * @throws FilesystemException
78     */
79    public function getAllDependencies(): array
80    {
81        $this->recursiveGetAllDependencies($this->requiredPackageNames);
82
83        return $this->flatDependencyTree;
84    }
85
86    /**
87     * @param string[] $requiredPackageNames
88     * @throws FilesystemException
89     * @throws JsonException
90     * @throws Exception
91     */
92    protected function recursiveGetAllDependencies(array $requiredPackageNames): void
93    {
94        $requiredPackageNames = array_filter($requiredPackageNames, array( $this, 'removeVirtualPackagesFilter' ));
95
96        $installedJsonPath = $this->filesystem->makeAbsolute(
97            sprintf(
98                "%s/composer/installed.json",
99                $this->config->getAbsoluteVendorDirectory()
100            )
101        );
102        $installedJsonTxt = $this->filesystem->read($installedJsonPath);
103        $installedJson = json_decode($installedJsonTxt, true);
104        $installedJsonPackages = [];
105        foreach ($installedJson['packages'] as $package) {
106            $installedJsonPackages[$package['name']] = $package;
107        }
108
109        foreach ($requiredPackageNames as $requiredPackageName) {
110            // Avoid infinite recursion.
111            if (isset($this->flatDependencyTree[$requiredPackageName])) {
112                continue;
113            }
114
115            $packageComposerFile = sprintf(
116                '%s/%s/composer.json',
117                $this->config->getAbsoluteVendorDirectory(),
118                $requiredPackageName
119            );
120
121            /**
122             * 1. Remove `mem://`
123             * 2. Add `c:\` or `/`
124             * @see https://github.com/composer/composer/pull/12396
125             */
126            $packageComposerFile = $this->filesystem->normalizePath($packageComposerFile);
127            $packageComposerFile = $this->filesystem->makeAbsolute($packageComposerFile);
128
129            $overrideAutoload = $this->overrideAutoload[ $requiredPackageName ] ?? null;
130
131            if ($this->filesystem->fileExists($packageComposerFile)) {
132                $this->logger->debug('Loading ComposerPackage::fromFile ' . $packageComposerFile);
133
134                $requiredComposerPackage = ComposerPackage::fromFile($packageComposerFile, $overrideAutoload);
135            } else {
136                // Some packages download with NO `composer.json`! E.g. woocommerce/action-scheduler.
137                // Some packages download to a different directory than the package name.
138                $this->logger->debug('Could not find ' . $requiredPackageName . '\'s composer.json in vendor dir, trying composer.lock: ' . $packageComposerFile);
139
140                // TODO: These (.json, .lock) should be read once and reused.
141                $composerJsonString = $this->filesystem->read($this->config->getProjectAbsolutePath() . '/' . Factory::getComposerFile());
142                /** @var ComposerJsonArray $composerJson */
143                $composerJson       = json_decode($composerJsonString, true, 512, JSON_THROW_ON_ERROR);
144
145                if (isset($composerJson['provide']) && in_array($requiredPackageName, array_keys($composerJson['provide']))) {
146                    $this->logger->info('Skipping ' . $requiredPackageName . ' as it is in the composer.json provide list');
147                    continue;
148                }
149
150                $composerLockPath = $this->config->getProjectAbsolutePath() . '/' . Factory::getLockFile(Factory::getComposerFile());
151                $composerLockString     = $this->filesystem->read($composerLockPath);
152                /** @var null|array{packages:array{name:string, type:string, requires?:array<string,string>, autoload?:AutoloadKeyArray}} $composerLockJsonArray */
153                $composerLockJsonArray           = json_decode($composerLockString, true);
154
155                if (is_null($composerLockJsonArray)) {
156                    continue;
157                }
158
159                /** @var ?ComposerJsonArray $requiredPackageComposerJson */
160                $requiredPackageComposerJson = null;
161                /** @var array{name:string, type:string, requires?:array<string,string>, autoload?:AutoloadKeyArray} $packageJson */
162                foreach ($composerLockJsonArray['packages'] as $packageJson) {
163                    if ($requiredPackageName === $packageJson['name']) {
164                        $requiredPackageComposerJson = $packageJson;
165                        break;
166                    }
167                }
168
169                if (is_null($requiredPackageComposerJson)) {
170                    // e.g. composer-plugin-api, composer-runtime-api
171                    $this->logger->info('Skipping ' . $requiredPackageName . ' as it is not in composer.lock');
172                    continue;
173                }
174
175                if (!isset($requiredPackageComposerJson['autoload'])
176                    && empty($requiredPackageComposerJson['require'])
177                    && (!isset($requiredPackageComposerJson['type']) || $requiredPackageComposerJson['type'] != 'metapackage')
178                    && ! $this->filesystem->directoryExists(dirname($packageComposerFile))
179                ) {
180                    // e.g. symfony/polyfill-php72 when installed on PHP 7.2 or later.
181                    $this->logger->info('Skipping ' . $requiredPackageName . ' as it is has no autoload key (possibly a polyfill unnecessary for this version of PHP).');
182                    continue;
183                }
184
185                $projectVendorAbsoluteDir = $this->config->getAbsoluteVendorDirectory();
186
187                $requiredComposerPackage = ComposerPackage::fromComposerJsonArray($requiredPackageComposerJson, $overrideAutoload, $projectVendorAbsoluteDir);
188            }
189
190            $installedPackage = $installedJsonPackages[$requiredPackageName];
191            if (isset($installedPackage['dist'], $installedPackage['dist']['type']) && $installedPackage['dist']['type'] === 'path') {
192                $path = $installedPackage['dist']['url'];
193
194                $packageRealPath = $this->filesystem->normalizePath($this->config->getProjectAbsolutePath() . '/'.  $path);
195                $requiredComposerPackage->setRealpath(
196                    $packageRealPath
197                );
198            }
199            unset($installedPackage);
200            $requiredComposerPackage->setProjectVendorDirectory(
201                $this->filesystem->normalizePath(
202                    $this->config->getAbsoluteVendorDirectory()
203                )
204            );
205
206            $this->logger->info('Analysing package ' . $requiredComposerPackage->getPackageName());
207            $this->flatDependencyTree[$requiredComposerPackage->getPackageName()] = $requiredComposerPackage;
208
209            $nextRequiredPackageNames = $requiredComposerPackage->getRequiresNames();
210
211            if (0 !== count($nextRequiredPackageNames)) {
212                $packageRequiresString = $requiredComposerPackage->getPackageName() . ' requires packages: ';
213                $this->logger->debug($packageRequiresString . implode(', ', $nextRequiredPackageNames));
214            } else {
215                $this->logger->debug($requiredComposerPackage->getPackageName() . ' requires no packages.');
216                continue;
217            }
218
219            $newPackages = array_diff($nextRequiredPackageNames, array_keys($this->flatDependencyTree));
220
221            $newPackagesString = implode(', ', $newPackages);
222            if (!empty($newPackagesString)) {
223                $this->logger->debug(sprintf(
224                    'New packages: %s%s',
225                    str_repeat(' ', strlen($packageRequiresString) - strlen('New packages: ')),
226                    $newPackagesString
227                ));
228            } else {
229                $this->logger->debug('No new packages.');
230                continue;
231            }
232
233            $this->recursiveGetAllDependencies($newPackages);
234        }
235    }
236
237    /**
238     * Get the recorded files autoloaders.
239     *
240     * @return array<string, array<string>>
241     */
242    public function getAllFilesAutoloaders(): array
243    {
244        $filesAutoloaders = array();
245        foreach ($this->flatDependencyTree as $packageName => $composerPackage) {
246            if (isset($composerPackage->getAutoload()['files'])) {
247                $filesAutoloaders[$packageName] = $composerPackage->getAutoload()['files'];
248            }
249        }
250        return $filesAutoloaders;
251    }
252
253    /**
254     * Unset PHP, ext-*, ...
255     */
256    protected function removeVirtualPackagesFilter(string $requiredPackageName): bool
257    {
258        return ! (
259            0 === strpos($requiredPackageName, 'ext')
260            // E.g. `php`, `php-64bit`.
261            || (0 === strpos($requiredPackageName, 'php') && false === strpos($requiredPackageName, '/'))
262            || in_array($requiredPackageName, $this->virtualPackages)
263        );
264    }
265}