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