Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.42% covered (warning)
68.42%
143 / 209
46.77% covered (danger)
46.77%
29 / 62
CRAP
0.00% covered (danger)
0.00%
0 / 1
StraussConfig
68.42% covered (warning)
68.42%
143 / 209
46.77% covered (danger)
46.77%
29 / 62
708.93
0.00% covered (danger)
0.00%
0 / 1
 __construct
93.41% covered (success)
93.41%
85 / 91
0.00% covered (danger)
0.00%
0 / 1
35.35
 getAbsoluteTargetDirectory
100.00% covered (success)
100.00%
1 / 1
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
 setTargetVendorRelativePath
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
 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
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 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
 getProjectAbsolutePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setProjectAbsolutePath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
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 $projectAbsolutePath;
52
53    /**
54     * The output directory.
55     */
56    protected string $targetVendorRelativePath = '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     * Read any existing Mozart config.
213     * Overwrite it with any Strauss config.
214     * Provide sensible defaults.
215     *
216     * @param ?Composer $composer
217     *
218     * @throws Exception
219     */
220    public function __construct(?Composer $composer = null)
221    {
222        $normalizer = FileSystem::makePathNormalizer(getcwd());
223        if (isset($composer)) {
224            $composerDir = $composer->getConfig()->getConfigSource()->getName();
225            // Composer factory accepts a file or directory.
226            $composerDir               = str_ends_with($composerDir, '.json') // TODO: replace with a file exists/dir exists check.
227                ? dirname($composerDir) : $composerDir;
228            $this->projectAbsolutePath = $normalizer->normalizePath($composerDir);
229        } else {
230            $this->projectAbsolutePath = $normalizer->normalizePath(getcwd());
231        }
232
233        $configExtraSettings = null;
234
235        // Backwards compatibility with Mozart.
236        if (isset($composer, $composer->getPackage()->getExtra()['mozart'])) {
237            $configExtraSettings = (object)$composer->getPackage()->getExtra()['mozart'];
238
239            // Default setting for Mozart.
240            $this->setDeleteVendorFiles(true);
241        }
242
243        if (isset($composer, $composer->getPackage()->getExtra()['strauss'])) {
244            $configExtraSettings = (object)$composer->getPackage()->getExtra()['strauss'];
245        }
246
247        if (!is_null($configExtraSettings)) {
248            $mapper = (new JsonMapperFactory())->bestFit();
249
250            $rename = new Rename();
251            $rename->addMapping(StraussConfig::class, 'target_directory', 'targetVendorRelativePath');
252            $rename->addMapping(StraussConfig::class, 'dep_directory', 'targetVendorRelativePath');
253            $rename->addMapping(StraussConfig::class, 'dep_namespace', 'namespacePrefix');
254
255            $rename->addMapping(StraussConfig::class, 'exclude_packages', 'excludePackages');
256            $rename->addMapping(StraussConfig::class, 'delete_vendor_files', 'deleteVendorFiles');
257            $rename->addMapping(StraussConfig::class, 'delete_vendor_packages', 'deleteVendorPackages');
258
259            $rename->addMapping(StraussConfig::class, 'exclude_prefix_packages', 'excludePackagesFromPrefixing');
260
261            $rename->addMapping(StraussConfig::class, 'include_root_autoload', 'includeRootAutoload');
262
263            $rename->addMapping(StraussConfig::class, 'function_prefix', 'functionsPrefix');
264
265            $rename->addMapping(StraussConfig::class, 'constant_prefix', 'constantsPrefix');
266
267            $mapper->unshift($rename);
268            $mapper->push(new CaseConversion(TextNotation::UNDERSCORE(), TextNotation::CAMEL_CASE()));
269
270            $mapper->mapObject($configExtraSettings, $this);
271        }
272
273        // Defaults.
274        // * Use PSR-4 autoloader key
275        // * Use PSR-0 autoloader key
276        // * Use the package name
277        if (! isset($this->namespacePrefix)) {
278            if (isset($composer, $composer->getPackage()->getAutoload()['psr-4']) && !empty($composer->getPackage()->getAutoload()['psr-4'])) {
279                $this->setNamespacePrefix(array_key_first($composer->getPackage()->getAutoload()['psr-4']));
280            } elseif (isset($composer, $composer->getPackage()->getAutoload()['psr-0']) && !empty($composer->getPackage()->getAutoload()['psr-0'])) {
281                $this->setNamespacePrefix(array_key_first($composer->getPackage()->getAutoload()['psr-0']));
282            } elseif (isset($composer) && '__root__' !== $composer->getPackage()->getName()) {
283                $packageName = $composer->getPackage()->getName();
284                // Replace all non-word characters with underscores.
285                $namespacePrefix = preg_replace('/[^\w\/]+/', '_', $packageName) ?? $packageName;
286                $namespacePrefix = str_replace('/', '\\', $namespacePrefix) . '\\';
287                $namespacePrefix = preg_replace_callback('/(?<=^|_|\\\\)[a-z]/', function ($match) {
288                    return strtoupper($match[0]);
289                }, $namespacePrefix) ?? $namespacePrefix;
290                $this->setNamespacePrefix($namespacePrefix);
291            } elseif (isset($this->classmapPrefix) && !empty($this->getClassmapPrefix())) {
292                $namespacePrefix = rtrim($this->getClassmapPrefix(), '_');
293                $this->setNamespacePrefix($namespacePrefix);
294            }
295        }
296
297        if (! isset($this->classmapPrefix)) {
298            if (isset($composer, $composer->getPackage()->getAutoload()['psr-4'])) {
299                $autoloadKey = array_key_first($composer->getPackage()->getAutoload()['psr-4']);
300                $classmapPrefix = str_replace("\\", "_", $autoloadKey);
301                $this->setClassmapPrefix($classmapPrefix);
302            } elseif (isset($composer, $composer->getPackage()->getAutoload()['psr-0'])) {
303                $autoloadKey = array_key_first($composer->getPackage()->getAutoload()['psr-0']);
304                $classmapPrefix = str_replace("\\", "_", $autoloadKey);
305                $this->setClassmapPrefix($classmapPrefix);
306            } elseif (isset($composer) && '__root__' !== $composer->getPackage()->getName()) {
307                $packageName = $composer->getPackage()->getName();
308                $classmapPrefix = preg_replace('/[^\w\/]+/', '_', $packageName);
309                $classmapPrefix = str_replace('/', '\\', $classmapPrefix);
310                // Uppercase the first letter of each word.
311                $classmapPrefix = preg_replace_callback('/(?<=^|_|\\\\)[a-z]/', function ($match) {
312                    return strtoupper($match[0]);
313                }, $classmapPrefix);
314                $classmapPrefix = str_replace("\\", "_", $classmapPrefix);
315                $this->setClassmapPrefix($classmapPrefix);
316            } elseif (isset($this->namespacePrefix)) {
317                $classmapPrefix = preg_replace('/[^\w\/]+/', '_', $this->getNamespacePrefix()) ?? str_replace('\\', '_', $this->getNamespacePrefix());
318                $classmapPrefix = rtrim($classmapPrefix, '_') . '_';
319                $this->setClassmapPrefix($classmapPrefix);
320            }
321        }
322
323//        if (!isset($this->namespacePrefix) || !isset($this->classmapPrefix)) {
324//            throw new Exception('Prefix not set. Please set `namespace_prefix`, `classmap_prefix` in composer.json/extra/strauss.');
325//        }
326
327        if (isset($composer) && empty($this->packages)) {
328            $this->packages = array_map(function (\Composer\Package\Link $element) {
329                return $element->getTarget();
330            }, $composer->getPackage()->getRequires());
331        }
332
333        // @deprecated.
334        // If the bool flag for classmapOutput wasn't set in the JSON config.
335        if (!isset($this->classmapOutput)) {
336            $this->classmapOutput = true;
337            // Check each autoloader.
338            if (isset($composer)) {
339                $autoloadKey = $composer->getPackage()->getAutoload();
340                if (isset($autoloadKey['classmap']) && in_array($this->targetVendorRelativePath, $autoloadKey['classmap'], true)) {
341                    $this->classmapOutput = false;
342                }
343                foreach ($composer->getPackage()->getAutoload() as $autoload) {
344                    // To see if one of its paths.
345                    foreach ($autoload as $entry) {
346                        $paths = (array) $entry;
347                        foreach ($paths as $path) {
348                            // Matches the target directory.
349                            if (trim($path, '\\/') === $this->getAbsoluteTargetDirectory()) {
350                                $this->classmapOutput = false;
351                                break 3;
352                            }
353                        }
354                    }
355                }
356            }
357        }
358
359        // TODO: Throw an exception if any regex patterns in config are invalid.
360        // https://stackoverflow.com/questions/4440626/how-can-i-validate-regex
361        // preg_match('~Valid(Regular)Expression~', null) === false);
362
363        if (isset($configExtraSettings, $configExtraSettings->updateCallSites)) {
364            if (true === $configExtraSettings->updateCallSites) {
365                $this->updateCallSites = null;
366            } elseif (false === $configExtraSettings->updateCallSites) {
367                $this->updateCallSites = array();
368            } elseif (is_array($configExtraSettings->updateCallSites)) {
369                $this->updateCallSites = $configExtraSettings->updateCallSites;
370            } else {
371                // uh oh.
372            }
373        }
374    }
375
376    /**
377     * `target_directory` will always be returned without a leading nor trailing slash.
378     */
379    public function getAbsoluteTargetDirectory(): string
380    {
381        return $this->getProjectAbsolutePath() . '/' . $this->targetVendorRelativePath;
382    }
383
384    public function isTargetDirectoryVendor(): bool
385    {
386        return $this->getAbsoluteVendorDirectory() === $this->getAbsoluteTargetDirectory();
387    }
388
389    /**
390     * Default 'vendor-prefixed'. No leading or trailing slash.
391     */
392    public function getRelativeTargetDirectory(): string
393    {
394        return FileSystem::normalizeDirSeparator(
395            trim($this->targetVendorRelativePath, '\\/')
396        );
397    }
398
399    /**
400     * @param string $targetVendorRelativePath
401     */
402    public function setTargetVendorRelativePath(string $targetVendorRelativePath): void
403    {
404        $this->targetVendorRelativePath = $targetVendorRelativePath;
405    }
406
407    /**
408     * No leading or trailing slash.
409     */
410    public function getAbsoluteVendorDirectory(): string
411    {
412        return $this->getProjectAbsolutePath() . '/' . $this->relativeVendorDirectory;
413    }
414
415    /**
416     * @param string $relativeVendorDirectory
417     */
418    public function setRelativeVendorDirectory(string $relativeVendorDirectory): void
419    {
420        $this->relativeVendorDirectory = $relativeVendorDirectory;
421    }
422
423    /**
424     * With no trailing slash and no leading slash.
425     */
426    public function getNamespacePrefix(): ?string
427    {
428        return !isset($this->namespacePrefix) ? null : trim($this->namespacePrefix, '\\');
429    }
430
431    /**
432     * @param string $namespacePrefix
433     */
434    public function setNamespacePrefix(string $namespacePrefix): void
435    {
436        $this->namespacePrefix = $namespacePrefix;
437    }
438
439    /**
440     * @return string
441     */
442    public function getClassmapPrefix(): ?string
443    {
444        return $this->classmapPrefix;
445    }
446
447    /**
448     * @param string $classmapPrefix
449     */
450    public function setClassmapPrefix(string $classmapPrefix): void
451    {
452        $this->classmapPrefix = $classmapPrefix;
453    }
454
455    public function getFunctionsPrefix(): ?string
456    {
457        if (!isset($this->functionsPrefix) && !is_null($this->getClassmapPrefix())) {
458            return strtolower($this->getClassmapPrefix());
459        }
460        if (empty($this->functionsPrefix)) {
461            return null;
462        }
463        if (is_string($this->functionsPrefix)) {
464            return $this->functionsPrefix;
465        }
466        return null;
467    }
468
469    /**
470     * @param string|bool|null $functionsPrefix
471     */
472    public function setFunctionsPrefix($functionsPrefix): void
473    {
474        $this->functionsPrefix = $functionsPrefix;
475    }
476
477    /**
478     * @return string
479     */
480    public function getConstantsPrefix(): ?string
481    {
482        return $this->constantsPrefix;
483    }
484
485    /**
486     * @param string $constantsPrefix
487     */
488    public function setConstantsPrefix(string $constantsPrefix): void
489    {
490        $this->constantsPrefix = $constantsPrefix;
491    }
492
493    /**
494     * List of files and directories to update call sites in. Empty to disable. Null infers from the project's autoload key.
495     *
496     * @return string[]|null
497     */
498    public function getUpdateCallSites(): ?array
499    {
500        return $this->updateCallSites;
501    }
502
503    /**
504     * @param string[]|array{0:bool}|null $updateCallSites
505     * @throws InvalidArgumentException
506     */
507    public function setUpdateCallSites($updateCallSites): void
508    {
509        if (is_array($updateCallSites) && count($updateCallSites) === 1 && $updateCallSites[0] === true) {
510            // Setting `null` instructs Strauss to update call sites in the project's autoload key.
511            $this->updateCallSites = null;
512            return;
513        } elseif (is_array($updateCallSites) && count($updateCallSites) === 1 && $updateCallSites[0] === false) {
514            $this->updateCallSites = array();
515            return;
516        } elseif (is_array($updateCallSites) && isset($updateCallSites[0]) && !is_bool($updateCallSites[0])) {
517            $this->updateCallSites = array_filter(
518                $updateCallSites,
519                'is_string'
520            );
521            return;
522        }
523        throw new InvalidArgumentException('Unexpected value for updateCallSites');
524    }
525
526    /**
527     * @param array{packages?:array<string>, namespaces?:array<string>, file_patterns?:array<string>} $excludeFromCopy
528     */
529    public function setExcludeFromCopy(array $excludeFromCopy): void
530    {
531        foreach (array( 'packages', 'namespaces', 'file_patterns' ) as $key) {
532            if (isset($excludeFromCopy[$key])) {
533                $this->excludeFromCopy[$key] = $excludeFromCopy[$key];
534            }
535        }
536    }
537
538    /**
539     * @return string[]
540     */
541    public function getExcludePackagesFromCopy(): array
542    {
543        return $this->excludeFromCopy['packages'] ?? array();
544    }
545
546    /**
547     * @return string[]
548     */
549    public function getExcludeNamespacesFromCopy(): array
550    {
551        return $this->excludeFromCopy['namespaces'] ?? array();
552    }
553
554    /**
555     * @return string[]
556     */
557    public function getExcludeFilePatternsFromCopy(): array
558    {
559        return $this->excludeFromCopy['file_patterns'] ?? array();
560    }
561
562    /**
563     * @param array{packages?:array<string>, namespaces?:array<string>, file_patterns?:array<string>} $excludeFromPrefix
564     */
565    public function setExcludeFromPrefix(array $excludeFromPrefix): void
566    {
567        if (isset($excludeFromPrefix['packages'])) {
568            $this->excludeFromPrefix['packages'] = $excludeFromPrefix['packages'];
569        }
570        if (isset($excludeFromPrefix['namespaces'])) {
571            $this->excludeFromPrefix['namespaces'] = $excludeFromPrefix['namespaces'];
572        }
573        if (isset($excludeFromPrefix['file_patterns'])) {
574            $this->excludeFromPrefix['file_patterns'] = $excludeFromPrefix['file_patterns'];
575        }
576    }
577
578    /**
579     * When prefixing, do not prefix these packages (which have been copied).
580     *
581     * @return string[]
582     */
583    public function getExcludePackagesFromPrefixing(): array
584    {
585        return $this->excludeFromPrefix['packages'] ?? [];
586    }
587
588    /**
589     * @param string[] $excludePackagesFromPrefixing
590     */
591    public function setExcludePackagesFromPrefixing(array $excludePackagesFromPrefixing): void
592    {
593        $this->excludeFromPrefix['packages'] = $excludePackagesFromPrefixing;
594    }
595
596    /**
597     * @return string[]
598     */
599    public function getExcludeNamespacesFromPrefixing(): array
600    {
601        return array_map(
602            fn(string $packageName) => trim($packageName, '\\/'),
603            $this->excludeFromPrefix['namespaces'] ?? []
604        );
605    }
606
607    /**
608     * @return string[]
609     */
610    public function getExcludeFilePatternsFromPrefixing(): array
611    {
612        return $this->excludeFromPrefix['file_patterns'] ?? array();
613    }
614
615    /**
616     * @param array{packages?:array<string>, namespaces?:array<string>, file_patterns?:array<string>, constants?:array<string>} $excludeConstants
617     */
618    public function setExcludeConstants(array $excludeConstants): void
619    {
620        if (isset($excludeConstants['packages'])) {
621            $this->excludeConstants['packages'] = $excludeConstants['packages'];
622        }
623        if (isset($excludeConstants['namespaces'])) {
624            $this->excludeConstants['namespaces'] = $excludeConstants['namespaces'];
625        }
626        if (isset($excludeConstants['file_patterns'])) {
627            $this->excludeConstants['file_patterns'] = $excludeConstants['file_patterns'];
628        }
629        if (isset($excludeConstants['constants'])) {
630            $this->excludeConstants['constants'] = $excludeConstants['constants'];
631        }
632    }
633
634    /**
635     * @return string[]
636     */
637    public function getExcludePackagesFromConstantPrefixing(): array
638    {
639        return $this->excludeConstants['packages'] ?? [];
640    }
641
642    /**
643     * @return string[]
644     */
645    public function getExcludeNamespacesFromConstantPrefixing(): array
646    {
647        return array_map(
648            fn(string $ns) => trim($ns, '\\/'),
649            $this->excludeConstants['namespaces'] ?? []
650        );
651    }
652
653    /**
654     * @return string[]
655     */
656    public function getExcludeFilePatternsFromConstantPrefixing(): array
657    {
658        return $this->excludeConstants['file_patterns'] ?? [];
659    }
660
661    /**
662     * @return string[]
663     */
664    public function getExcludeConstantNames(): array
665    {
666        return $this->excludeConstants['constants'] ?? [];
667    }
668
669    /**
670     * @return array{}|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 getOverrideAutoload(): array
673    {
674        return $this->overrideAutoload;
675    }
676
677    /**
678     * @param array<string, array{files?:array<string>,classmap?:array<string>,"psr-4":array<string|array<string>>}> $overrideAutoload Dictionary of package name: autoload rules.
679     */
680    public function setOverrideAutoload(array $overrideAutoload): void
681    {
682        $this->overrideAutoload = $overrideAutoload;
683    }
684
685    /**
686     * @return bool
687     */
688    public function isDeleteVendorFiles(): bool
689    {
690        return $this->deleteVendorFiles;
691    }
692
693    /**
694     * @return bool
695     */
696    public function isDeleteVendorPackages(): bool
697    {
698        return $this->deleteVendorPackages;
699    }
700
701    /**
702     * @param bool $deleteVendorFiles
703     */
704    public function setDeleteVendorFiles(bool $deleteVendorFiles): void
705    {
706        $this->deleteVendorFiles = $deleteVendorFiles;
707    }
708
709    /**
710     * @param bool $deleteVendorPackages
711     */
712    public function setDeleteVendorPackages(bool $deleteVendorPackages): void
713    {
714        $this->deleteVendorPackages = $deleteVendorPackages;
715    }
716
717    /**
718     * @return string[]
719     */
720    public function getPackages(): array
721    {
722        return $this->packages;
723    }
724
725    /**
726     * @param string[] $packages
727     */
728    public function setPackages(array $packages): void
729    {
730        $this->packages = $packages;
731    }
732
733    /**
734     * @used-by DumpAutoload::createInstalledVersionsFiles()
735     * @return array<string,ComposerPackage>
736     */
737    public function getPackagesToCopy(): array
738    {
739        return $this->packagesToCopy;
740    }
741
742    /**
743     * @used-by DependenciesCommand::buildDependencyList()
744     *
745     * @param array<string,ComposerPackage> $packagesToCopy
746     */
747    public function setPackagesToCopy(array $packagesToCopy): void
748    {
749        $this->packagesToCopy = $packagesToCopy;
750    }
751
752    /**
753     * @return array<string,ComposerPackage>
754     */
755    public function getPackagesToPrefix(): array
756    {
757        return $this->packagesToPrefix;
758    }
759
760    /**
761     * @param array<string,ComposerPackage> $packagesToPrefix
762     */
763    public function setPackagesToPrefix(array $packagesToPrefix): void
764    {
765        $this->packagesToPrefix = $packagesToPrefix;
766    }
767    /**
768     * TODO: Can we name this `isClassmapOutputEnabled`?
769     */
770    public function isClassmapOutput(): bool
771    {
772        return $this->classmapOutput;
773    }
774
775    /**
776     * @param bool $classmapOutput
777     */
778    public function setClassmapOutput(bool $classmapOutput): void
779    {
780        $this->classmapOutput = $classmapOutput;
781    }
782
783    /**
784     * Backwards compatibility with Mozart.
785     *
786     * @param string[] $excludePackages
787     */
788    public function setExcludePackages(array $excludePackages): void
789    {
790        $this->excludeFromPrefix['packages'] = $excludePackages;
791    }
792
793    /**
794     * @return array<string,string>
795     */
796    public function getNamespaceReplacementPatterns(): array
797    {
798        return $this->namespaceReplacementPatterns;
799    }
800
801    /**
802     * @param array<string,string> $namespaceReplacementPatterns
803     */
804    public function setNamespaceReplacementPatterns(array $namespaceReplacementPatterns): void
805    {
806        $this->namespaceReplacementPatterns = $namespaceReplacementPatterns;
807    }
808
809    /**
810     * @return bool
811     */
812    public function isIncludeModifiedDate(): bool
813    {
814        return $this->includeModifiedDate;
815    }
816
817    /**
818     * @param bool $includeModifiedDate
819     */
820    public function setIncludeModifiedDate(bool $includeModifiedDate): void
821    {
822        $this->includeModifiedDate = $includeModifiedDate;
823    }
824
825
826    /**
827     * @return bool
828     */
829    public function isIncludeAuthor(): bool
830    {
831        return $this->includeAuthor;
832    }
833
834    /**
835     * @param bool $includeAuthor
836     */
837    public function setIncludeAuthor(bool $includeAuthor): void
838    {
839        $this->includeAuthor = $includeAuthor;
840    }
841
842    /**
843     * Should expected changes be printed to console rather than files modified?
844     */
845    public function isDryRun(): bool
846    {
847        return $this->dryRun;
848    }
849
850    /**
851     * Disable making changes to files; output changes to console instead.
852     */
853    public function setDryRun(bool $dryRun): void
854    {
855        $this->dryRun = $dryRun;
856    }
857
858    /**
859     * Should the root autoload be included when generating the strauss autoloader?
860     */
861    public function isIncludeRootAutoload(): bool
862    {
863        return $this->includeRootAutoload;
864    }
865
866    public function isOptimizeAutoloader(): bool
867    {
868        return $this->optimizeAutoloader;
869    }
870
871    /**
872     * @param bool $includeRootAutoload Include the project root autoload in the strauss autoloader.
873     */
874    public function setIncludeRootAutoload(bool $includeRootAutoload): void
875    {
876        $this->includeRootAutoload = $includeRootAutoload;
877    }
878
879    public function setOptimizeAutoloader(bool $optimizeAutoloader): void
880    {
881        $this->optimizeAutoloader = $optimizeAutoloader;
882    }
883
884    /**
885     * @param InputInterface $input To access the command line options.
886     */
887    public function updateFromCli(InputInterface $input): void
888    {
889
890        // strauss --updateCallSites=false (default)
891        // strauss --updateCallSites=true
892        // strauss --updateCallSites=src,input,extra
893
894        if ($input->hasOption('updateCallSites') && $input->getOption('updateCallSites') !== null) {
895            $updateCallSitesInput = $input->getOption('updateCallSites');
896
897            if ('false' === $updateCallSitesInput) {
898                $this->updateCallSites = array();
899            } elseif ('true' === $updateCallSitesInput) {
900                $this->updateCallSites = null;
901            } elseif (is_string($updateCallSitesInput)) {
902                $this->updateCallSites = explode(',', $updateCallSitesInput);
903            }
904        }
905
906        if ($input->hasOption('deleteVendorPackages')  && $input->getOption('deleteVendorPackages') !== false) {
907            $isDeleteVendorPackagesCommandLine = $input->getOption('deleteVendorPackages') === 'true'
908                || $input->getOption('deleteVendorPackages') === null;
909            $this->setDeleteVendorPackages($isDeleteVendorPackagesCommandLine);
910        } elseif ($input->hasOption('delete_vendor_packages') && $input->getOption('delete_vendor_packages') !== false) {
911            $isDeleteVendorPackagesCommandLine = $input->getOption('delete_vendor_packages') === 'true'
912                || $input->getOption('delete_vendor_packages') === null;
913            $this->setDeleteVendorPackages($isDeleteVendorPackagesCommandLine);
914        }
915
916        if ($input->hasOption('dry-run') && $input->getOption('dry-run') !== false) {
917            // If we're here, the parameter was passed in the CLI command.
918            $this->dryRun = empty($input->getOption('dry-run')) || (bool)filter_var($input->getOption('dry-run'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
919        }
920    }
921
922    /**
923     * Should we create the `autoload_aliases.php` file in `vendor/composer`?
924     *
925     * TODO:
926     * [x] YES when we are deleting vendor packages or files
927     * [ ] NO when we are running composer install `--no-dev`
928     * [ ] SOMETIMES: see https://github.com/BrianHenryIE/strauss/issues/144
929     * [ ] Add `aliases` to `extra` in `composer.json`
930     * [ ] Add `--aliases=true` CLI option
931     */
932    public function isCreateAliases(): bool
933    {
934        return $this->deleteVendorPackages || $this->deleteVendorFiles || trim($this->targetVendorRelativePath, '\\/') === 'vendor';
935    }
936
937    public function getProjectAbsolutePath(): string
938    {
939        return $this->projectAbsolutePath;
940    }
941
942    /**
943     * @param string $projectAbsolutePath Should be normalized path.
944     */
945    public function setProjectAbsolutePath(string $projectAbsolutePath): void
946    {
947        $this->projectAbsolutePath = $projectAbsolutePath;
948    }
949}