Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 76
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 / 76
0.00% covered (danger)
0.00%
0 / 5
702
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 / 58
0.00% covered (danger)
0.00%
0 / 1
306
 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        foreach ($requiredPackageNames as $requiredPackageName) {
97            // Avoid infinite recursion.
98            if (isset($this->flatDependencyTree[$requiredPackageName])) {
99                continue;
100            }
101
102            $packageComposerFile = sprintf(
103                '%s%s/composer.json',
104                $this->config->getVendorDirectory(),
105                $requiredPackageName
106            );
107            $packageComposerFile = str_replace('mem://', '/', $packageComposerFile);
108
109            $overrideAutoload = $this->overrideAutoload[ $requiredPackageName ] ?? null;
110
111            if ($this->filesystem->fileExists($packageComposerFile)) {
112                $requiredComposerPackage = ComposerPackage::fromFile($packageComposerFile, $overrideAutoload);
113            } else {
114                // Some packages download with NO `composer.json`! E.g. woocommerce/action-scheduler.
115                // Some packages download to a different directory than the package name.
116                $this->logger->debug('Could not find ' . $requiredPackageName . '\'s composer.json in vendor dir, trying composer.lock: ' . $packageComposerFile);
117
118                // TODO: These (.json, .lock) should be read once and reused.
119                $composerJsonString = $this->filesystem->read($this->config->getProjectDirectory() . Factory::getComposerFile());
120                /** @var ComposerJsonArray $composerJson */
121                $composerJson       = json_decode($composerJsonString, true, 512, JSON_THROW_ON_ERROR);
122
123                if (isset($composerJson['provide']) && in_array($requiredPackageName, array_keys($composerJson['provide']))) {
124                    $this->logger->info('Skipping ' . $requiredPackageName . ' as it is in the composer.json provide list');
125                    continue;
126                }
127
128                $composerLockPath = $this->config->getProjectDirectory() . Factory::getLockFile(Factory::getComposerFile());
129                $composerLockString     = $this->filesystem->read($composerLockPath);
130                /** @var null|array{packages:array{name:string, type:string, requires?:array<string,string>, autoload?:AutoloadKeyArray}} $composerLockJsonArray */
131                $composerLockJsonArray           = json_decode($composerLockString, true);
132
133                if (is_null($composerLockJsonArray)) {
134                    continue;
135                }
136
137                /** @var ?ComposerJsonArray $requiredPackageComposerJson */
138                $requiredPackageComposerJson = null;
139                /** @var array{name:string, type:string, requires?:array<string,string>, autoload?:AutoloadKeyArray} $packageJson */
140                foreach ($composerLockJsonArray['packages'] as $packageJson) {
141                    if ($requiredPackageName === $packageJson['name']) {
142                        $requiredPackageComposerJson = $packageJson;
143                        break;
144                    }
145                }
146
147                if (is_null($requiredPackageComposerJson)) {
148                    // e.g. composer-plugin-api, composer-runtime-api
149                    $this->logger->info('Skipping ' . $requiredPackageName . ' as it is not in composer.lock');
150                    continue;
151                }
152
153                if (!isset($requiredPackageComposerJson['autoload'])
154                    && empty($requiredPackageComposerJson['require'])
155                    && (!isset($requiredPackageComposerJson['type']) || $requiredPackageComposerJson['type'] != 'metapackage')
156                    && ! $this->filesystem->directoryExists(dirname($packageComposerFile))
157                ) {
158                    // e.g. symfony/polyfill-php72 when installed on PHP 7.2 or later.
159                    $this->logger->info('Skipping ' . $requiredPackageName . ' as it is has no autoload key (possibly a polyfill unnecessary for this version of PHP).');
160                    continue;
161                }
162
163                $requiredComposerPackage = ComposerPackage::fromComposerJsonArray($requiredPackageComposerJson, $overrideAutoload);
164            }
165
166            $this->logger->info('Analysing package ' . $requiredComposerPackage->getPackageName());
167            $this->flatDependencyTree[$requiredComposerPackage->getPackageName()] = $requiredComposerPackage;
168
169            $nextRequiredPackageNames = $requiredComposerPackage->getRequiresNames();
170
171            if (0 !== count($nextRequiredPackageNames)) {
172                $packageRequiresString = $requiredComposerPackage->getPackageName() . ' requires packages: ';
173                $this->logger->debug($packageRequiresString . implode(', ', $nextRequiredPackageNames));
174            } else {
175                $this->logger->debug($requiredComposerPackage->getPackageName() . ' requires no packages.');
176                continue;
177            }
178
179            $newPackages = array_diff($nextRequiredPackageNames, array_keys($this->flatDependencyTree));
180
181            $newPackagesString = implode(', ', $newPackages);
182            if (!empty($newPackagesString)) {
183                $this->logger->debug(sprintf(
184                    'New packages: %s%s',
185                    str_repeat(' ', strlen($packageRequiresString) - strlen('New packages: ')),
186                    $newPackagesString
187                ));
188            } else {
189                $this->logger->debug('No new packages.');
190                continue;
191            }
192
193            $this->recursiveGetAllDependencies($newPackages);
194        }
195    }
196
197    /**
198     * Get the recorded files autoloaders.
199     *
200     * @return array<string, array<string>>
201     */
202    public function getAllFilesAutoloaders(): array
203    {
204        $filesAutoloaders = array();
205        foreach ($this->flatDependencyTree as $packageName => $composerPackage) {
206            if (isset($composerPackage->getAutoload()['files'])) {
207                $filesAutoloaders[$packageName] = $composerPackage->getAutoload()['files'];
208            }
209        }
210        return $filesAutoloaders;
211    }
212
213    /**
214     * Unset PHP, ext-*, ...
215     */
216    protected function removeVirtualPackagesFilter(string $requiredPackageName): bool
217    {
218        return ! (
219            0 === strpos($requiredPackageName, 'ext')
220            // E.g. `php`, `php-64bit`.
221            || (0 === strpos($requiredPackageName, 'php') && false === strpos($requiredPackageName, '/'))
222            || in_array($requiredPackageName, $this->virtualPackages)
223        );
224    }
225}