Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.12% covered (warning)
68.12%
47 / 69
50.00% covered (danger)
50.00%
12 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
ComposerPackage
68.12% covered (warning)
68.12%
47 / 69
50.00% covered (danger)
50.00%
12 / 24
84.80
0.00% covered (danger)
0.00%
0 / 1
 fromFile
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 fromComposerJsonArray
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
82.86% covered (warning)
82.86%
29 / 35
0.00% covered (danger)
0.00%
0 / 1
12.73
 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
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPackageAbsolutePath
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getAutoload
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRequiresNames
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getLicense
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 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 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 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
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\FileWithDependency;
11use BrianHenryIE\Strauss\Helpers\FileSystem;
12use Composer\Composer;
13use Composer\Factory;
14use Composer\IO\NullIO;
15use Composer\Util\Platform;
16use Exception;
17
18/**
19 * @phpstan-type AutoloadKeyArray array{files?:array<string>, "classmap"?:array<string>, "psr-4"?:array<string,string|array<string>>, "exclude_from_classmap"?:array<string>}
20 * @phpstan-type ComposerConfigArray array{vendor-dir?:string}
21 * @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>}
22 * @see \Composer\Config::merge()
23 */
24class ComposerPackage
25{
26    /**
27     * The composer.json file as parsed by Composer.
28     *
29     * @see Factory::create
30     *
31     * @var Composer
32     */
33    protected Composer $composer;
34
35    /**
36     * The name of the project in composer.json.
37     *
38     * e.g. brianhenryie/my-project
39     *
40     * @var string
41     */
42    protected string $packageName;
43
44    /**
45     * Virtual packages and meta packages do not have a composer.json.
46     * Some packages are installed in a different directory name than their package name.
47     *
48     * @var ?string
49     */
50    protected ?string $vendorRelativePath = null;
51
52    /**
53     * The full path to the package.
54     *
55     * This may be relative to the Flysystem adapter.
56     */
57    protected ?string $packageAbsolutePath = null;
58
59    /**
60     * Packages can be symlinked from outside the current project directory.
61     *
62     * TODO: When could a package _not_ have an absolute path? Virtual packages, ext-*...
63     */
64    protected ?string $packageRealPath = null;
65
66    /**
67     * The discovered files, classmap, psr0 and psr4 autoload keys discovered (as parsed by Composer).
68     *
69     * @var AutoloadKeyArray
70     */
71    protected array $autoload = [];
72
73    /**
74     * The names in the composer.json's "requires" field (without versions).
75     *
76     * @var string[]
77     */
78    protected array $requiresNames = [];
79
80    protected string $license;
81
82    /**
83     * Should the package be copied to the vendor-prefixed/target directory? Default: true.
84     */
85    protected bool $isCopy = true;
86    /**
87     * Has the package been copied to the vendor-prefixed/target directory? False until the package is copied.
88     */
89    protected bool $didCopy = false;
90    /**
91     * Should the package be deleted from the vendor directory? Default: false.
92     */
93    protected bool $isDelete = false;
94    /**
95     * Has the package been deleted from the vendor directory? False until the package is deleted.
96     */
97    protected bool $didDelete = false;
98
99    /**
100     * List of files found in the package directory.
101     *
102     * @var FileWithDependency[]
103     */
104    protected array $files = [];
105
106    /**
107     * @param string $absolutePath The absolute path to composer.json
108     * @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
109     *                                    another which Strauss can use.
110     * @return ComposerPackage
111     * @throws Exception
112     */
113    public static function fromFile(string $absolutePath, ?array $overrideAutoload = null): ComposerPackage
114    {
115        $composer = Factory::create(new NullIO(), $absolutePath, true);
116
117        return new ComposerPackage($composer, $overrideAutoload);
118    }
119
120    /**
121     * This is used for virtual packages, which don't have a composer.json.
122     *
123     * @param ComposerJsonArray $jsonArray composer.json decoded to array
124     * @param ?AutoloadKeyArray $overrideAutoload New autoload rules to replace the existing ones.
125     * @throws Exception
126     */
127    public static function fromComposerJsonArray(array $jsonArray, ?array $overrideAutoload = null): ComposerPackage
128    {
129        $factory = new Factory();
130        $io = new NullIO();
131        $composer = $factory->createComposer($io, $jsonArray, true);
132
133        return new ComposerPackage($composer, $overrideAutoload);
134    }
135
136    /**
137     * Create a PHP object to represent a composer package.
138     *
139     * @param Composer $composer
140     * @param ?AutoloadKeyArray $overrideAutoload Optional configuration to replace the package's own autoload definition with another which Strauss can use.
141     * @throws Exception
142     */
143    public function __construct(Composer $composer, ?array $overrideAutoload = null)
144    {
145        $this->composer = $composer;
146
147        $this->packageName = $composer->getPackage()->getName();
148
149        $pathNormalizer = FileSystem::makePathNormalizer(getcwd());
150
151        // This is null for some packages, e.g. `wptrt/admin-notices`
152        $packageVendorDirAbsolute = $this->composer->getConfig()->get('vendor-dir');
153        $projectVendorDirAbsolute = null;
154        if (1 === preg_match('/(.*vendor)/U', $packageVendorDirAbsolute, $projectVendorDirRegexMatch)) {
155            $projectVendorDirAbsolute = $pathNormalizer->normalizePath(
156                $projectVendorDirRegexMatch[1]
157            );
158        }
159        $fsComposerJsonFileAbsolute = $composer->getConfig()->getConfigSource()->getName();
160        $composerJsonFileAbsolute = $pathNormalizer->normalizePath($fsComposerJsonFileAbsolute);
161
162        $fsComposerAbsoluteDirectoryPath = realpath(dirname($fsComposerJsonFileAbsolute));
163        if (false !== $fsComposerAbsoluteDirectoryPath) {
164            if (str_starts_with($composerJsonFileAbsolute, $projectVendorDirAbsolute)) {
165                $this->packageAbsolutePath = $composerJsonFileAbsolute;
166            }
167        }
168
169
170        $fsCurrentWorkingDirectory = getcwd();
171        if ($fsCurrentWorkingDirectory === false) {
172            /**
173             * @see Platform::getCwd()
174             */
175            throw new Exception('Could not determine working directory. Please comment out ~'.__LINE__.' in ' . __FILE__.' and see does it work regardless.');
176        }
177        $fsCurrentWorkingDirectory = FileSystem::normalizeDirSeparator($fsCurrentWorkingDirectory);
178
179        /** @var string $vendorAbsoluteDirectoryPath */
180        $vendorAbsoluteDirectoryPath = $this->composer->getConfig()->get('vendor-dir');
181        if (file_exists($vendorAbsoluteDirectoryPath . '/' . $this->packageName)) {
182            $this->vendorRelativePath = $this->packageName;
183            $this->packageAbsolutePath = $pathNormalizer->normalizePath(realpath($vendorAbsoluteDirectoryPath . '/' . $this->packageName));
184        // If the package is symlinked, the path will be outside the working directory.
185        } elseif (0 !== strpos($fsComposerAbsoluteDirectoryPath, $fsCurrentWorkingDirectory) && 1 === preg_match('/.*[\/\\\\]([^\/\\\\]*[\/\\\\][^\/\\\\]*)[\/\\\\][^\/\\\\]*/', $vendorAbsoluteDirectoryPath, $output_array)) {
186            $this->vendorRelativePath = $output_array[1];
187        } elseif (1 === preg_match('/.*[\/\\\\]([^\/\\\\]+[\/\\\\][^\/\\\\]+)[\/\\\\]composer.json/', $composerJsonFileAbsolute, $output_array)) {
188        // Not every package gets installed to a folder matching its name (crewlabs/unsplash).
189            $this->vendorRelativePath = $output_array[1];
190        }
191
192        if (!is_null($overrideAutoload)) {
193            $composer->getPackage()->setAutoload($overrideAutoload);
194        }
195
196        $this->autoload = $composer->getPackage()->getAutoload();
197
198        foreach ($composer->getPackage()->getRequires() as $_name => $packageLink) {
199            $this->requiresNames[] = $packageLink->getTarget();
200        }
201
202        // Try to get the license from the package's composer.json, assume proprietary (all rights reserved!).
203        $this->license = !empty($composer->getPackage()->getLicense())
204            ? implode(',', $composer->getPackage()->getLicense())
205            : 'proprietary?';
206    }
207
208    /**
209     * Composer package project name.
210     *
211     * vendor/project-name
212     *
213     * @return string
214     */
215    public function getPackageName(): string
216    {
217        return $this->packageName;
218    }
219
220    /**
221     * This is relative to vendor.
222     */
223    public function getVendorRelativePath(): ?string
224    {
225        return is_null($this->vendorRelativePath)
226               ? null
227             : FileSystem::normalizeDirSeparator($this->vendorRelativePath);
228    }
229
230    /**
231     * This is relative to vendor.
232     */
233    public function getRelativePath(): ?string
234    {
235        return $this->vendorRelativePath;
236    }
237
238    /**
239     * No leading or tailing slash
240     */
241    public function getPackageAbsolutePath(): ?string
242    {
243        return !empty($this->packageAbsolutePath)
244            ? $this->packageAbsolutePath
245            : null;
246    }
247
248    /**
249     *
250     * e.g. ['psr-4' => [ 'BrianHenryIE\Project' => 'src' ]]
251     * e.g. ['psr-4' => [ 'BrianHenryIE\Project' => ['src','lib] ]]
252     * e.g. ['classmap' => [ 'src', 'lib' ]]
253     * e.g. ['files' => [ 'lib', 'functions.php' ]]
254     *
255     * @return AutoloadKeyArray
256     */
257    public function getAutoload(): array
258    {
259        return $this->autoload;
260    }
261
262    /**
263     * The names of the packages in the composer.json's "requires" field (without version).
264     *
265     * Excludes PHP, ext-*, since we won't be copying or prefixing them.
266     *
267     * @return string[]
268     */
269    public function getRequiresNames(): array
270    {
271        // Unset PHP, ext-*.
272        $removePhpExt = function ($element) {
273            return !( 0 === strpos($element, 'ext') || 'php' === $element );
274        };
275
276        return array_filter($this->requiresNames, $removePhpExt);
277    }
278
279    public function getLicense():string
280    {
281        return $this->license;
282    }
283
284    /**
285     * Should the file be copied? (defaults to yes)
286     */
287    public function setCopy(bool $isCopy): void
288    {
289        $this->isCopy = $isCopy;
290    }
291
292    /**
293     * Should the file be copied? (defaults to yes)
294     */
295    public function isCopy(): bool
296    {
297        return $this->isCopy;
298    }
299
300    /**
301     * Has the file been copied? (defaults to no)
302     */
303    public function setDidCopy(bool $didCopy): void
304    {
305        $this->didCopy = $didCopy;
306    }
307
308    /**
309     * Has the file been copied? (defaults to no)
310     */
311    public function didCopy(): bool
312    {
313        return $this->didCopy;
314    }
315
316    /**
317     * Should the file be deleted? (defaults to no)
318     */
319    public function setDelete(bool $isDelete): void
320    {
321        $this->isDelete = $isDelete;
322    }
323
324    /**
325     * Should the file be deleted? (defaults to no)
326     */
327    public function isDoDelete(): bool
328    {
329        return $this->isDelete;
330    }
331
332    /**
333     * Has the file been deleted? (defaults to no)
334     */
335    public function setDidDelete(bool $didDelete): void
336    {
337        $this->didDelete = $didDelete;
338    }
339
340    /**
341     * Has the file been deleted? (defaults to no)
342     */
343    public function didDelete(): bool
344    {
345        return $this->didDelete;
346    }
347
348    public function setProjectVendorDirectory(string $parentProjectVendorDirectory)
349    {
350        $this->packageAbsolutePath = $parentProjectVendorDirectory . '/' . $this->vendorRelativePath;
351    }
352
353    public function setPackageAbsolutePath(string $packageAbsolutePath)
354    {
355        $this->packageAbsolutePath = $packageAbsolutePath;
356    }
357
358    public function setRealpath(string $realpath)
359    {
360        $this->packageRealPath = $realpath;
361    }
362
363    public function getRealPath(): ?string
364    {
365        return $this->packageRealPath ?? $this->getPackageAbsolutePath();
366    }
367
368    public function addFile(FileWithDependency $file): void
369    {
370        $this->files[$file->getPackageRelativePath()] = $file;
371    }
372
373    public function getFile(string $path): ?FileWithDependency
374    {
375        return $this->files[$path] ?? null;
376    }
377}