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