Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
64.66% covered (warning)
64.66%
75 / 116
48.65% covered (danger)
48.65%
18 / 37
CRAP
0.00% covered (danger)
0.00%
0 / 1
ComposerPackage
64.66% covered (warning)
64.66%
75 / 116
48.65% covered (danger)
48.65%
18 / 37
212.70
0.00% covered (danger)
0.00%
0 / 1
 fromFile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromComposerJsonArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createPartialComposer
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 __construct
77.19% covered (warning)
77.19%
44 / 57
0.00% covered (danger)
0.00%
0 / 1
17.67
 getPackageName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVendorRelativePath
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getRelativePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPackageAbsolutePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAutoload
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasPsr0
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRequiresNames
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 isPlatformPackageName
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 getLicense
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCopy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isCopy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDidCopy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 didCopy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDelete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDoDelete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDidDelete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 didDelete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setProjectVendorDirectory
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 setPackageAbsolutePath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setRealpath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRealPath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFiles
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addDiscoveredSymbol
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDiscoveredSymbols
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addDependency
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDependencies
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFlatDependencyTree
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDiscoveredSymbolsDeep
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 __toString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isPsr0Autoloaded
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isFilesAutoloaded
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Object for getting typed values from composer.json.
4 *
5 * Use this for dependencies. Use ProjectComposerPackage for the primary composer.json.
6 */
7
8namespace BrianHenryIE\Strauss\Composer;
9
10use BrianHenryIE\Strauss\Files\DiscoveredFiles;
11use BrianHenryIE\Strauss\Files\FileWithDependency;
12use BrianHenryIE\Strauss\Helpers\Flysystem\FileSystem;
13use BrianHenryIE\Strauss\Types\DiscoveredSymbol;
14use BrianHenryIE\Strauss\Types\DiscoveredSymbols;
15use Composer\Factory;
16use Composer\IO\NullIO;
17use Composer\PartialComposer;
18use Composer\Util\Platform;
19use Exception;
20
21/**
22 * @phpstan-type AutoloadKeyArray array{files?:array<string>, "classmap"?:array<string>, "psr-4"?:array<string,string|array<string>>, "psr-0"?:array<string,string|array<string>>, "exclude_from_classmap"?:array<string>}
23 * @phpstan-type ComposerConfigArray array{vendor-dir?:string}
24 * @phpstan-type ComposerJsonArray array{name?:string, type?:string, license?:string, require?:array<string,string>, autoload?:AutoloadKeyArray, config?:ComposerConfigArray, repositories?:array<mixed>, provide?:array<string,string>}
25 * @see \Composer\Config::merge()
26 */
27class ComposerPackage
28{
29    /**
30     * The composer.json file as parsed by Composer.
31     *
32     * @see Factory::create
33     *
34     * @var PartialComposer
35     */
36    protected PartialComposer $composer;
37
38    /**
39     * The name of the project in composer.json.
40     *
41     * e.g. brianhenryie/my-project
42     *
43     * @var string
44     */
45    protected string $packageName;
46
47    /**
48     * Virtual packages and meta packages do not have a composer.json.
49     * Some packages are installed in a different directory name than their package name.
50     *
51     * @var ?string
52     */
53    protected ?string $vendorRelativePath = null;
54
55    /**
56     * The full path to the package.
57     *
58     * This may be relative to the Flysystem adapter.
59     */
60    protected ?string $packageAbsolutePath = null;
61
62    /**
63     * Packages can be symlinked from outside the current project directory.
64     *
65     * TODO: When could a package _not_ have an absolute path? Virtual packages, ext-*...
66     */
67    protected ?string $packageRealPath = null;
68
69    /**
70     * The discovered files, classmap, psr0 and psr4 autoload keys discovered (as parsed by Composer).
71     *
72     * @var AutoloadKeyArray
73     */
74    protected array $autoload = [];
75
76    /**
77     * The names in the composer.json's "requires" field (without versions).
78     *
79     * @var string[]
80     */
81    protected array $requiresNames = [];
82
83    /**
84     * @var ComposerPackage[]
85     */
86    protected array $dependencies = [];
87
88    protected string $license;
89
90    /**
91     * Should the package be copied to the vendor-prefixed/target directory? Default: true.
92     */
93    protected bool $isCopy = true;
94    /**
95     * Has the package been copied to the vendor-prefixed/target directory? False until the package is copied.
96     */
97    protected bool $didCopy = false;
98    /**
99     * Should the package be deleted from the vendor directory? Default: false.
100     */
101    protected bool $isDelete = false;
102    /**
103     * Has the package been deleted from the vendor directory? False until the package is deleted.
104     */
105    protected bool $didDelete = false;
106
107    /**
108     * List of files found in the package directory.
109     *
110     * @var FileWithDependency[]
111     */
112    protected array $files = [];
113
114    protected DiscoveredSymbols $discoveredSymbols;
115
116    protected DiscoveredSymbols $discoveredSymbolsDeep;
117
118    /**
119     * @param string $absolutePath The absolute path to composer.json
120     * @param ?array{files?:array<string>, classmap?:array<string>, psr?:array<string,string|array<string>>} $overrideAutoload Optional configuration to replace the package's own autoload definition with
121     *                                    another which Strauss can use.
122     * @return ComposerPackage
123     * @throws Exception
124     */
125    public static function fromFile(string $absolutePath, ?array $overrideAutoload = null): ComposerPackage
126    {
127        return new ComposerPackage(self::createPartialComposer($absolutePath), $overrideAutoload);
128    }
129
130    /**
131     * This is used for virtual packages, which don't have a composer.json.
132     *
133     * @param ComposerJsonArray $jsonArray composer.json decoded to array
134     * @param ?AutoloadKeyArray $overrideAutoload New autoload rules to replace the existing ones.
135     * @throws Exception
136     */
137    public static function fromComposerJsonArray(array $jsonArray, ?array $overrideAutoload = null): ComposerPackage
138    {
139        return new ComposerPackage(self::createPartialComposer($jsonArray), $overrideAutoload);
140    }
141
142    /**
143     * @param string|ComposerJsonArray $composerInput
144     */
145    private static function createPartialComposer($composerInput): PartialComposer
146    {
147        $factory = new Factory();
148
149        /** @var PartialComposer $composer */
150        $composer = $factory->createComposer(new NullIO(), $composerInput, true, null, false);
151
152        return $composer;
153    }
154
155    /**
156     * Create a PHP object to represent a composer package.
157     *
158     * @param PartialComposer $composer
159     * @param ?AutoloadKeyArray $overrideAutoload Optional configuration to replace the package's own autoload definition with another which Strauss can use.
160     * @throws Exception
161     */
162    public function __construct(PartialComposer $composer, ?array $overrideAutoload = null)
163    {
164        $this->discoveredSymbols = new DiscoveredSymbols();
165
166        $this->composer = $composer;
167
168        $this->packageName = $composer->getPackage()->getName();
169
170        $pathNormalizer = FileSystem::makePathNormalizer(Platform::getcwd());
171
172        // This is null for some packages, e.g. `wptrt/admin-notices`
173        $packageVendorDirAbsolute = $this->composer->getConfig()->get('vendor-dir');
174        $projectVendorDirAbsolute = null;
175        if (1 === preg_match('/(.*vendor)/U', $packageVendorDirAbsolute, $projectVendorDirRegexMatch)) {
176            $projectVendorDirAbsolute = $pathNormalizer->normalizePath(
177                $projectVendorDirRegexMatch[1]
178            );
179        }
180        $fsComposerJsonFileAbsolute = $composer->getConfig()->getConfigSource()->getName();
181        $fsComposerAbsoluteDirectoryPath = realpath(dirname($fsComposerJsonFileAbsolute));
182        $composerJsonFileAbsolute = $pathNormalizer->normalizePath($fsComposerJsonFileAbsolute);
183
184        if (false !== $fsComposerAbsoluteDirectoryPath) {
185            if (str_starts_with($composerJsonFileAbsolute, $projectVendorDirAbsolute)) {
186                $this->packageAbsolutePath = $composerJsonFileAbsolute;
187            }
188        }
189
190
191        $fsCurrentWorkingDirectory = getcwd();
192        if ($fsCurrentWorkingDirectory === false) {
193            /**
194             * @see Platform::getCwd()
195             */
196            throw new Exception('Could not determine working directory. Please comment out ~'.__LINE__.' in ' . __FILE__.' and see does it work regardless.');
197        }
198        $fsCurrentWorkingDirectory = FileSystem::normalizeDirSeparator($fsCurrentWorkingDirectory);
199
200        /** @var string $packageVendorAbsoluteDirectoryPath */
201        $packageVendorAbsoluteDirectoryPath = $this->composer->getConfig()->get('vendor-dir');
202        $packageAbsoluteDirectoryPath = dirname($packageVendorAbsoluteDirectoryPath);
203        $vendorAbsoluteDirectoryPath = $fsCurrentWorkingDirectory . '/vendor';
204        if ('metapackage' !== $composer->getPackage()->getType()) {
205            if (file_exists($vendorAbsoluteDirectoryPath . '/' . $this->packageName)) {
206                $resolvedPackagePath = realpath($vendorAbsoluteDirectoryPath . '/' . $this->packageName);
207                $this->packageAbsolutePath = $pathNormalizer->normalizePath(
208                    false !== $resolvedPackagePath
209                        ? $resolvedPackagePath
210                        : $vendorAbsoluteDirectoryPath . '/' . $this->packageName
211                );
212
213
214                $this->vendorRelativePath  = $this->packageName;
215                //$this->packageAbsolutePath = $pathNormalizer->normalizePath(Platform::realpath($vendorAbsoluteDirectoryPath . '/' . $this->packageName));
216                // If the package is symlinked, the path will be outside the working directory.
217//        } elseif (0 !== strpos($fsComposerAbsoluteDirectoryPath, $fsCurrentWorkingDirectory) && 1 === preg_match('/.*[\/\\\\]([^\/\\\\]*[\/\\\\][^\/\\\\]*)[\/\\\\][^\/\\\\]*/', $vendorAbsoluteDirectoryPath, $output_array)) {
218//            $this->vendorRelativePath = $output_array[1];
219            } elseif (! ( $this instanceof ProjectComposerPackage ) && file_exists($packageAbsoluteDirectoryPath)) {
220                $this->vendorRelativePath  =
221                    implode(
222                        '/',
223                        array_slice(
224                            explode(
225                                '/',
226                                FileSystem::normalizeDirSeparator($packageAbsoluteDirectoryPath)
227                            ),
228                            - 2
229                        )
230                    );
231                $this->packageAbsolutePath = $pathNormalizer->normalizePath($packageAbsoluteDirectoryPath);
232            } elseif (1 === preg_match('/.*[\/\\\\]([^\/\\\\]+[\/\\\\][^\/\\\\]+)[\/\\\\]composer.json/', $composerJsonFileAbsolute, $output_array)) {
233                // Not every package gets installed to a folder matching its name (crewlabs/unsplash).
234                if (! ( $this instanceof ProjectComposerPackage )) {
235                    $this->vendorRelativePath = $output_array[1];
236                }
237                $this->packageAbsolutePath = dirname($composerJsonFileAbsolute);
238            }
239        }
240
241        if (!is_null($overrideAutoload)) {
242            $composer->getPackage()->setAutoload($overrideAutoload);
243        }
244
245        $this->autoload = $composer->getPackage()->getAutoload();
246
247        foreach ($composer->getPackage()->getRequires() as $_name => $packageLink) {
248            $this->requiresNames[] = $packageLink->getTarget();
249        }
250
251        // Try to get the license from the package's composer.json, assume proprietary (all rights reserved!).
252        $this->license = !empty($composer->getPackage()->getLicense())
253            ? implode(',', $composer->getPackage()->getLicense())
254            : 'proprietary?';
255    }
256
257    /**
258     * Composer package project name.
259     *
260     * vendor/project-name
261     *
262     * @return string
263     */
264    public function getPackageName(): string
265    {
266        return $this->packageName;
267    }
268
269    /**
270     * This is relative to vendor.
271     */
272    public function getVendorRelativePath(): ?string
273    {
274        return is_null($this->vendorRelativePath)
275               ? null
276             : FileSystem::normalizeDirSeparator($this->vendorRelativePath);
277    }
278
279    /**
280     * This is relative to vendor.
281     */
282    public function getRelativePath(): ?string
283    {
284        return $this->vendorRelativePath;
285    }
286
287    /**
288     * No leading or tailing slash
289     *
290     * Possibly a symlink.
291     */
292    public function getPackageAbsolutePath(): ?string
293    {
294        return $this->packageAbsolutePath;
295    }
296
297    /**
298     *
299     * e.g. ['psr-4' => [ 'BrianHenryIE\Project' => 'src' ]]
300     * e.g. ['psr-4' => [ 'BrianHenryIE\Project' => ['src','lib] ]]
301     * e.g. ['classmap' => [ 'src', 'lib' ]]
302     * e.g. ['files' => [ 'lib', 'functions.php' ]]
303     *
304     * @return AutoloadKeyArray
305     */
306    public function getAutoload(): array
307    {
308        return $this->autoload;
309    }
310
311    public function hasPsr0(): bool
312    {
313        return isset($this->autoload['psr-0']);
314    }
315
316    /**
317     * The names of the packages in the composer.json's "requires" field (without version).
318     *
319     * Excludes PHP, ext-*, since we won't be copying or prefixing them.
320     *
321     * @return string[]
322     */
323    public function getRequiresNames(): array
324    {
325        return array_filter(
326            $this->requiresNames,
327            static function (string $requiredPackageName): bool {
328                return !self::isPlatformPackageName($requiredPackageName);
329            }
330        );
331    }
332
333    public static function isPlatformPackageName(string $packageName, bool $includePhpVariants = false): bool
334    {
335        return 0 === strpos($packageName, 'ext')
336            || 'php' === $packageName
337            || (
338                $includePhpVariants
339                && 0 === strpos($packageName, 'php')
340                && false === strpos($packageName, '/')
341            );
342    }
343
344    public function getLicense():string
345    {
346        return $this->license;
347    }
348
349    /**
350     * Should the file be copied? (defaults to yes)
351     */
352    public function setCopy(bool $isCopy): void
353    {
354        $this->isCopy = $isCopy;
355    }
356
357    /**
358     * Should the file be copied? (defaults to yes)
359     */
360    public function isCopy(): bool
361    {
362        return $this->isCopy;
363    }
364
365    /**
366     * Has the file been copied? (defaults to no)
367     */
368    public function setDidCopy(bool $didCopy): void
369    {
370        $this->didCopy = $didCopy;
371    }
372
373    /**
374     * Has the file been copied? (defaults to no)
375     */
376    public function didCopy(): bool
377    {
378        return $this->didCopy;
379    }
380
381    /**
382     * Should the file be deleted? (defaults to no)
383     */
384    public function setDelete(bool $isDelete): void
385    {
386        $this->isDelete = $isDelete;
387    }
388
389    /**
390     * Should the file be deleted? (defaults to no)
391     */
392    public function isDoDelete(): bool
393    {
394        return $this->isDelete;
395    }
396
397    /**
398     * Has the file been deleted? (defaults to no)
399     */
400    public function setDidDelete(bool $didDelete): void
401    {
402        $this->didDelete = $didDelete;
403    }
404
405    /**
406     * Has the file been deleted? (defaults to no)
407     */
408    public function didDelete(): bool
409    {
410        return $this->didDelete;
411    }
412
413    public function setProjectVendorDirectory(string $parentProjectVendorDirectory): void
414    {
415        if (!is_null($this->packageAbsolutePath)) {
416            $this->packageAbsolutePath = $parentProjectVendorDirectory . '/' . $this->vendorRelativePath;
417        }
418    }
419
420    public function setPackageAbsolutePath(string $packageAbsolutePath): void
421    {
422        $this->packageAbsolutePath = $packageAbsolutePath;
423    }
424
425    public function setRealpath(string $realpath): void
426    {
427        $this->packageRealPath = $realpath;
428    }
429
430    public function getRealPath(): ?string
431    {
432        return $this->packageRealPath;
433    }
434
435    public function addFile(FileWithDependency $file): void
436    {
437        $this->files[$file->getPackageRelativePath()] = $file;
438    }
439
440    public function getFile(string $path): ?FileWithDependency
441    {
442        return $this->files[$path] ?? null;
443    }
444
445    public function getFiles(): DiscoveredFiles
446    {
447        return new DiscoveredFiles($this->files);
448    }
449
450    public function addDiscoveredSymbol(DiscoveredSymbol $symbol): void
451    {
452        $this->discoveredSymbols->add($symbol);
453    }
454
455    public function getDiscoveredSymbols(): DiscoveredSymbols
456    {
457        return $this->discoveredSymbols;
458    }
459
460    public function addDependency(ComposerPackage $composerPackage): void
461    {
462        $this->dependencies[$composerPackage->getPackageName()] = $composerPackage;
463    }
464
465    /**
466     * @return ComposerPackage[]
467     */
468    public function getDependencies(): array
469    {
470        return $this->dependencies;
471    }
472
473    public function getFlatDependencyTree(): DeepDependenciesCollection
474    {
475        return new DeepDependenciesCollection($this->dependencies);
476    }
477
478    public function getDiscoveredSymbolsDeep(): DiscoveredSymbols
479    {
480        if (isset($this->discoveredSymbolsDeep)) {
481            return $this->discoveredSymbolsDeep;
482        }
483        $flatDependencyTree = $this->getFlatDependencyTree();
484        $dependencyDiscoveredSymbolsArray = $this->discoveredSymbols->toArray();
485        foreach ($flatDependencyTree as $dependency) {
486            $dependencyDiscoveredSymbolsArray = array_merge($dependencyDiscoveredSymbolsArray, $dependency->getDiscoveredSymbols()->toArray());
487        }
488        $this->discoveredSymbolsDeep = new DiscoveredSymbols($dependencyDiscoveredSymbolsArray);
489        return $this->discoveredSymbolsDeep;
490    }
491
492    /**
493     * So it works with `array_unique()`.
494     *
495     * @return string
496     */
497    public function __toString()
498    {
499        return $this->getPackageName();
500    }
501
502    public function isPsr0Autoloaded(): bool
503    {
504        return isset($this->autoload['psr-0']);
505    }
506    public function isFilesAutoloaded(): bool
507    {
508        return isset($this->autoload['files']);
509    }
510}