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