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