Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.58% covered (warning)
73.58%
39 / 53
68.42% covered (warning)
68.42%
13 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
ComposerPackage
73.58% covered (warning)
73.58%
39 / 53
68.42% covered (warning)
68.42%
13 / 19
48.71
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
78.57% covered (warning)
78.57%
22 / 28
0.00% covered (danger)
0.00%
0 / 1
12.19
 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
1
 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        $composerAbsoluteDirectoryPath = realpath(dirname($composerJsonFileAbsolute));
145        if (false !== $composerAbsoluteDirectoryPath) {
146            $composerAbsoluteDirectoryPath = FileSystem::normalizeDirSeparator($composerAbsoluteDirectoryPath);
147            $this->packageAbsolutePath = $composerAbsoluteDirectoryPath . '/';
148        }
149        $composerAbsoluteDirectoryPath = $composerAbsoluteDirectoryPath ?: FileSystem::normalizeDirSeparator(dirname($composerJsonFileAbsolute));
150
151        $currentWorkingDirectory = getcwd();
152        if ($currentWorkingDirectory === false) {
153            /**
154             * @see Platform::getCwd()
155             */
156            throw new Exception('Could not determine working directory. Please comment out ~'.__LINE__.' in ' . __FILE__.' and see does it work regardless.');
157        }
158        $currentWorkingDirectory = FileSystem::normalizeDirSeparator($currentWorkingDirectory);
159
160        /** @var string $vendorAbsoluteDirectoryPath */
161        $vendorAbsoluteDirectoryPath = $this->composer->getConfig()->get('vendor-dir');
162        if (file_exists($vendorAbsoluteDirectoryPath . '/' . $this->packageName)) {
163            $this->relativePath = $this->packageName;
164            $this->packageAbsolutePath = FileSystem::normalizeDirSeparator(realpath($vendorAbsoluteDirectoryPath . '/' . $this->packageName)) . '/';
165        // If the package is symlinked, the path will be outside the working directory.
166        } elseif (0 !== strpos($composerAbsoluteDirectoryPath, $currentWorkingDirectory) && 1 === preg_match('/.*[\/\\\\]([^\/\\\\]*[\/\\\\][^\/\\\\]*)[\/\\\\][^\/\\\\]*/', $vendorAbsoluteDirectoryPath, $output_array)) {
167            $this->relativePath = $output_array[1];
168        } elseif (1 === preg_match('/.*[\/\\\\]([^\/\\\\]+[\/\\\\][^\/\\\\]+)[\/\\\\]composer.json/', $composerJsonFileAbsolute, $output_array)) {
169        // Not every package gets installed to a folder matching its name (crewlabs/unsplash).
170            $this->relativePath = $output_array[1];
171        }
172
173        if (!is_null($overrideAutoload)) {
174            $composer->getPackage()->setAutoload($overrideAutoload);
175        }
176
177        $this->autoload = $composer->getPackage()->getAutoload();
178
179        foreach ($composer->getPackage()->getRequires() as $_name => $packageLink) {
180            $this->requiresNames[] = $packageLink->getTarget();
181        }
182
183        // Try to get the license from the package's composer.json, assume proprietary (all rights reserved!).
184        $this->license = !empty($composer->getPackage()->getLicense())
185            ? implode(',', $composer->getPackage()->getLicense())
186            : 'proprietary?';
187    }
188
189    /**
190     * Composer package project name.
191     *
192     * vendor/project-name
193     *
194     * @return string
195     */
196    public function getPackageName(): string
197    {
198        return $this->packageName;
199    }
200
201    /**
202     * Is this relative to vendor?
203     */
204    public function getRelativePath(): ?string
205    {
206        return is_null($this->relativePath) ? null : FileSystem::normalizeDirSeparator($this->relativePath) . '/';
207    }
208
209    public function getPackageAbsolutePath(): ?string
210    {
211        return $this->packageAbsolutePath;
212    }
213
214    /**
215     *
216     * e.g. ['psr-4' => [ 'BrianHenryIE\Project' => 'src' ]]
217     * e.g. ['psr-4' => [ 'BrianHenryIE\Project' => ['src','lib] ]]
218     * e.g. ['classmap' => [ 'src', 'lib' ]]
219     * e.g. ['files' => [ 'lib', 'functions.php' ]]
220     *
221     * @return AutoloadKeyArray
222     */
223    public function getAutoload(): array
224    {
225        return $this->autoload;
226    }
227
228    /**
229     * The names of the packages in the composer.json's "requires" field (without version).
230     *
231     * Excludes PHP, ext-*, since we won't be copying or prefixing them.
232     *
233     * @return string[]
234     */
235    public function getRequiresNames(): array
236    {
237        // Unset PHP, ext-*.
238        $removePhpExt = function ($element) {
239            return !( 0 === strpos($element, 'ext') || 'php' === $element );
240        };
241
242        return array_filter($this->requiresNames, $removePhpExt);
243    }
244
245    public function getLicense():string
246    {
247        return $this->license;
248    }
249
250    /**
251     * Should the file be copied? (defaults to yes)
252     */
253    public function setCopy(bool $isCopy): void
254    {
255        $this->isCopy = $isCopy;
256    }
257
258    /**
259     * Should the file be copied? (defaults to yes)
260     */
261    public function isCopy(): bool
262    {
263        return $this->isCopy;
264    }
265
266    /**
267     * Has the file been copied? (defaults to no)
268     */
269    public function setDidCopy(bool $didCopy): void
270    {
271        $this->didCopy = $didCopy;
272    }
273
274    /**
275     * Has the file been copied? (defaults to no)
276     */
277    public function didCopy(): bool
278    {
279        return $this->didCopy;
280    }
281
282    /**
283     * Should the file be deleted? (defaults to no)
284     */
285    public function setDelete(bool $isDelete): void
286    {
287        $this->isDelete = $isDelete;
288    }
289
290    /**
291     * Should the file be deleted? (defaults to no)
292     */
293    public function isDoDelete(): bool
294    {
295        return $this->isDelete;
296    }
297
298    /**
299     * Has the file been deleted? (defaults to no)
300     */
301    public function setDidDelete(bool $didDelete): void
302    {
303        $this->didDelete = $didDelete;
304    }
305
306    /**
307     * Has the file been deleted? (defaults to no)
308     */
309    public function didDelete(): bool
310    {
311        return $this->didDelete;
312    }
313
314    public function addFile(FileWithDependency $file): void
315    {
316        $this->files[$file->getPackageRelativePath()] = $file;
317    }
318
319    public function getFile(string $path): ?FileWithDependency
320    {
321        return $this->files[$path] ?? null;
322    }
323}