Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
66.67% covered (warning)
66.67%
26 / 39
33.33% covered (danger)
33.33%
3 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ComposerPackage
66.67% covered (warning)
66.67%
26 / 39
33.33% covered (danger)
33.33%
3 / 9
32.37
0.00% covered (danger)
0.00%
0 / 1
 fromFile
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 fromComposerJsonArray
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
77.27% covered (warning)
77.27%
17 / 22
0.00% covered (danger)
0.00%
0 / 1
9.95
 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
2
 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
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 Composer\Composer;
11use Composer\Factory;
12use Composer\IO\NullIO;
13
14/**
15 * @phpstan-type AutoloadKey array{files?:array<string>,classmap?:array<string>,"psr-4"?:array<string,string|array<string>>}
16 */
17class ComposerPackage
18{
19    /**
20     * The composer.json file as parsed by Composer.
21     *
22     * @see \Composer\Factory::create
23     *
24     * @var \Composer\Composer
25     */
26    protected \Composer\Composer $composer;
27
28    /**
29     * The name of the project in composer.json.
30     *
31     * e.g. brianhenryie/my-project
32     *
33     * @var string
34     */
35    protected string $packageName;
36
37    /**
38     * Virtual packages and meta packages do not have a composer.json.
39     * Some packages are installed in a different directory name than their package name.
40     *
41     * @var ?string
42     */
43    protected ?string $relativePath = null;
44
45    /**
46     * Packages can be symlinked from outside the current project directory.
47     *
48     * @var ?string
49     */
50    protected ?string $packageAbsolutePath = null;
51
52    /**
53     * The discovered files, classmap, psr0 and psr4 autoload keys discovered (as parsed by Composer).
54     *
55     * @var AutoloadKey
56     */
57    protected array $autoload = [];
58
59    /**
60     * The names in the composer.json's "requires" field (without versions).
61     *
62     * @var string[]
63     */
64    protected array $requiresNames = [];
65
66    protected string $license;
67
68    /**
69     * @param string $absolutePath The absolute path to the vendor folder with the composer.json "name",
70     *          i.e. the domain/package definition, which is the vendor subdir from where the package's
71     *          composer.json should be read.
72     * @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
73     *                                    another which Strauss can use.
74     * @return ComposerPackage
75     */
76    public static function fromFile(string $absolutePath, array $overrideAutoload = null): ComposerPackage
77    {
78        if (is_dir($absolutePath)) {
79            $absolutePath = rtrim($absolutePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'composer.json';
80        }
81
82        $composer = Factory::create(new NullIO(), $absolutePath, true);
83
84        return new ComposerPackage($composer, $overrideAutoload);
85    }
86
87    /**
88     * This is used for virtual packages, which don't have a composer.json.
89     *
90     * @param array{name?:string, license?:string, requires?:array<string,string>, autoload?:AutoloadKey} $jsonArray composer.json decoded to array
91     * @param ?AutoloadKey $overrideAutoload New autoload rules to replace the existing ones.
92     */
93    public static function fromComposerJsonArray($jsonArray, array $overrideAutoload = null): ComposerPackage
94    {
95        $factory = new Factory();
96        $io = new NullIO();
97        $composer = $factory->createComposer($io, $jsonArray, true);
98
99        return new ComposerPackage($composer, $overrideAutoload);
100    }
101
102    /**
103     * Create a PHP object to represent a composer package.
104     *
105     * @param Composer $composer
106     * @param ?AutoloadKey $overrideAutoload Optional configuration to replace the package's own autoload definition with another which Strauss can use.
107     */
108    public function __construct(Composer $composer, array $overrideAutoload = null)
109    {
110        $this->composer = $composer;
111
112        $this->packageName = $composer->getPackage()->getName();
113
114        $composerJsonFileAbsolute = $composer->getConfig()->getConfigSource()->getName();
115
116        $absolutePath = realpath(dirname($composerJsonFileAbsolute));
117        if (false !== $absolutePath) {
118            $this->packageAbsolutePath = $absolutePath . DIRECTORY_SEPARATOR;
119        }
120
121        $vendorDirectory = $this->composer->getConfig()->get('vendor-dir');
122        if (file_exists($vendorDirectory . DIRECTORY_SEPARATOR . $this->packageName)) {
123            $this->relativePath = $this->packageName;
124            $this->packageAbsolutePath = realpath($vendorDirectory . DIRECTORY_SEPARATOR . $this->packageName) . DIRECTORY_SEPARATOR;
125        // If the package is symlinked, the path will be outside the working directory.
126        } elseif (0 !== strpos($absolutePath, getcwd()) && 1 === preg_match('/.*[\/\\\\]([^\/\\\\]*[\/\\\\][^\/\\\\]*)[\/\\\\][^\/\\\\]*/', $vendorDirectory, $output_array)) {
127            $this->relativePath = $output_array[1];
128        } elseif (1 === preg_match('/.*[\/\\\\]([^\/\\\\]+[\/\\\\][^\/\\\\]+)[\/\\\\]composer.json/', $composerJsonFileAbsolute, $output_array)) {
129        // Not every package gets installed to a folder matching its name (crewlabs/unsplash).
130            $this->relativePath = $output_array[1];
131        }
132
133        if (!is_null($overrideAutoload)) {
134            $composer->getPackage()->setAutoload($overrideAutoload);
135        }
136
137        $this->autoload = $composer->getPackage()->getAutoload();
138
139        foreach ($composer->getPackage()->getRequires() as $_name => $packageLink) {
140            $this->requiresNames[] = $packageLink->getTarget();
141        }
142
143        // Try to get the license from the package's composer.json, assume proprietary (all rights reserved!).
144        $this->license = !empty($composer->getPackage()->getLicense())
145            ? implode(',', $composer->getPackage()->getLicense())
146            : 'proprietary?';
147    }
148
149    /**
150     * Composer package project name.
151     *
152     * vendor/project-name
153     *
154     * @return string
155     */
156    public function getPackageName(): string
157    {
158        return $this->packageName;
159    }
160
161    public function getRelativePath(): ?string
162    {
163        return $this->relativePath . DIRECTORY_SEPARATOR;
164    }
165
166    public function getPackageAbsolutePath(): ?string
167    {
168        return $this->packageAbsolutePath;
169    }
170
171    /**
172     *
173     * e.g. ['psr-4' => [ 'BrianHenryIE\Project' => 'src' ]]
174     * e.g. ['psr-4' => [ 'BrianHenryIE\Project' => ['src','lib] ]]
175     * e.g. ['classmap' => [ 'src', 'lib' ]]
176     * e.g. ['files' => [ 'lib', 'functions.php' ]]
177     *
178     * @return AutoloadKey
179     */
180    public function getAutoload(): array
181    {
182        return $this->autoload;
183    }
184
185    /**
186     * The names of the packages in the composer.json's "requires" field (without version).
187     *
188     * Excludes PHP, ext-*, since we won't be copying or prefixing them.
189     *
190     * @return string[]
191     */
192    public function getRequiresNames(): array
193    {
194        // Unset PHP, ext-*.
195        $removePhpExt = function ($element) {
196            return !( 0 === strpos($element, 'ext') || 'php' === $element );
197        };
198
199        return array_filter($this->requiresNames, $removePhpExt);
200    }
201
202    public function getLicense():string
203    {
204        return $this->license;
205    }
206}