Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.12% covered (warning)
69.12%
141 / 204
49.21% covered (danger)
49.21%
31 / 63
CRAP
0.00% covered (danger)
0.00%
0 / 1
StraussConfig
69.12% covered (warning)
69.12%
141 / 204
49.21% covered (danger)
49.21%
31 / 63
654.00
0.00% covered (danger)
0.00%
0 / 1
 __construct
95.00% covered (success)
95.00%
76 / 80
0.00% covered (danger)
0.00%
0 / 1
31
 getAbsoluteTargetDirectory
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 isTargetDirectoryVendor
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRelativeTargetDirectory
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 setTargetDirectory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAbsoluteVendorDirectory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setRelativeVendorDirectory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNamespacePrefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 setNamespacePrefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getClassmapPrefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setClassmapPrefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFunctionsPrefix
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
5.07
 setFunctionsPrefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getConstantsPrefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setConstantsPrefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUpdateCallSites
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setUpdateCallSites
61.54% covered (warning)
61.54%
8 / 13
0.00% covered (danger)
0.00%
0 / 1
15.69
 setExcludeFromCopy
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 getExcludePackagesFromCopy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExcludeNamespacesFromCopy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExcludeFilePatternsFromCopy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setExcludeFromPrefix
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
4.59
 getExcludePackagesFromPrefixing
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setExcludePackagesFromPrefixing
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExcludeNamespacesFromPrefixing
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getExcludeFilePatternsFromPrefixing
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setExcludeConstants
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 getExcludePackagesFromConstantPrefixing
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExcludeNamespacesFromConstantPrefixing
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getExcludeFilePatternsFromConstantPrefixing
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExcludeConstantNames
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOverrideAutoload
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setOverrideAutoload
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDeleteVendorFiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDeleteVendorPackages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDeleteVendorFiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDeleteVendorPackages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExcludeGitFiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setExcludeGitFiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPackages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPackages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPackagesToCopy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setPackagesToCopy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPackagesToPrefix
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setPackagesToPrefix
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isClassmapOutput
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setClassmapOutput
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setExcludePackages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNamespaceReplacementPatterns
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setNamespaceReplacementPatterns
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isIncludeModifiedDate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setIncludeModifiedDate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isIncludeAuthor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setIncludeAuthor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDryRun
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDryRun
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isIncludeRootAutoload
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isOptimizeAutoloader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setIncludeRootAutoload
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setOptimizeAutoloader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 updateFromCli
61.11% covered (warning)
61.11%
11 / 18
0.00% covered (danger)
0.00%
0 / 1
28.23
 isCreateAliases
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
12
 getProjectDirectory
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
1<?php
2/**
3 * The extra/strauss key in composer.json.
4 */
5
6namespace BrianHenryIE\Strauss\Composer\Extra;
7
8use BrianHenryIE\Strauss\Composer\ComposerPackage;
9use BrianHenryIE\Strauss\Config\AliasesConfigInterface;
10use BrianHenryIE\Strauss\Config\AutoloadConfigInterface;
11use BrianHenryIE\Strauss\Config\AutoloadFilesEnumeratorConfigInterface;
12use BrianHenryIE\Strauss\Config\ChangeEnumeratorConfigInterface;
13use BrianHenryIE\Strauss\Config\CleanupConfigInterface;
14use BrianHenryIE\Strauss\Config\CopierConfigInterface;
15use BrianHenryIE\Strauss\Config\MarkSymbolsForRenamingConfigInterface;
16use BrianHenryIE\Strauss\Config\FileCopyScannerConfigInterface;
17use BrianHenryIE\Strauss\Config\FileEnumeratorConfig;
18use BrianHenryIE\Strauss\Config\FileSymbolScannerConfigInterface;
19use BrianHenryIE\Strauss\Config\OptimizeAutoloaderConfigInterface;
20use BrianHenryIE\Strauss\Config\PrefixerConfigInterface;
21use BrianHenryIE\Strauss\Console\Commands\DependenciesCommand;
22use BrianHenryIE\Strauss\Helpers\FileSystem;
23use BrianHenryIE\Strauss\Pipeline\Autoload\DumpAutoload;
24use Composer\Composer;
25use Exception;
26use InvalidArgumentException;
27use JsonMapper\Enums\TextNotation;
28use JsonMapper\JsonMapperFactory;
29use JsonMapper\Middleware\CaseConversion;
30use JsonMapper\Middleware\Rename\Rename;
31use Symfony\Component\Console\Input\InputInterface;
32
33class StraussConfig implements
34    AliasesConfigInterface,
35    AutoloadConfigInterface,
36    AutoloadFilesEnumeratorConfigInterface,
37    ChangeEnumeratorConfigInterface,
38    CleanupConfigInterface,
39    CopierConfigInterface,
40    MarkSymbolsForRenamingConfigInterface,
41    FileSymbolScannerConfigInterface,
42    FileEnumeratorConfig,
43    FileCopyScannerConfigInterface,
44    OptimizeAutoloaderConfigInterface,
45    PrefixerConfigInterface,
46    ReplaceConfigInterface
47{
48    /**
49     * The directory containing `composer.json`. Probably `cwd()`.
50     */
51    protected string $projectDirectory;
52
53    /**
54     * The output directory.
55     */
56    protected string $targetDirectory = 'vendor-prefixed';
57
58    /**
59     * The vendor directory.
60     *
61     * Probably 'vendor/'
62     */
63    protected string $relativeVendorDirectory = 'vendor';
64
65    /**
66     * `namespacePrefix` is the prefix to be given to any namespaces.
67     * Presumably this will take the form `My_Project_Namespace\dep_directory`.
68     *
69     * @link https://www.php-fig.org/psr/psr-4/
70     */
71    protected ?string $namespacePrefix = null;
72
73    /**
74     *
75     */
76    protected ?string $classmapPrefix = null;
77
78    /**
79     * Null to disable. Otherwise, suggested it is all lowercase with a trailing underscore.
80     *
81     * @var string|bool|null
82     */
83    protected $functionsPrefix;
84
85    /**
86     * @var ?string
87     */
88    protected ?string $constantsPrefix = null;
89
90    /**
91     * Should replacements be performed in project files?
92     *
93     * When null, files in the project's `autoload` key are scanned and changes which have been performed on the
94     * vendor packages are reflected in the project files.
95     *
96     * When an array of relative file paths are provided, the files in those directories are updated.
97     *
98     * An empty array disables updating project files.
99     *
100     * @var ?string[]
101     */
102    protected ?array $updateCallSites = array();
103
104    /**
105     * Packages to copy and (maybe) prefix.
106     *
107     * If this is empty, the "requires" list in the project composer.json is used.
108     *
109     * @var string[]
110     */
111    protected array $packages = [];
112
113    /**
114     * @var array<string,ComposerPackage>
115     */
116    protected array $packagesToCopy = [];
117
118    /**
119     *
120     * @var array<string,ComposerPackage>
121     */
122    protected array $packagesToPrefix = [];
123
124    /**
125     * Back-compatibility with Mozart.
126     *
127     * @var string[]
128     */
129    private array $excludePackages;
130
131    /**
132     * 'exclude_from_copy' in composer/extra config.
133     *
134     * @var array{packages: string[], namespaces: string[], file_patterns: string[]}
135     */
136    protected array $excludeFromCopy = array('file_patterns'=>array(),'namespaces'=>array(),'packages'=>array());
137
138    /**
139     * @var array{packages: string[], namespaces: string[], file_patterns: string[]}
140     */
141    protected array $excludeFromPrefix = array('file_patterns'=>array(),'namespaces'=>array(),'packages'=>array());
142
143    /**
144     * Exclude constants from prefixing only (same shape as exclude_from_prefix).
145     *
146     * @var array{packages: string[], namespaces: string[], file_patterns: string[], constants: string[]}
147     */
148    protected array $excludeConstants = array('file_patterns'=>array(),'namespaces'=>array(),'packages'=>array(),'constants'=>array());
149
150    /**
151     * An array of autoload keys to replace packages' existing autoload key.
152     *
153     * e.g. when
154     * * A package has no autoloader
155     * * A package specified both a PSR-4 and a classmap but only needs one
156     * ...
157     *
158     * @var array<string, array{files?:array<string>,classmap?:array<string>,"psr-4":array<string|array<string>>}>|array{} $overrideAutoload
159     */
160    protected array $overrideAutoload = [];
161
162    /**
163     * After completing prefixing should the source files be deleted?
164     * This does not affect symlinked directories.
165     */
166    protected bool $deleteVendorFiles = false;
167
168    /**
169     * After completing prefixing should the source packages be deleted?
170     * This does not affect symlinked directories.
171     */
172    protected bool $deleteVendorPackages = false;
173
174    protected bool $classmapOutput;
175
176    /**
177     * A dictionary of regex captures => regex replacements.
178     *
179     * E.g. used to avoid repetition of the plugin vendor name in namespaces.
180     * `"~BrianHenryIE\\\\(.*)~" : "BrianHenryIE\\WC_Cash_App_Gateway\\\\$1"`.
181     *
182     * @var array<string, string> $namespaceReplacementPatterns
183     */
184    protected array $namespaceReplacementPatterns = array();
185
186    /**
187     * Should a modified date be included in the header for modified files?
188     */
189    protected bool $includeModifiedDate = true;
190
191    /**
192     * Should the author name be included in the header for modified files?
193     */
194    protected bool $includeAuthor = true;
195
196    /**
197     * Should the changes be printed to console rather than files modified?
198     */
199    protected bool $dryRun = false;
200
201    /**
202     * Should the root autoload be included when generating the strauss autoloader?
203     */
204    protected bool $includeRootAutoload = false;
205
206    /**
207     * Should Composer autoload generation be optimized and classmap authoritative?
208     */
209    protected bool $optimizeAutoloader = true;
210
211    /**
212     * Should `.git`, `.gitignore`-matched and `.gitattributes export-ignore` files be skipped when
213     * enumerating each package's files (mimicking `git archive` / Composer dist behaviour)?
214     */
215    protected bool $excludeGitFiles = true;
216
217    /**
218     * Read any existing Mozart config.
219     * Overwrite it with any Strauss config.
220     * Provide sensible defaults.
221     *
222     * @param ?Composer $composer
223     *
224     * @throws Exception
225     */
226    public function __construct(?Composer $composer = null)
227    {
228
229        $configExtraSettings = null;
230
231        // Backwards compatibility with Mozart.
232        if (isset($composer, $composer->getPackage()->getExtra()['mozart'])) {
233            $configExtraSettings = (object)$composer->getPackage()->getExtra()['mozart'];
234
235            // Default setting for Mozart.
236            $this->setDeleteVendorFiles(true);
237        }
238
239        if (isset($composer, $composer->getPackage()->getExtra()['strauss'])) {
240            $configExtraSettings = (object)$composer->getPackage()->getExtra()['strauss'];
241        }
242
243        if (!is_null($configExtraSettings)) {
244            $mapper = (new JsonMapperFactory())->bestFit();
245
246            $rename = new Rename();
247            $rename->addMapping(StraussConfig::class, 'dep_directory', 'targetDirectory');
248            $rename->addMapping(StraussConfig::class, 'dep_namespace', 'namespacePrefix');
249
250            $rename->addMapping(StraussConfig::class, 'exclude_packages', 'excludePackages');
251            $rename->addMapping(StraussConfig::class, 'delete_vendor_files', 'deleteVendorFiles');
252            $rename->addMapping(StraussConfig::class, 'delete_vendor_packages', 'deleteVendorPackages');
253
254            $rename->addMapping(StraussConfig::class, 'exclude_prefix_packages', 'excludePackagesFromPrefixing');
255
256            $rename->addMapping(StraussConfig::class, 'include_root_autoload', 'includeRootAutoload');
257
258            $rename->addMapping(StraussConfig::class, 'function_prefix', 'functionsPrefix');
259
260            $rename->addMapping(StraussConfig::class, 'constant_prefix', 'constantsPrefix');
261
262            $mapper->unshift($rename);
263            $mapper->push(new CaseConversion(TextNotation::UNDERSCORE(), TextNotation::CAMEL_CASE()));
264
265            $mapper->mapObject($configExtraSettings, $this);
266        }
267
268        // Defaults.
269        // * Use PSR-4 autoloader key
270        // * Use PSR-0 autoloader key
271        // * Use the package name
272        if (! isset($this->namespacePrefix)) {
273            if (isset($composer, $composer->getPackage()->getAutoload()['psr-4']) && !empty($composer->getPackage()->getAutoload()['psr-4'])) {
274                $this->setNamespacePrefix(array_key_first($composer->getPackage()->getAutoload()['psr-4']));
275            } elseif (isset($composer, $composer->getPackage()->getAutoload()['psr-0']) && !empty($composer->getPackage()->getAutoload()['psr-0'])) {
276                $this->setNamespacePrefix(array_key_first($composer->getPackage()->getAutoload()['psr-0']));
277            } elseif (isset($composer) && '__root__' !== $composer->getPackage()->getName()) {
278                $packageName = $composer->getPackage()->getName();
279                // Replace all non-word characters with underscores.
280                $namespacePrefix = preg_replace('/[^\w\/]+/', '_', $packageName) ?? $packageName;
281                $namespacePrefix = str_replace('/', '\\', $namespacePrefix) . '\\';
282                $namespacePrefix = preg_replace_callback('/(?<=^|_|\\\\)[a-z]/', function ($match) {
283                    return strtoupper($match[0]);
284                }, $namespacePrefix) ?? $namespacePrefix;
285                $this->setNamespacePrefix($namespacePrefix);
286            } elseif (isset($this->classmapPrefix) && !empty($this->getClassmapPrefix())) {
287                $namespacePrefix = rtrim($this->getClassmapPrefix(), '_');
288                $this->setNamespacePrefix($namespacePrefix);
289            }
290        }
291
292        if (! isset($this->classmapPrefix)) {
293            if (isset($composer, $composer->getPackage()->getAutoload()['psr-4'])) {
294                $autoloadKey = array_key_first($composer->getPackage()->getAutoload()['psr-4']);
295                $classmapPrefix = str_replace("\\", "_", $autoloadKey);
296                $this->setClassmapPrefix($classmapPrefix);
297            } elseif (isset($composer, $composer->getPackage()->getAutoload()['psr-0'])) {
298                $autoloadKey = array_key_first($composer->getPackage()->getAutoload()['psr-0']);
299                $classmapPrefix = str_replace("\\", "_", $autoloadKey);
300                $this->setClassmapPrefix($classmapPrefix);
301            } elseif (isset($composer) && '__root__' !== $composer->getPackage()->getName()) {
302                $packageName = $composer->getPackage()->getName();
303                $classmapPrefix = preg_replace('/[^\w\/]+/', '_', $packageName);
304                $classmapPrefix = str_replace('/', '\\', $classmapPrefix);
305                // Uppercase the first letter of each word.
306                $classmapPrefix = preg_replace_callback('/(?<=^|_|\\\\)[a-z]/', function ($match) {
307                    return strtoupper($match[0]);
308                }, $classmapPrefix);
309                $classmapPrefix = str_replace("\\", "_", $classmapPrefix);
310                $this->setClassmapPrefix($classmapPrefix);
311            } elseif (isset($this->namespacePrefix)) {
312                $classmapPrefix = preg_replace('/[^\w\/]+/', '_', $this->getNamespacePrefix()) ?? str_replace('\\', '_', $this->getNamespacePrefix());
313                $classmapPrefix = rtrim($classmapPrefix, '_') . '_';
314                $this->setClassmapPrefix($classmapPrefix);
315            }
316        }
317
318//        if (!isset($this->namespacePrefix) || !isset($this->classmapPrefix)) {
319//            throw new Exception('Prefix not set. Please set `namespace_prefix`, `classmap_prefix` in composer.json/extra/strauss.');
320//        }
321
322        if (isset($composer) && empty($this->packages)) {
323            $this->packages = array_map(function (\Composer\Package\Link $element) {
324                return $element->getTarget();
325            }, $composer->getPackage()->getRequires());
326        }
327
328        // If the bool flag for classmapOutput wasn't set in the JSON config.
329        if (!isset($this->classmapOutput)) {
330            $this->classmapOutput = true;
331            // Check each autoloader.
332            if (isset($composer)) {
333                foreach ($composer->getPackage()->getAutoload() as $autoload) {
334                    // To see if one of its paths.
335                    foreach ($autoload as $entry) {
336                        $paths = (array) $entry;
337                        foreach ($paths as $path) {
338                            // Matches the target directory.
339                            if (trim($path, '\\/') === $this->getAbsoluteTargetDirectory()) {
340                                $this->classmapOutput = false;
341                                break 3;
342                            }
343                        }
344                    }
345                }
346            }
347        }
348
349        // TODO: Throw an exception if any regex patterns in config are invalid.
350        // https://stackoverflow.com/questions/4440626/how-can-i-validate-regex
351        // preg_match('~Valid(Regular)Expression~', null) === false);
352
353        if (isset($configExtraSettings, $configExtraSettings->updateCallSites)) {
354            if (true === $configExtraSettings->updateCallSites) {
355                $this->updateCallSites = null;
356            } elseif (false === $configExtraSettings->updateCallSites) {
357                $this->updateCallSites = array();
358            } elseif (is_array($configExtraSettings->updateCallSites)) {
359                $this->updateCallSites = $configExtraSettings->updateCallSites;
360            } else {
361                // uh oh.
362            }
363        }
364    }
365
366    /**
367     * `target_directory` will always be returned without a leading nor trailing slash.
368     */
369    public function getAbsoluteTargetDirectory(): string
370    {
371        return FileSystem::normalizeDirSeparator(
372            trim($this->getProjectDirectory(), '\\/') . '/' . trim($this->targetDirectory, '\\/')
373        );
374    }
375
376    public function isTargetDirectoryVendor(): bool
377    {
378        return $this->getAbsoluteVendorDirectory() === $this->getAbsoluteTargetDirectory();
379    }
380
381    /**
382     * Default 'vendor-prefixed'. No leading or trailing slash.
383     */
384    public function getRelativeTargetDirectory(): string
385    {
386        return FileSystem::normalizeDirSeparator(
387            trim($this->targetDirectory, '\\/')
388        );
389    }
390
391    /**
392     * @param string $targetDirectory
393     */
394    public function setTargetDirectory(string $targetDirectory): void
395    {
396        $this->targetDirectory = $targetDirectory;
397    }
398
399    /**
400     * No leading or trailing slash.
401     */
402    public function getAbsoluteVendorDirectory(): string
403    {
404        return trim($this->getProjectDirectory() . '/' . $this->relativeVendorDirectory, '\\/');
405    }
406
407    /**
408     * @param string $relativeVendorDirectory
409     */
410    public function setRelativeVendorDirectory(string $relativeVendorDirectory): void
411    {
412        $this->relativeVendorDirectory = $relativeVendorDirectory;
413    }
414
415    /**
416     * With no trailing slash and no leading slash.
417     */
418    public function getNamespacePrefix(): ?string
419    {
420        return !isset($this->namespacePrefix) ? null : trim($this->namespacePrefix, '\\');
421    }
422
423    /**
424     * @param string $namespacePrefix
425     */
426    public function setNamespacePrefix(string $namespacePrefix): void
427    {
428        $this->namespacePrefix = $namespacePrefix;
429    }
430
431    /**
432     * @return string
433     */
434    public function getClassmapPrefix(): ?string
435    {
436        return $this->classmapPrefix;
437    }
438
439    /**
440     * @param string $classmapPrefix
441     */
442    public function setClassmapPrefix(string $classmapPrefix): void
443    {
444        $this->classmapPrefix = $classmapPrefix;
445    }
446
447    public function getFunctionsPrefix(): ?string
448    {
449        if (!isset($this->functionsPrefix) && !is_null($this->getClassmapPrefix())) {
450            return strtolower($this->getClassmapPrefix());
451        }
452        if (empty($this->functionsPrefix)) {
453            return null;
454        }
455        if (is_string($this->functionsPrefix)) {
456            return $this->functionsPrefix;
457        }
458        return null;
459    }
460
461    /**
462     * @param string|bool|null $functionsPrefix
463     */
464    public function setFunctionsPrefix($functionsPrefix): void
465    {
466        $this->functionsPrefix = $functionsPrefix;
467    }
468
469    /**
470     * @return string
471     */
472    public function getConstantsPrefix(): ?string
473    {
474        return $this->constantsPrefix;
475    }
476
477    /**
478     * @param string $constantsPrefix
479     */
480    public function setConstantsPrefix(string $constantsPrefix): void
481    {
482        $this->constantsPrefix = $constantsPrefix;
483    }
484
485    /**
486     * List of files and directories to update call sites in. Empty to disable. Null infers from the project's autoload key.
487     *
488     * @return string[]|null
489     */
490    public function getUpdateCallSites(): ?array
491    {
492        return $this->updateCallSites;
493    }
494
495    /**
496     * @param string[]|array{0:bool}|null $updateCallSites
497     * @throws InvalidArgumentException
498     */
499    public function setUpdateCallSites($updateCallSites): void
500    {
501        if (is_array($updateCallSites) && count($updateCallSites) === 1 && $updateCallSites[0] === true) {
502            // Setting `null` instructs Strauss to update call sites in the project's autoload key.
503            $this->updateCallSites = null;
504            return;
505        } elseif (is_array($updateCallSites) && count($updateCallSites) === 1 && $updateCallSites[0] === false) {
506            $this->updateCallSites = array();
507            return;
508        } elseif (is_array($updateCallSites) && isset($updateCallSites[0]) && !is_bool($updateCallSites[0])) {
509            $this->updateCallSites = array_filter(
510                $updateCallSites,
511                'is_string'
512            );
513            return;
514        }
515        throw new InvalidArgumentException('Unexpected value for updateCallSites');
516    }
517
518    /**
519     * @param array{packages?:array<string>, namespaces?:array<string>, file_patterns?:array<string>} $excludeFromCopy
520     */
521    public function setExcludeFromCopy(array $excludeFromCopy): void
522    {
523        foreach (array( 'packages', 'namespaces', 'file_patterns' ) as $key) {
524            if (isset($excludeFromCopy[$key])) {
525                $this->excludeFromCopy[$key] = $excludeFromCopy[$key];
526            }
527        }
528    }
529
530    /**
531     * @return string[]
532     */
533    public function getExcludePackagesFromCopy(): array
534    {
535        return $this->excludeFromCopy['packages'] ?? array();
536    }
537
538    /**
539     * @return string[]
540     */
541    public function getExcludeNamespacesFromCopy(): array
542    {
543        return $this->excludeFromCopy['namespaces'] ?? array();
544    }
545
546    /**
547     * @return string[]
548     */
549    public function getExcludeFilePatternsFromCopy(): array
550    {
551        return $this->excludeFromCopy['file_patterns'] ?? array();
552    }
553
554    /**
555     * @param array{packages?:array<string>, namespaces?:array<string>, file_patterns?:array<string>} $excludeFromPrefix
556     */
557    public function setExcludeFromPrefix(array $excludeFromPrefix): void
558    {
559        if (isset($excludeFromPrefix['packages'])) {
560            $this->excludeFromPrefix['packages'] = $excludeFromPrefix['packages'];
561        }
562        if (isset($excludeFromPrefix['namespaces'])) {
563            $this->excludeFromPrefix['namespaces'] = $excludeFromPrefix['namespaces'];
564        }
565        if (isset($excludeFromPrefix['file_patterns'])) {
566            $this->excludeFromPrefix['file_patterns'] = $excludeFromPrefix['file_patterns'];
567        }
568    }
569
570    /**
571     * When prefixing, do not prefix these packages (which have been copied).
572     *
573     * @return string[]
574     */
575    public function getExcludePackagesFromPrefixing(): array
576    {
577        return $this->excludeFromPrefix['packages'] ?? [];
578    }
579
580    /**
581     * @param string[] $excludePackagesFromPrefixing
582     */
583    public function setExcludePackagesFromPrefixing(array $excludePackagesFromPrefixing): void
584    {
585        $this->excludeFromPrefix['packages'] = $excludePackagesFromPrefixing;
586    }
587
588    /**
589     * @return string[]
590     */
591    public function getExcludeNamespacesFromPrefixing(): array
592    {
593        return array_map(
594            fn(string $packageName) => trim($packageName, '\\/'),
595            $this->excludeFromPrefix['namespaces'] ?? []
596        );
597    }
598
599    /**
600     * @return string[]
601     */
602    public function getExcludeFilePatternsFromPrefixing(): array
603    {
604        return $this->excludeFromPrefix['file_patterns'] ?? array();
605    }
606
607    /**
608     * @param array{packages?:array<string>, namespaces?:array<string>, file_patterns?:array<string>, constants?:array<string>} $excludeConstants
609     */
610    public function setExcludeConstants(array $excludeConstants): void
611    {
612        if (isset($excludeConstants['packages'])) {
613            $this->excludeConstants['packages'] = $excludeConstants['packages'];
614        }
615        if (isset($excludeConstants['namespaces'])) {
616            $this->excludeConstants['namespaces'] = $excludeConstants['namespaces'];
617        }
618        if (isset($excludeConstants['file_patterns'])) {
619            $this->excludeConstants['file_patterns'] = $excludeConstants['file_patterns'];
620        }
621        if (isset($excludeConstants['constants'])) {
622            $this->excludeConstants['constants'] = $excludeConstants['constants'];
623        }
624    }
625
626    /**
627     * @return string[]
628     */
629    public function getExcludePackagesFromConstantPrefixing(): array
630    {
631        return $this->excludeConstants['packages'] ?? [];
632    }
633
634    /**
635     * @return string[]
636     */
637    public function getExcludeNamespacesFromConstantPrefixing(): array
638    {
639        return array_map(
640            fn(string $ns) => trim($ns, '\\/'),
641            $this->excludeConstants['namespaces'] ?? []
642        );
643    }
644
645    /**
646     * @return string[]
647     */
648    public function getExcludeFilePatternsFromConstantPrefixing(): array
649    {
650        return $this->excludeConstants['file_patterns'] ?? [];
651    }
652
653    /**
654     * @return string[]
655     */
656    public function getExcludeConstantNames(): array
657    {
658        return $this->excludeConstants['constants'] ?? [];
659    }
660
661    /**
662     * @return array{}|array<string, array{files?:array<string>,classmap?:array<string>,"psr-4":array<string|array<string>>}> $overrideAutoload Dictionary of package name: autoload rules.
663     */
664    public function getOverrideAutoload(): array
665    {
666        return $this->overrideAutoload;
667    }
668
669    /**
670     * @param array<string, array{files?:array<string>,classmap?:array<string>,"psr-4":array<string|array<string>>}> $overrideAutoload Dictionary of package name: autoload rules.
671     */
672    public function setOverrideAutoload(array $overrideAutoload): void
673    {
674        $this->overrideAutoload = $overrideAutoload;
675    }
676
677    /**
678     * @return bool
679     */
680    public function isDeleteVendorFiles(): bool
681    {
682        return $this->deleteVendorFiles;
683    }
684
685    /**
686     * @return bool
687     */
688    public function isDeleteVendorPackages(): bool
689    {
690        return $this->deleteVendorPackages;
691    }
692
693    /**
694     * @param bool $deleteVendorFiles
695     */
696    public function setDeleteVendorFiles(bool $deleteVendorFiles): void
697    {
698        $this->deleteVendorFiles = $deleteVendorFiles;
699    }
700
701    /**
702     * @param bool $deleteVendorPackages
703     */
704    public function setDeleteVendorPackages(bool $deleteVendorPackages): void
705    {
706        $this->deleteVendorPackages = $deleteVendorPackages;
707    }
708
709    /**
710     * Should exclude `.git` directory and read and follow `.gitignore` and `.gitattributes` files. Mostly relevant for local symlinked packages.
711     */
712    public function getExcludeGitFiles(): bool
713    {
714        return $this->excludeGitFiles;
715    }
716
717    /**
718     * Should Strauss read and respect `.gitignore` and `.gitattributes` files and exclude `.git` directory. Packagist packages have already respected these.
719     */
720    public function setExcludeGitFiles(bool $excludeGitFiles): void
721    {
722        $this->excludeGitFiles = $excludeGitFiles;
723    }
724
725    /**
726     * @return string[]
727     */
728    public function getPackages(): array
729    {
730        return $this->packages;
731    }
732
733    /**
734     * @param string[] $packages
735     */
736    public function setPackages(array $packages): void
737    {
738        $this->packages = $packages;
739    }
740
741    /**
742     * @used-by DumpAutoload::createInstalledVersionsFiles()
743     * @return array<string,ComposerPackage>
744     */
745    public function getPackagesToCopy(): array
746    {
747        return $this->packagesToCopy;
748    }
749
750    /**
751     * @used-by DependenciesCommand::buildDependencyList()
752     *
753     * @param array<string,ComposerPackage> $packagesToCopy
754     */
755    public function setPackagesToCopy(array $packagesToCopy): void
756    {
757        $this->packagesToCopy = $packagesToCopy;
758    }
759
760    /**
761     * @return array<string,ComposerPackage>
762     */
763    public function getPackagesToPrefix(): array
764    {
765        return $this->packagesToPrefix;
766    }
767
768    /**
769     * @param array<string,ComposerPackage> $packagesToPrefix
770     */
771    public function setPackagesToPrefix(array $packagesToPrefix): void
772    {
773        $this->packagesToPrefix = $packagesToPrefix;
774    }
775    /**
776     * TODO: Can we name this `isClassmapOutputEnabled`?
777     */
778    public function isClassmapOutput(): bool
779    {
780        return $this->classmapOutput;
781    }
782
783    /**
784     * @param bool $classmapOutput
785     */
786    public function setClassmapOutput(bool $classmapOutput): void
787    {
788        $this->classmapOutput = $classmapOutput;
789    }
790
791    /**
792     * Backwards compatibility with Mozart.
793     *
794     * @param string[] $excludePackages
795     */
796    public function setExcludePackages(array $excludePackages): void
797    {
798        $this->excludeFromPrefix['packages'] = $excludePackages;
799    }
800
801    /**
802     * @return array<string,string>
803     */
804    public function getNamespaceReplacementPatterns(): array
805    {
806        return $this->namespaceReplacementPatterns;
807    }
808
809    /**
810     * @param array<string,string> $namespaceReplacementPatterns
811     */
812    public function setNamespaceReplacementPatterns(array $namespaceReplacementPatterns): void
813    {
814        $this->namespaceReplacementPatterns = $namespaceReplacementPatterns;
815    }
816
817    /**
818     * @return bool
819     */
820    public function isIncludeModifiedDate(): bool
821    {
822        return $this->includeModifiedDate;
823    }
824
825    /**
826     * @param bool $includeModifiedDate
827     */
828    public function setIncludeModifiedDate(bool $includeModifiedDate): void
829    {
830        $this->includeModifiedDate = $includeModifiedDate;
831    }
832
833
834    /**
835     * @return bool
836     */
837    public function isIncludeAuthor(): bool
838    {
839        return $this->includeAuthor;
840    }
841
842    /**
843     * @param bool $includeAuthor
844     */
845    public function setIncludeAuthor(bool $includeAuthor): void
846    {
847        $this->includeAuthor = $includeAuthor;
848    }
849
850    /**
851     * Should expected changes be printed to console rather than files modified?
852     */
853    public function isDryRun(): bool
854    {
855        return $this->dryRun;
856    }
857
858    /**
859     * Disable making changes to files; output changes to console instead.
860     */
861    public function setDryRun(bool $dryRun): void
862    {
863        $this->dryRun = $dryRun;
864    }
865
866    /**
867     * Should the root autoload be included when generating the strauss autoloader?
868     */
869    public function isIncludeRootAutoload(): bool
870    {
871        return $this->includeRootAutoload;
872    }
873
874    public function isOptimizeAutoloader(): bool
875    {
876        return $this->optimizeAutoloader;
877    }
878
879    /**
880     * @param bool $includeRootAutoload Include the project root autoload in the strauss autoloader.
881     */
882    public function setIncludeRootAutoload(bool $includeRootAutoload): void
883    {
884        $this->includeRootAutoload = $includeRootAutoload;
885    }
886
887    public function setOptimizeAutoloader(bool $optimizeAutoloader): void
888    {
889        $this->optimizeAutoloader = $optimizeAutoloader;
890    }
891
892    /**
893     * @param InputInterface $input To access the command line options.
894     */
895    public function updateFromCli(InputInterface $input): void
896    {
897
898        // strauss --updateCallSites=false (default)
899        // strauss --updateCallSites=true
900        // strauss --updateCallSites=src,input,extra
901
902        if ($input->hasOption('updateCallSites') && $input->getOption('updateCallSites') !== null) {
903            $updateCallSitesInput = $input->getOption('updateCallSites');
904
905            if ('false' === $updateCallSitesInput) {
906                $this->updateCallSites = array();
907            } elseif ('true' === $updateCallSitesInput) {
908                $this->updateCallSites = null;
909            } elseif (is_string($updateCallSitesInput)) {
910                $this->updateCallSites = explode(',', $updateCallSitesInput);
911            }
912        }
913
914        if ($input->hasOption('deleteVendorPackages')  && $input->getOption('deleteVendorPackages') !== false) {
915            $isDeleteVendorPackagesCommandLine = $input->getOption('deleteVendorPackages') === 'true'
916                || $input->getOption('deleteVendorPackages') === null;
917            $this->setDeleteVendorPackages($isDeleteVendorPackagesCommandLine);
918        } elseif ($input->hasOption('delete_vendor_packages') && $input->getOption('delete_vendor_packages') !== false) {
919            $isDeleteVendorPackagesCommandLine = $input->getOption('delete_vendor_packages') === 'true'
920                || $input->getOption('delete_vendor_packages') === null;
921            $this->setDeleteVendorPackages($isDeleteVendorPackagesCommandLine);
922        }
923
924        if ($input->hasOption('dry-run') && $input->getOption('dry-run') !== false) {
925            // If we're here, the parameter was passed in the CLI command.
926            $this->dryRun = empty($input->getOption('dry-run')) || (bool)filter_var($input->getOption('dry-run'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
927        }
928    }
929
930    /**
931     * Should we create the `autoload_aliases.php` file in `vendor/composer`?
932     *
933     * TODO:
934     * [x] YES when we are deleting vendor packages or files
935     * [ ] NO when we are running composer install `--no-dev`
936     * [ ] SOMETIMES: see https://github.com/BrianHenryIE/strauss/issues/144
937     * [ ] Add `aliases` to `extra` in `composer.json`
938     * [ ] Add `--aliases=true` CLI option
939     */
940    public function isCreateAliases(): bool
941    {
942        return $this->deleteVendorPackages || $this->deleteVendorFiles || trim($this->targetDirectory, '\\/') === 'vendor';
943    }
944
945    public function getProjectDirectory(): string
946    {
947        $projectDirectory = rtrim(FileSystem::normalizeDirSeparator($this->projectDirectory ?? getcwd()), '\\/');
948
949        return $this->isDryRun()
950            ? 'mem://' . ltrim($projectDirectory, '/')
951            : $projectDirectory;
952    }
953}