├── .phive
└── phars.xml
├── .typos.toml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bin
└── strauss
├── bootstrap.php
├── composer.json
├── phive.phar.asc
└── src
├── Composer
├── ComposerPackage.php
├── Extra
│ ├── ReplaceConfigInterface.php
│ └── StraussConfig.php
└── ProjectComposerPackage.php
├── Config
├── AliasesConfigInterface.php
├── AutoloadConfigInterface.php
├── ChangeEnumeratorConfigInterface.php
├── CleanupConfigInterface.php
├── FileCopyScannerConfigInterface.php
├── FileEnumeratorConfig.php
├── FileSymbolScannerConfigInterface.php
└── PrefixerConfigInterface.php
├── Console
├── Application.php
└── Commands
│ ├── DependenciesCommand.php
│ ├── IncludeAutoloaderCommand.php
│ └── ReplaceCommand.php
├── Files
├── DiscoveredFiles.php
├── File.php
├── FileBase.php
├── FileWithDependency.php
└── HasDependency.php
├── Helpers
├── FileSystem.php
├── FlysystemBackCompatInterface.php
├── FlysystemBackCompatTrait.php
├── InMemoryFilesystemAdapter.php
├── NamespaceSort.php
└── ReadOnlyFileSystem.php
├── Pipeline
├── Aliases.php
├── Autoload.php
├── Autoload
│ ├── ComposerAutoloadGenerator.php
│ ├── DumpAutoload.php
│ └── VendorComposerAutoload.php
├── ChangeEnumerator.php
├── Cleanup.php
├── Cleanup
│ └── InstalledJson.php
├── Copier.php
├── DependenciesEnumerator.php
├── FileCopyScanner.php
├── FileEnumerator.php
├── FileSymbol
│ └── builtinsymbols.php
├── FileSymbolScanner.php
├── Licenser.php
└── Prefixer.php
└── Types
├── ClassSymbol.php
├── ConstantSymbol.php
├── DiscoveredSymbol.php
├── DiscoveredSymbols.php
├── FunctionSymbol.php
└── NamespaceSymbol.php
/.phive/phars.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.typos.toml:
--------------------------------------------------------------------------------
1 | [files]
2 | extend-exclude = [
3 | ".git/",
4 | "tests/Issues/data/",
5 | ]
6 | ignore-hidden = false
7 |
8 | [default]
9 | extend-ignore-re = [
10 | "ComposerAutoloaderInit[0-9a-f]+",
11 | ]
12 |
13 | [default.extend-identifiers]
14 | # Typos
15 | "Github" = "GitHub"
16 | "Wordpress" = "WordPress"
17 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## 0.22.2 April 2025
4 |
5 | * Fix: `psr-0` autoloaders were no longer autoloaded because the directory structure did not match
6 | * Fix: `files` autoloaders failed when not unique (the whole point of this tool)
7 | * Fix: spelling
8 |
9 | ## 0.22.1 April 2025
10 |
11 | * Fix: jsonmapper latest version caused problems with PhpDoc
12 |
13 | ## 0.22.0 April 2025
14 |
15 | * Add: `--info`, `--debug` and `--silent` verbosity levels
16 | * Add: `--dry-run` which runs with `--debug` output but does not write files
17 | * Add: `autoload_aliases.php` file for dev dependencies to load modified classes using their original fqdn
18 | * Fix: relative namespaces
19 | * Fix: allow vendor and target directories to be in parent directory of `composer.json`
20 | * Fix: incorrectly updating call sites
21 | * Dev: major refactor to use `thephpleague/Flysystem` and `elazar/flystream` for file operations
22 | * Dev: print diff code coverage report on PRs
23 | * Dev: skip / speed-up some tests
24 | * Dev: improvements to tests' names and coverage reporting specificity
25 | * Docs: improve installation instructions in `README.md`
26 | * CI: Set up problem matcher for PHPUnit
27 |
28 | ## 0.21.1 January 2025
29 |
30 | * Fix: global functions prefixed too liberally when defined as strings
31 | * Add: include changelog in phar
32 |
33 | ## 0.21.0 January 2025
34 |
35 | * Add: prefix global functions
36 |
37 | ## 0.20.1 December 2024
38 |
39 | * Fix: `vendor-prefixed` subdirectories' permissions being copied as 0700 instead of 0755
40 |
41 | ## 0.20.0 November 2024
42 |
43 | * Fix: `Generic<\namespaced\class-type>` not prefixed
44 | * Add `strauss replace` command (e.g. if you fork a project and want to change its namespace)
45 |
46 | ## 0.19.5 October 2024
47 |
48 | * Fix: `use GlobalClass as Alias;` not prefixed
49 | * Add: `.gitattributes` file to exclude dev files from distribution
50 | * CI: Fail releases if `bin/strauss` version number is out of sync
51 | * Tests: Add first tests for `DiscoveredFiles.php`
52 | * Improve `README.md`
53 | * Fix: typos in code
54 |
55 | ## 0.19.4 October 2024
56 |
57 | * Fix: out of sync version number in `bin/strauss`
58 |
59 | ## 0.19.3 October 2024
60 |
61 | * Fix: handle `@` symbol for error suppression
62 | * Fix: handle `preg_replace...` returning `null` in `Licenser`
63 | * Fix: only search for symbols in PHP files
64 |
65 | ## 0.19.2 June 2024
66 |
67 | * Fix: available CLI arguments were overwriting extra.strauss config
68 | * Fix: updating `league/flysystem` changed the default directory permissions
69 |
70 | ## 0.19.1 April 2024
71 |
72 | * Fix: was incorrectly deleting autoload keys from installed.json
73 |
74 | ## 0.19.0 April 2024
75 |
76 | * Fix: check for array before loop
77 | * Fix: filepaths on Windows (still work to do for Windows)
78 | * Update: tidy `bin/strauss`
79 | * Run tests with project classes + with built phar
80 | * Allow `symfony/console` & `symfony/finder` `^7` for Laravel 11 compatibility
81 | * Add: `scripts/createphar.sh`
82 | * Lint: most PhpStan level 7
83 |
84 | ## 0.18.0 April 2024
85 |
86 | * Add: GitHub Action to update bin version number from CHANGELOG.md
87 | * Fix: casting a namespaced class to a string
88 | * Fix: composer dump-autoload error after delete-vendor-files/delete-vendor-packages
89 | * Fix: add missing built-in PHP interfaces to exclude rules
90 | * Fix: Undefined offset when seeing namespace
91 | * Refactoring for clarity and pending issues
92 |
93 | ## 0.14.0 07-March-2023
94 |
95 | * Merge `in-situ` branch (bugs expected)
96 | * Add: `delete_vendor_packages` option (`delete_vendor_files` is maybe deprecated now)
97 | * Add: GPG .phar signing for Phive
98 | * Breaking: Stop excluding `psr/*` from `file_patterns` prefixing
99 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Coen Jacobs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://brianhenryie.github.io/strauss/) [](https://phpstan.org/)
2 |
3 | # Strauss – PHP Namespace Renamer
4 |
5 | A tool to prefix namespaces, classnames, and constants in PHP files to avoid autoloading collisions.
6 |
7 | A fork of [Mozart](https://github.com/coenjacobs/mozart/) for [Composer](https://getcomposer.org/) for PHP.
8 |
9 | Have you ever activated a WordPress plugin that has a conflict with another because the plugins use two different versions of the same PHP library? **Strauss is the solution to that problem** - it ensures that _your_ plugin's PHP dependencies are isolated and loaded from your plugin rather than loading from whichever plugin's autoloader registers & runs first.
10 |
11 | > ⚠️ **Sponsorship**: I don't want your money. [Please write a unit test to help the project](https://brianhenryie.github.io/strauss/).
12 |
13 | ## Table of Contents
14 |
15 | * [Installation](#installation)
16 | * [As a `.phar` file](#as-a-phar-file-recommended) (recommended)
17 | * [As a dev dependency via composer](#as-a-dev-dependency-via-composer-not-recommended) (not recommended)
18 | * [Edit `composer.json` scripts](#edit-composerjson-scripts)
19 | * [Usage](#usage)
20 | * [Configuration](#configuration)
21 | * [Autoloading](#autoloading)
22 | * [Motivation & Comparison to Mozart](#motivation--comparison-to-mozart)
23 | * [Alternatives](#alternatives)
24 | * [Breaking Changes](#breaking-changes)
25 | * [Acknowledgements](#acknowledgements)
26 |
27 | ## Installation
28 |
29 | ### As a `.phar` file (recommended)
30 |
31 | There are a couple of small steps to make this possible.
32 |
33 | #### Create a `bin/.gitkeep` file
34 |
35 | This ensures that there is a `bin/` directory in the root of your project. This is where the `.phar` file will go.
36 |
37 | ```bash
38 | mkdir bin
39 | touch bin/.gitkeep
40 | ```
41 |
42 | #### `.gitignore` the `.phar` file
43 |
44 | Add the following to your `.gitignore`:
45 |
46 | ```bash
47 | bin/strauss.phar
48 | ```
49 |
50 | #### Edit `composer.json` `scripts
51 |
52 | In your `composer.json`, add `strauss` to the `scripts` section:
53 |
54 | ```json
55 | "scripts": {
56 | "prefix-namespaces": [
57 | "sh -c 'test -f ./bin/strauss.phar || curl -o bin/strauss.phar -L -C - https://github.com/BrianHenryIE/strauss/releases/latest/download/strauss.phar'",
58 | "@php bin/strauss.phar",
59 | "@composer dump-autoload"
60 | ],
61 | "post-install-cmd": [
62 | "@prefix-namespaces"
63 | ],
64 | "post-update-cmd": [
65 | "@prefix-namespaces"
66 | ],
67 | "post-autoload-dump": [
68 | "@php bin/strauss.phar include-autoloader"
69 | ]
70 | }
71 | ```
72 |
73 | This provides `composer strauss`, which does the following:
74 |
75 | 1. The `sh -c` command tests if `bin/strauss.phar` exists, and if not, downloads it from [releases](https://github.com/BrianHenryIE/strauss/releases).
76 | 2. Then `@php bin/strauss.phar` is run to prefix the namespaces.
77 | 3. Ensure that composer's autoload map is updated.
78 |
79 | ### As a dev dependency via composer (not recommended)
80 |
81 | If you prefer to include Strauss as a dev dependency, you can still do so. You mileage may vary when you include it this way.
82 |
83 | ```
84 | composer require --dev brianhenryie/strauss
85 | ```
86 |
87 | #### Edit `composer.json` `scripts
88 |
89 | ```json
90 | "scripts": {
91 | "prefix-namespaces": [
92 | "strauss",
93 | "@php composer dump-autoload"
94 | ],
95 | "post-install-cmd": [
96 | "@prefix-namespaces"
97 | ],
98 | "post-update-cmd": [
99 | "@prefix-namespaces"
100 | ],
101 | "post-autoload-dump": [
102 | "strauss include-autoloader"
103 | ]
104 | }
105 | ```
106 |
107 | ## Usage
108 |
109 | If you add Strauss to your `composer.json` as indicated in [Installation](#installation), it will run when you `composer install` or `composer update`. To run Strauss directly, simply use:
110 |
111 | ```bash
112 | composer prefix-namespaces
113 | ```
114 |
115 | To update the files that call the prefixed classes, you can use `--updateCallSites=true` which uses your autoload key, or `--updateCallSites=includes,templates` to explicitly specify the files and directories.
116 |
117 | ```bash
118 | composer -- prefix-namespaces --updateCallSites=true
119 | ```
120 |
121 | or
122 |
123 | ```bash
124 | composer -- prefix-namespaces --updateCallSites=includes,templates
125 | ```
126 |
127 | To try it out without making changes, you can use the `--dry-run` flag:
128 |
129 |
130 |
131 | strauss --dry-run
132 |
133 | 
134 |
135 |
136 |
137 | Verbosity can be controlled with `--notice` (default), `--info`, `--debug` and `--silent`.
138 |
139 | ## Configuration
140 |
141 | Strauss potentially requires zero configuration, but likely you'll want to customize a little, by adding in your `composer.json` an `extra/strauss` object. The following is the default config, where the `namespace_prefix` and `classmap_prefix` are determined from your `composer.json`'s `autoload` or `name` key and `packages` is determined from the `require` key:
142 |
143 | ```json
144 | "extra": {
145 | "strauss": {
146 | "target_directory": "vendor-prefixed",
147 | "namespace_prefix": "BrianHenryIE\\My_Project\\",
148 | "classmap_prefix": "BrianHenryIE_My_Project_",
149 | "constant_prefix": "BHMP_",
150 | "packages": [
151 | ],
152 | "update_call_sites": false,
153 | "override_autoload": {
154 | },
155 | "exclude_from_copy": {
156 | "packages": [
157 | ],
158 | "namespaces": [
159 | ],
160 | "file_patterns": [
161 | ]
162 | },
163 | "exclude_from_prefix": {
164 | "packages": [
165 | ],
166 | "namespaces": [
167 | ],
168 | "file_patterns": [
169 | ]
170 | },
171 | "namespace_replacement_patterns" : {
172 | },
173 | "delete_vendor_packages": false,
174 | "delete_vendor_files": false
175 | }
176 | },
177 | ```
178 |
179 | The following configuration is inferred:
180 |
181 | - `target_directory` defines the directory the files will be copied to, default `vendor-prefixed`
182 | - `namespace_prefix` defines the default string to prefix each namespace with
183 | - `classmap_prefix` defines the default string to prefix class names in the global namespace
184 | - `packages` is the list of packages to process. If absent, all packages in the `require` key of your `composer.json` are included
185 | - `classmap_output` is a `bool` to decide if Strauss will create `autoload-classmap.php` and `autoload.php`. If it is not set, it is `false` if `target_directory` is in your project's `autoload` key, `true` otherwise.
186 |
187 | The following configuration is default:
188 |
189 | - `delete_vendor_packages`: `false` a boolean flag to indicate if the packages' vendor directories should be deleted after being processed. It defaults to false, so any destructive change is opt-in.
190 | - `delete_vendor_files`: `false` a boolean flag to indicate if files copied from the packages' vendor directories should be deleted after being processed. It defaults to false, so any destructive change is opt-in. This is maybe deprecated! Is there any use to this that is more appropriate than `delete_vendor_packages`?
191 | - `include_modified_date` is a `bool` to decide if Strauss should include a date in the (phpdoc) header written to modified files. Defaults to `true`.
192 | - `include_author` is a `bool` to decide if Strauss should include the author name in the (phpdoc) header written to modified files. Defaults to `true`.
193 | - `update_call_sites`: `false`. This can be `true`, `false` or an `array` of directories/filepaths. When set to `true` it defaults to the directories and files in the project's `autoload` key. The PHP files and directories' PHP files will be updated where they call the prefixed classes.
194 |
195 | The remainder is empty:
196 |
197 | - `constant_prefix` is for `define( "A_CONSTANT", value );` -> `define( "MY_PREFIX_A_CONSTANT", value );`. If it is empty, constants are not prefixed (this may change to an inferred value).
198 | - `override_autoload` a dictionary, keyed with the package names, of autoload settings to replace those in the original packages' `composer.json` `autoload` property.
199 | - `exclude_from_prefix` / [`file_patterns`](https://github.com/BrianHenryIE/strauss/blob/83484b79cfaa399bba55af0bf4569c24d6eb169d/src/ChangeEnumerator.php#L92-L96)
200 | - `exclude_from_copy`
201 | - [`packages`](https://github.com/BrianHenryIE/strauss/blob/83484b79cfaa399bba55af0bf4569c24d6eb169d/src/FileEnumerator.php#L77-L79) array of package names to be skipped
202 | - [`namespaces`](https://github.com/BrianHenryIE/strauss/blob/83484b79cfaa399bba55af0bf4569c24d6eb169d/src/FileEnumerator.php#L95-L97) array of namespaces to skip (exact match from the package autoload keys)
203 | - [`file_patterns`](https://github.com/BrianHenryIE/strauss/blob/83484b79cfaa399bba55af0bf4569c24d6eb169d/src/FileEnumerator.php#L133-L137) array of regex patterns to check filenames against (including vendor relative path) where Strauss will skip that file if there is a match
204 | - `exclude_from_prefix`
205 | - [`packages`](https://github.com/BrianHenryIE/strauss/blob/83484b79cfaa399bba55af0bf4569c24d6eb169d/src/ChangeEnumerator.php#L86-L90) array of package names to exclude from prefixing.
206 | - [`namespaces`](https://github.com/BrianHenryIE/strauss/blob/83484b79cfaa399bba55af0bf4569c24d6eb169d/src/ChangeEnumerator.php#L177-L181) array of exact match namespaces to exclude (i.e. not substring/parent namespaces)
207 | - [`namespace_replacement_patterns`](https://github.com/BrianHenryIE/strauss/blob/83484b79cfaa399bba55af0bf4569c24d6eb169d/src/ChangeEnumerator.php#L183-L190) a dictionary to use in `preg_replace` instead of prefixing with `namespace_prefix`.
208 |
209 | ## Autoloading
210 |
211 | Strauss uses Composer's own tools to generate a set of autoload files in the `target_directory` and creates an `autoload.php` alongside it, so in many projects autoloading is just a matter of:
212 |
213 | ```php
214 | require_once __DIR__ . '/vendor-prefixed/autoload.php';
215 | ```
216 |
217 | If you plan to continue using Composer's autoloader you probably want to turn on `delete_vendor_packages` or set `target_directory` to `vendor`.
218 |
219 | You can use `strauss include-autoloader` to add a line to `vendor/autoload.php` which includes the autoloader for the new files.
220 |
221 | When `delete_vendor_packages` is enabled, `vendor/composer/autoload_aliases.php` is created to allow modified classes to be loaded with their old name during development. This file should not be included in your production code.
222 |
223 | ## Motivation & Comparison to Mozart
224 |
225 | I was happy to make PRs to Mozart to fix bugs, but they weren't being reviewed and merged. At the time of writing, somewhere approaching 50% of Mozart's code [was written by me](https://github.com/coenjacobs/mozart/graphs/contributors) with an additional [nine open PRs](https://github.com/coenjacobs/mozart/pulls?q=is%3Apr+author%3ABrianHenryIE+) and the majority of issues' solutions [provided by me](https://github.com/coenjacobs/mozart/issues?q=is%3Aissue+). This fork is a means to merge all outstanding bugfixes I've written and make some more drastic changes I see as a better approach to the problem.
226 |
227 | Benefits over Mozart:
228 |
229 | * A single output directory whose structure matches source vendor directory structure (conceptually easier than Mozart's independent `classmap_directory` and `dep_directory`)
230 | * A generated `autoload.php` to `include` in your project (analogous to Composer's `vendor/autoload.php`)
231 | * Handles `files` autoloaders – and any autoloaders that Composer itself recognises, since Strauss uses Composer's own tooling to parse the packages
232 | * Zero configuration – Strauss infers sensible defaults from your `composer.json`
233 | * No destructive defaults – `delete_vendor_files` defaults to `false`, so any destruction is explicitly opt-in
234 | * Licence files are included and PHP file headers are edited to adhere to licence requirements around modifications. My understanding is that re-distributing code that Mozart has handled is non-compliant with most open source licences – illegal!
235 | * Extensively tested – PhpUnit tests have been written to validate that many of Mozart's bugs are not present in Strauss
236 | * More configuration options – allowing exclusions in copying and editing files, and allowing specific/multiple namespace renaming
237 | * Respects `composer.json` `vendor-dir` configuration
238 | * Prefixes constants (`define`)
239 | * Handles meta-packages and virtual-packages
240 |
241 | Strauss will read the Mozart configuration from your `composer.json` to enable a seamless migration.
242 |
243 | ## Alternatives
244 |
245 | I don't have a strong opinion on these. I began using Mozart because it was easy, then I adapted it to what I felt was most natural. I've never used these.
246 |
247 | * [humbug/php-scoper](https://github.com/humbug/php-scoper)
248 | * [TypistTech/imposter-plugin](https://github.com/TypistTech/imposter-plugin)
249 | * [Automattic/jetpack-autoloader](https://github.com/Automattic/jetpack-autoloader)
250 | * [tschallacka/wordpress-composer-plugin-builder](https://github.com/tschallacka/wordpress-composer-plugin-builder)
251 | * [Interfacelab/namespacer](https://github.com/Interfacelab/namespacer)
252 | * [PHP-Prefixer](https://github.com/PHP-Prefixer) SaaS!
253 |
254 | ### Interesting
255 |
256 | * [composer-unused/composer-unused](https://github.com/composer-unused/composer-unused)
257 | * [sdrobov/autopsr4](https://github.com/sdrobov/autopsr4)
258 | * [jaem3l/unfuck](https://github.com/jaem3l/unfuck)
259 | * [bamarni/composer-bin-plugin](https://github.com/bamarni/composer-bin-plugin)
260 | * [phar-io/composer-distributor](https://github.com/phar-io/composer-distributor)
261 |
262 | ## Breaking Changes
263 |
264 | * v0.21.0 – will prefix global functions
265 | * v0.16.0 – will no longer prefix PHP built-in classes seen in polyfill packages
266 | * v0.14.0 – `psr/*` packages no longer excluded by default
267 | * v0.12.0 – default output `target_directory` changes from `strauss` to `vendor-prefixed`
268 |
269 | Please open issues to suggest possible breaking changes. I think we can probably move to 1.0.0 soon.
270 |
271 | ### Backward Compatibility Promise
272 |
273 | This project will not increase its minimum required PHP version ahead of WordPress.
274 |
275 | https://core.trac.wordpress.org/ticket/62622
276 |
277 | ## Changes before v1.0
278 |
279 | * Comprehensive attribution of code forked from Mozart – changes have been drastic and `git blame` is now useless, so I intend to add more attributions
280 | * More consistent naming. Are we prefixing or are we renaming?
281 | * Further unit tests, particularly file-system related
282 | * Regex patterns in config need to be validated
283 | * Change the name? "Renamespacer"?
284 |
285 | ## Changes before v2.0
286 |
287 | The correct approach to this problem is probably via [PHP-Parser](https://github.com/nikic/PHP-Parser/). At least all the tests will be useful.
288 |
289 | ## Acknowledgements
290 |
291 | [Coen Jacobs](https://github.com/coenjacobs/) and all the [contributors to Mozart](https://github.com/coenjacobs/mozart/graphs/contributors), particularly those who wrote nice issues.
292 |
--------------------------------------------------------------------------------
/bin/strauss:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | run();
36 | }, '0.22.2');
37 |
--------------------------------------------------------------------------------
/bootstrap.php:
--------------------------------------------------------------------------------
1 | =2.0.0 <=2.22.3",
24 | "league/flysystem": "^2.1|^3.0",
25 | "league/flysystem-memory": "*",
26 | "nikic/php-parser": "*",
27 | "symfony/console": "^4|^5|^6|^7",
28 | "symfony/finder": "^4|^5|^6|^7"
29 | },
30 | "autoload": {
31 | "psr-4": {
32 | "BrianHenryIE\\Strauss\\": "src/"
33 | },
34 | "files": [
35 | "bootstrap.php"
36 | ]
37 | },
38 | "autoload-dev": {
39 | "psr-4": {
40 | "BrianHenryIE\\Strauss\\Tests\\": "tests/",
41 | "BrianHenryIE\\Strauss\\": [
42 | "tests/",
43 | "tests/Integration",
44 | "tests/Unit"
45 | ]
46 | }
47 | },
48 | "require-dev": {
49 | "php": "^7.4|^8.0",
50 | "ext-json": "*",
51 | "clue/phar-composer": "^1.2",
52 | "jaschilz/php-coverage-badger": "^2.0",
53 | "mheap/phpunit-github-actions-printer": "^1.4",
54 | "mockery/mockery": "^1.6",
55 | "phpstan/phpstan": "^1.10",
56 | "phpunit/phpcov": "*",
57 | "phpunit/phpunit": "^9|^10",
58 | "squizlabs/php_codesniffer": "^3.5"
59 | },
60 | "scripts": {
61 | "post-install-cmd": [
62 | "@install-phive-dependencies"
63 | ],
64 | "post-update-cmd": [
65 | "@install-phive-dependencies"
66 | ],
67 | "cs": [
68 | "phpcs",
69 | "phpstan --memory-limit=4G"
70 | ],
71 | "install-phive-dependencies": [
72 | "if [ -z \"$(command -v phive)\" ]; then echo \"Phive is not installed. Run 'brew install gpg phive' or see https://phar.io/.\"; exit 1; fi;",
73 | "phive install"
74 | ],
75 | "cs-fix": [
76 | "phpcbf || true",
77 | "@cs"
78 | ],
79 | "test": [
80 | "phpunit"
81 | ],
82 | "test-changes": [
83 | "if [ -z \"$(command -v ./tools/php-diff-test)\" ]; then echo \"Please install 'php-diff-test' with 'phive install'.\"; exit 1; fi;",
84 | "if [ \"$XDEBUG_MODE\" != \"coverage\" ]; then echo 'Run with XDEBUG_MODE=coverage composer test-changes'; exit 1; fi;",
85 | "phpunit --filter=\"$(./tools/php-diff-test filter --input-files tests/_reports/php.cov --granularity=line)\" --coverage-text;"
86 | ],
87 | "test-changes-report": [
88 | "if [ -z \"$(command -v ./tools/php-diff-test)\" ]; then echo \"Please install 'php-diff-test' with 'phive install'.\"; exit 1; fi;",
89 | "if [ -z \"$(command -v ./tools/phpcov)\" ]; then echo \"Please install 'phpcov' with 'phive install'.\"; exit 1; fi;",
90 | "if [ \"$XDEBUG_MODE\" != \"coverage\" ]; then echo 'Run with XDEBUG_MODE=coverage composer test-changes-report'; exit 1; fi;",
91 | "if [ -d \"tests/_reports/diff\" ]; then rm -rf tests/_reports/diff; fi;",
92 | "phpunit --filter=\"$(./tools/php-diff-test filter --input-files tests/_reports/php.cov --granularity file)\" --coverage-text --coverage-php tests/_reports/diff/php.cov -d memory_limit=-1;",
93 | "./tools/php-diff-test coverage --input-files tests/_reports/diff/php.cov --output-file tests/_reports/diff/php.cov;",
94 | "./tools/phpcov merge tests/_reports/diff --html tests/_reports/diff/html;",
95 | "open tests/_reports/diff/html/index.html"
96 | ],
97 | "test-coverage": [
98 | "Composer\\Config::disableProcessTimeout",
99 | "if [ \"$XDEBUG_MODE\" != \"coverage\" ]; then echo \"Run with 'XDEBUG_MODE=coverage composer test-coverage'\"; exit 1; fi;",
100 | "phpunit ./tests/Unit --coverage-text --coverage-clover tests/_reports/partial/unitclover.xml --coverage-php tests/_reports/partial/unitphp.cov -d memory_limit=-1 --order-by=random",
101 | "phpcov merge --clover tests/_reports/clover.xml --html tests/_reports/html tests/_reports/partial;",
102 | "php-coverage-badger tests/_reports/clover.xml .github/coverage.svg",
103 | "if [ $(command -v ./tools/phpcov) ]; then git diff master...head > /tmp/master.diff; ./tools/phpcov patch-coverage --path-prefix $(pwd) ./tests/_reports/php.cov /tmp/master.diff || true; fi;",
104 | "# Run 'open ./tests/_reports/html/index.html' to view report."
105 | ]
106 | },
107 | "scripts-descriptions": {
108 | "test-changes": "Run PHPUnit only on lines that have changed in master...HEAD",
109 | "test-changes-report": "Run PHPUnit only on files that have changed in master...HEAD and display the HTML report.",
110 | "test-coverage": "Run PHPUnit tests with coverage. Use 'XDEBUG_MODE=coverage composer test-coverage' to run, 'open ./tests/_reports/html/index.html' to view."
111 | },
112 | "replace":{
113 | "coenjacobs/mozart": "*"
114 | },
115 | "config": {
116 | "sort-packages": true
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/phive.phar.asc:
--------------------------------------------------------------------------------
1 | -----BEGIN PGP SIGNATURE-----
2 |
3 | iQJBBAABCgArFiEEavclJwq4HgTXlEJUnYqYspstXXkFAmbHq+UNHHRlYW1AcGhh
4 | ci5pbwAKCRCdipiymy1deRwWD/4oVUr8uQC5Zjr0rPEkJ5BwWRIpm5PZfhSP/jLC
5 | vnL3TjtbLBy0/emJN69fUBa7oRYJX6x5Hil+P6i01COuLnvL+8ZItXT7ArYtgnJK
6 | wo9+z/jQ+F5xGsBlWECdKGxt3RULpbjyss5mgPLY41WTX7Bts7uSCD9O2ur1hfjE
7 | hJJjPnyhsX3zRS0rNe06SFovQYOItwKfucSjjOW04+XTdbol9Vayevi2M0ipaK16
8 | 8D8OquVxj4ZkXCaSQEz/2vQEb8sFJm4xAkaDNdpq2jSbDZ8Xmlklz39aBPu5TA0m
9 | sol9fkAiRBF2ITtCdO61JLCv2Llt/IYSyu/ONYzvPD9FGD5OKkF/MV7yhf2bY8lA
10 | 1mzfY/UIzmiZ9Cy2p/SJFsi1Mc1xzex4PmOuwxULaRftKtztgLQMA9LvIJMDjpYu
11 | Qr68SSIZ6pm3mzvmd7JUL0qgvDTWmKV1vIKSMMtXgqDkwuOcaLo+th7qxD5SpJhT
12 | mAgnItWmtgRZolB+E2M2V4AMVNou4ydtQxSd4qD6fheXXNmED+2jayD5rSmPlnVm
13 | oRMA1b1HIlz+zIZCYQo1XGrvkVxpfw0Zj4HfwObAnr+NE9JpH53OmZJ3vyqEwZhs
14 | nkC1gip3cK3ZoajeSktK16TqZj9Bl5RDREvyFU2I0XKjxr+5QF5Y6oNEHCD0MH4M
15 | 2mGB3A==
16 | =rU98
17 | -----END PGP SIGNATURE-----
18 |
--------------------------------------------------------------------------------
/src/Composer/ComposerPackage.php:
--------------------------------------------------------------------------------
1 | ,classmap?:array,"psr-4"?:array>}
16 | */
17 | class ComposerPackage
18 | {
19 | /**
20 | * The composer.json file as parsed by Composer.
21 | *
22 | * @see \Composer\Factory::create
23 | *
24 | * @var \Composer\Composer
25 | */
26 | protected \Composer\Composer $composer;
27 |
28 | /**
29 | * The name of the project in composer.json.
30 | *
31 | * e.g. brianhenryie/my-project
32 | *
33 | * @var string
34 | */
35 | protected string $packageName;
36 |
37 | /**
38 | * Virtual packages and meta packages do not have a composer.json.
39 | * Some packages are installed in a different directory name than their package name.
40 | *
41 | * @var ?string
42 | */
43 | protected ?string $relativePath = null;
44 |
45 | /**
46 | * Packages can be symlinked from outside the current project directory.
47 | *
48 | * @var ?string
49 | */
50 | protected ?string $packageAbsolutePath = null;
51 |
52 | /**
53 | * The discovered files, classmap, psr0 and psr4 autoload keys discovered (as parsed by Composer).
54 | *
55 | * @var AutoloadKey
56 | */
57 | protected array $autoload = [];
58 |
59 | /**
60 | * The names in the composer.json's "requires" field (without versions).
61 | *
62 | * @var string[]
63 | */
64 | protected array $requiresNames = [];
65 |
66 | protected string $license;
67 |
68 | /**
69 | * @param string $absolutePath The absolute path to composer.json
70 | * @param ?array{files?:array, classmap?:array, psr?:array>} $overrideAutoload Optional configuration to replace the package's own autoload definition with
71 | * another which Strauss can use.
72 | * @return ComposerPackage
73 | */
74 | public static function fromFile(string $absolutePath, array $overrideAutoload = null): ComposerPackage
75 | {
76 | $composer = Factory::create(new NullIO(), $absolutePath, true);
77 |
78 | return new ComposerPackage($composer, $overrideAutoload);
79 | }
80 |
81 | /**
82 | * This is used for virtual packages, which don't have a composer.json.
83 | *
84 | * @param array{name?:string, license?:string, requires?:array, autoload?:AutoloadKey} $jsonArray composer.json decoded to array
85 | * @param ?AutoloadKey $overrideAutoload New autoload rules to replace the existing ones.
86 | */
87 | public static function fromComposerJsonArray($jsonArray, array $overrideAutoload = null): ComposerPackage
88 | {
89 | $factory = new Factory();
90 | $io = new NullIO();
91 | $composer = $factory->createComposer($io, $jsonArray, true);
92 |
93 | return new ComposerPackage($composer, $overrideAutoload);
94 | }
95 |
96 | /**
97 | * Create a PHP object to represent a composer package.
98 | *
99 | * @param Composer $composer
100 | * @param ?AutoloadKey $overrideAutoload Optional configuration to replace the package's own autoload definition with another which Strauss can use.
101 | */
102 | public function __construct(Composer $composer, array $overrideAutoload = null)
103 | {
104 | $this->composer = $composer;
105 |
106 | $this->packageName = $composer->getPackage()->getName();
107 |
108 | $composerJsonFileAbsolute = $composer->getConfig()->getConfigSource()->getName();
109 |
110 | $absolutePath = realpath(dirname($composerJsonFileAbsolute));
111 | if (false !== $absolutePath) {
112 | $this->packageAbsolutePath = $absolutePath . '/';
113 | }
114 |
115 | $vendorDirectory = $this->composer->getConfig()->get('vendor-dir');
116 | if (file_exists($vendorDirectory . '/' . $this->packageName)) {
117 | $this->relativePath = $this->packageName;
118 | $this->packageAbsolutePath = realpath($vendorDirectory . '/' . $this->packageName) . '/';
119 | // If the package is symlinked, the path will be outside the working directory.
120 | } elseif (0 !== strpos($absolutePath, getcwd()) && 1 === preg_match('/.*[\/\\\\]([^\/\\\\]*[\/\\\\][^\/\\\\]*)[\/\\\\][^\/\\\\]*/', $vendorDirectory, $output_array)) {
121 | $this->relativePath = $output_array[1];
122 | } elseif (1 === preg_match('/.*[\/\\\\]([^\/\\\\]+[\/\\\\][^\/\\\\]+)[\/\\\\]composer.json/', $composerJsonFileAbsolute, $output_array)) {
123 | // Not every package gets installed to a folder matching its name (crewlabs/unsplash).
124 | $this->relativePath = $output_array[1];
125 | }
126 |
127 | if (!is_null($overrideAutoload)) {
128 | $composer->getPackage()->setAutoload($overrideAutoload);
129 | }
130 |
131 | $this->autoload = $composer->getPackage()->getAutoload();
132 |
133 | foreach ($composer->getPackage()->getRequires() as $_name => $packageLink) {
134 | $this->requiresNames[] = $packageLink->getTarget();
135 | }
136 |
137 | // Try to get the license from the package's composer.json, assume proprietary (all rights reserved!).
138 | $this->license = !empty($composer->getPackage()->getLicense())
139 | ? implode(',', $composer->getPackage()->getLicense())
140 | : 'proprietary?';
141 | }
142 |
143 | /**
144 | * Composer package project name.
145 | *
146 | * vendor/project-name
147 | *
148 | * @return string
149 | */
150 | public function getPackageName(): string
151 | {
152 | return $this->packageName;
153 | }
154 |
155 | /**
156 | * Is this relative to vendor?
157 | */
158 | public function getRelativePath(): ?string
159 | {
160 | return $this->relativePath . '/';
161 | }
162 |
163 | public function getPackageAbsolutePath(): ?string
164 | {
165 | return $this->packageAbsolutePath;
166 | }
167 |
168 | /**
169 | *
170 | * e.g. ['psr-4' => [ 'BrianHenryIE\Project' => 'src' ]]
171 | * e.g. ['psr-4' => [ 'BrianHenryIE\Project' => ['src','lib] ]]
172 | * e.g. ['classmap' => [ 'src', 'lib' ]]
173 | * e.g. ['files' => [ 'lib', 'functions.php' ]]
174 | *
175 | * @return AutoloadKey
176 | */
177 | public function getAutoload(): array
178 | {
179 | return $this->autoload;
180 | }
181 |
182 | /**
183 | * The names of the packages in the composer.json's "requires" field (without version).
184 | *
185 | * Excludes PHP, ext-*, since we won't be copying or prefixing them.
186 | *
187 | * @return string[]
188 | */
189 | public function getRequiresNames(): array
190 | {
191 | // Unset PHP, ext-*.
192 | $removePhpExt = function ($element) {
193 | return !( 0 === strpos($element, 'ext') || 'php' === $element );
194 | };
195 |
196 | return array_filter($this->requiresNames, $removePhpExt);
197 | }
198 |
199 | public function getLicense():string
200 | {
201 | return $this->license;
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/src/Composer/Extra/ReplaceConfigInterface.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | public function getNamespaceReplacementPatterns(): array;
17 |
18 | public function isIncludeModifiedDate(): bool;
19 |
20 | public function isIncludeAuthor(): bool;
21 |
22 | public function getUpdateCallSites(): ?array;
23 | }
24 |
--------------------------------------------------------------------------------
/src/Composer/ProjectComposerPackage.php:
--------------------------------------------------------------------------------
1 | ,classmap?:array,"psr-4"?:array>} $overrideAutoload
21 | */
22 | public function __construct(string $absolutePathFile, ?array $overrideAutoload = null)
23 | {
24 | $composer = Factory::create(new NullIO(), $absolutePathFile, true);
25 |
26 | parent::__construct($composer, $overrideAutoload);
27 |
28 | $authors = $this->composer->getPackage()->getAuthors();
29 | if (empty($authors) || !isset($authors[0]['name'])) {
30 | $this->author = explode("/", $this->packageName, 2)[0];
31 | } else {
32 | $this->author = $authors[0]['name'];
33 | }
34 |
35 | $this->vendorDirectory = is_string($this->composer->getConfig()->get('vendor-dir'))
36 | ? ltrim(str_replace(dirname($absolutePathFile), '', $this->composer->getConfig()->get('vendor-dir')), '\\/')
37 | : 'vendor';
38 | }
39 |
40 | /**
41 | * @return StraussConfig
42 | * @throws \Exception
43 | */
44 | public function getStraussConfig(): StraussConfig
45 | {
46 | $config = new StraussConfig($this->composer);
47 | $config->setVendorDirectory($this->getVendorDirectory());
48 | return $config;
49 | }
50 |
51 |
52 | public function getAuthor(): string
53 | {
54 | return $this->author;
55 | }
56 |
57 | /**
58 | * Relative vendor directory with trailing slash.
59 | */
60 | public function getVendorDirectory(): string
61 | {
62 | return rtrim($this->vendorDirectory, '\\/') . '/';
63 | }
64 |
65 | /**
66 | * Get all values in the autoload key as a flattened array.
67 | *
68 | * @return string[]
69 | */
70 | public function getFlatAutoloadKey(): array
71 | {
72 | $autoload = $this->getAutoload();
73 | $values = [];
74 | array_walk_recursive(
75 | $autoload,
76 | function ($value, $key) use (&$values) {
77 | $values[] = $value;
78 | }
79 | );
80 | return $values;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Config/AliasesConfigInterface.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | public function getNamespaceReplacementPatterns(): array;
21 |
22 | public function getNamespacePrefix(): ?string;
23 |
24 | public function getClassmapPrefix(): ?string;
25 | }
26 |
--------------------------------------------------------------------------------
/src/Config/CleanupConfigInterface.php:
--------------------------------------------------------------------------------
1 | add($composeCommand);
21 |
22 | $replaceCommand = new ReplaceCommand();
23 | $this->add($replaceCommand);
24 |
25 | $this->add(new IncludeAutoloaderCommand());
26 |
27 | $this->setDefaultCommand('dependencies');
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Console/Commands/IncludeAutoloaderCommand.php:
--------------------------------------------------------------------------------
1 | setName('include-autoloader');
52 | $this->setDescription("Adds `require autoload_aliases.php` and `require vendor-prefixed/autoload.php` to `vendor/autoload.php`.");
53 |
54 | // TODO: permissions?
55 | $this->filesystem = new Filesystem(
56 | new \League\Flysystem\Filesystem(new LocalFilesystemAdapter('/')),
57 | getcwd() . '/'
58 | );
59 | }
60 |
61 | /**
62 | * @param InputInterface $input
63 | * @param OutputInterface $output
64 | *
65 | * @see Command::execute()
66 | *
67 | */
68 | protected function execute(InputInterface $input, OutputInterface $output): int
69 | {
70 | $logger = new ConsoleLogger(
71 | $output,
72 | [ LogLevel::INFO => OutputInterface::VERBOSITY_NORMAL ]
73 | );
74 |
75 | $this->setLogger($logger);
76 |
77 | $workingDir = getcwd() . '/';
78 | $this->workingDir = $workingDir;
79 |
80 | try {
81 | $this->loadProjectComposerPackage();
82 | $this->loadConfigFromComposerJson();
83 |
84 | // Pipeline
85 |
86 | // TODO: check for `--no-dev` somewhere.
87 |
88 | $vendorComposerAutoload = new VendorComposerAutoload(
89 | $this->config,
90 | $this->filesystem,
91 | $logger
92 | );
93 |
94 | $vendorComposerAutoload->addAliasesFileToComposer();
95 | $vendorComposerAutoload->addVendorPrefixedAutoloadToVendorAutoload();
96 | } catch (Exception $e) {
97 | $this->logger->error($e->getMessage());
98 |
99 | return Command::FAILURE;
100 | }
101 |
102 | return Command::SUCCESS;
103 | }
104 |
105 | /**
106 | * 1. Load the composer.json.
107 | *
108 | * @throws Exception
109 | */
110 | protected function loadProjectComposerPackage(): void
111 | {
112 | $this->logger->notice('Loading package...');
113 |
114 | $this->projectComposerPackage = new ProjectComposerPackage($this->workingDir . 'composer.json');
115 | }
116 |
117 | protected function loadConfigFromComposerJson(): void
118 | {
119 | $this->logger->notice('Loading composer.json config...');
120 |
121 | $this->config = $this->projectComposerPackage->getStraussConfig();
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/Console/Commands/ReplaceCommand.php:
--------------------------------------------------------------------------------
1 | setName('replace');
64 | $this->setDescription("Rename a namespace in files.");
65 | $this->setHelp('');
66 |
67 | $this->addOption(
68 | 'from',
69 | null,
70 | InputArgument::OPTIONAL,
71 | 'Original namespace'
72 | );
73 |
74 | $this->addOption(
75 | 'to',
76 | null,
77 | InputArgument::OPTIONAL,
78 | 'New namespace'
79 | );
80 |
81 | $this->addOption(
82 | 'paths',
83 | null,
84 | InputArgument::OPTIONAL,
85 | 'Comma separated list of files and directories to update. Default is the current working directory.',
86 | getcwd()
87 | );
88 |
89 | // TODO: permissions?
90 | $this->filesystem = new Filesystem(
91 | new \League\Flysystem\Filesystem(new LocalFilesystemAdapter('/')),
92 | getcwd() . '/'
93 | );
94 | }
95 |
96 | /**
97 | * @param InputInterface $input
98 | * @param OutputInterface $output
99 | *
100 | * @see Command::execute()
101 | *
102 | */
103 | protected function execute(InputInterface $input, OutputInterface $output): int
104 | {
105 | $this->setLogger(
106 | new ConsoleLogger(
107 | $output,
108 | [ LogLevel::INFO => OutputInterface::VERBOSITY_NORMAL ]
109 | )
110 | );
111 |
112 | $workingDir = getcwd() . '/';
113 | $this->workingDir = $workingDir;
114 |
115 | try {
116 | $config = $this->createConfig($input);
117 | $this->config = $config;
118 |
119 | // Pipeline
120 |
121 | $this->enumerateFiles($config);
122 |
123 | $this->determineChanges($config);
124 |
125 | $this->performReplacements($config);
126 |
127 | $this->performReplacementsInProjectFiles($config);
128 |
129 | $this->addLicenses($config);
130 | } catch (Exception $e) {
131 | $this->logger->error($e->getMessage());
132 |
133 | return 1;
134 | }
135 |
136 | return Command::SUCCESS;
137 | }
138 |
139 | protected function createConfig(InputInterface $input): ReplaceConfigInterface
140 | {
141 | $config = new StraussConfig();
142 |
143 | $from = $input->getOption('from');
144 | $to = $input->getOption('to');
145 |
146 | // TODO:
147 | $config->setNamespaceReplacementPatterns([$from => $to]);
148 |
149 | $paths = explode(',', $input->getOption('paths'));
150 |
151 | $config->setUpdateCallSites($paths);
152 |
153 | return $config;
154 | }
155 |
156 |
157 | protected function enumerateFiles(ReplaceConfigInterface $config): void
158 | {
159 | $this->logger->info('Enumerating files...');
160 | $relativeUpdateCallSites = $config->getUpdateCallSites();
161 | $updateCallSites = array_map(
162 | fn($path) => false !== strpos($path, trim($this->workingDir, '/')) ? $path : $this->workingDir . $path,
163 | $relativeUpdateCallSites
164 | );
165 | $fileEnumerator = new FileEnumerator($config, $this->filesystem);
166 | $this->discoveredFiles = $fileEnumerator->compileFileListForPaths($updateCallSites);
167 | }
168 |
169 | // 4. Determine namespace and classname changes
170 | protected function determineChanges(ReplaceConfigInterface $config): void
171 | {
172 | $this->logger->info('Determining changes...');
173 |
174 | $fileScanner = new FileSymbolScanner(
175 | $config,
176 | $this->filesystem
177 | );
178 |
179 | $this->discoveredSymbols = $fileScanner->findInFiles($this->discoveredFiles);
180 |
181 | $changeEnumerator = new ChangeEnumerator(
182 | $config,
183 | $this->filesystem
184 | );
185 | $changeEnumerator->determineReplacements($this->discoveredSymbols);
186 | }
187 |
188 | // 5. Update namespaces and class names.
189 | // Replace references to updated namespaces and classnames throughout the dependencies.
190 | protected function performReplacements(ReplaceConfigInterface $config): void
191 | {
192 | $this->logger->info('Performing replacements...');
193 |
194 | $this->replacer = new Prefixer($config, $this->filesystem, $this->logger);
195 |
196 | $this->replacer->replaceInFiles($this->discoveredSymbols, $this->discoveredFiles->getFiles());
197 | }
198 |
199 | protected function performReplacementsInProjectFiles(ReplaceConfigInterface $config): void
200 | {
201 |
202 | $relativeCallSitePaths = $this->config->getUpdateCallSites();
203 |
204 | if (empty($relativeCallSitePaths)) {
205 | return;
206 | }
207 |
208 | $callSitePaths = array_map(
209 | fn($path) => false !== strpos($path, trim($this->workingDir, '/')) ? $path : $this->workingDir . $path,
210 | $relativeCallSitePaths
211 | );
212 |
213 | $projectReplace = new Prefixer($config, $this->filesystem, $this->logger);
214 |
215 | $fileEnumerator = new FileEnumerator(
216 | $config,
217 | $this->filesystem
218 | );
219 |
220 | $phpFilePaths = $fileEnumerator->compileFileListForPaths($callSitePaths);
221 |
222 | // TODO: Warn when a file that was specified is not found (during config validation).
223 | // $this->logger->warning('Expected file not found from project autoload: ' . $absolutePath);
224 |
225 | $phpFilesAbsolutePaths = array_map(
226 | fn($file) => $file->getSourcePath(),
227 | $phpFilePaths->getFiles()
228 | );
229 |
230 | $projectReplace->replaceInProjectFiles($this->discoveredSymbols, $phpFilesAbsolutePaths);
231 | }
232 |
233 |
234 | protected function addLicenses(ReplaceConfigInterface $config): void
235 | {
236 | $this->logger->info('Adding licenses...');
237 |
238 | $username = trim(shell_exec('git config user.name'));
239 | $email = trim(shell_exec('git config user.email'));
240 |
241 | if (!empty($username) && !empty($email)) {
242 | // e.g. "Brian Henry ".
243 | $author = $username . ' <' . $email . '>';
244 | } else {
245 | // e.g. "brianhenry".
246 | $author = get_current_user();
247 | }
248 |
249 | // TODO: Update to use DiscoveredFiles
250 | $dependencies = $this->flatDependencyTree;
251 | $licenser = new Licenser($config, $dependencies, $author, $this->filesystem, $this->logger);
252 |
253 | $licenser->copyLicenses();
254 |
255 | $modifiedFiles = $this->replacer->getModifiedFiles();
256 | $licenser->addInformationToUpdatedFiles($modifiedFiles);
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/src/Files/DiscoveredFiles.php:
--------------------------------------------------------------------------------
1 | */
10 | protected array $files = [];
11 |
12 | public function add(FileBase $file): void
13 | {
14 | $this->files[$file->getSourcePath()] = $file;
15 | }
16 |
17 | /**
18 | * @return array
19 | */
20 | public function getFiles(): array
21 | {
22 | return $this->files;
23 | }
24 |
25 | /**
26 | * Fetch/check if a file exists in the discovered files.
27 | *
28 | * @param string $sourceAbsolutePath Full path to the file.
29 | */
30 | public function getFile(string $sourceAbsolutePath): ?FileBase
31 | {
32 | return $this->files[$sourceAbsolutePath] ?? null;
33 | }
34 |
35 | public function sort(): void
36 | {
37 | ksort($this->files);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Files/File.php:
--------------------------------------------------------------------------------
1 | sourceAbsolutePath = $sourceAbsolutePath;
34 | }
35 |
36 | public function getSourcePath(): string
37 | {
38 | return $this->sourceAbsolutePath;
39 | }
40 |
41 | public function isPhpFile(): bool
42 | {
43 | return substr($this->sourceAbsolutePath, -4) === '.php';
44 | }
45 |
46 | /**
47 | * Some combination of file copy exclusions and vendor-dir == target-dir
48 | *
49 | * @param bool $doCopy
50 | *
51 | * @return void
52 | */
53 | public function setDoCopy(bool $doCopy): void
54 | {
55 | $this->doCopy = $doCopy;
56 | }
57 | public function isDoCopy(): bool
58 | {
59 | return $this->doCopy;
60 | }
61 |
62 | public function setDoPrefix(bool $doPrefix): void
63 | {
64 | }
65 |
66 | /**
67 | * Is this correct? Is there ever a time that NO changes should be made to a file? I.e. another file would have its
68 | * namespace changed and it needs to be updated throughout.
69 | *
70 | * Is this really a Symbol level function?
71 | */
72 | public function isDoPrefix(): bool
73 | {
74 | return true;
75 | }
76 |
77 | /**
78 | * Used to mark files that are symlinked as not-to-be-deleted.
79 | *
80 | * @param bool $doDelete
81 | */
82 | public function setDoDelete(bool $doDelete): void
83 | {
84 | $this->doDelete = $doDelete;
85 | }
86 |
87 | /**
88 | * Should file be deleted?
89 | *
90 | * NB: Also respect the "delete_vendor_files"|"delete_vendor_packages" settings.
91 | */
92 | public function isDoDelete(): bool
93 | {
94 | return $this->doDelete;
95 | }
96 |
97 | public function setDidDelete(bool $didDelete): void
98 | {
99 | $this->didDelete = $didDelete;
100 | }
101 | public function getDidDelete(): bool
102 | {
103 | return $this->didDelete;
104 | }
105 |
106 | public function addDiscoveredSymbol(DiscoveredSymbol $symbol): void
107 | {
108 | $this->discoveredSymbols[$symbol->getOriginalSymbol()] = $symbol;
109 | }
110 |
111 | /**
112 | * @return array The discovered symbols in the file, indexed by their original string name.
113 | */
114 | public function getDiscoveredSymbols(): array
115 | {
116 | return $this->discoveredSymbols;
117 | }
118 |
119 | public function setAbsoluteTargetPath(string $absoluteTargetPath): void
120 | {
121 | $this->absoluteTargetPath = $absoluteTargetPath;
122 | }
123 |
124 | /**
125 | * The target path to (maybe) copy the file to, and the target path to perform replacements in (which may be the
126 | * original path).
127 | */
128 | public function getAbsoluteTargetPath(): string
129 | {
130 | // TODO: Maybe this is a mistake and should better be an exception.
131 | return isset($this->absoluteTargetPath) ? $this->absoluteTargetPath : $this->sourceAbsolutePath;
132 | }
133 |
134 | protected bool $didUpdate = false;
135 | public function setDidUpdate(): void
136 | {
137 | $this->didUpdate = true;
138 | }
139 | public function getDidUpdate(): bool
140 | {
141 | return $this->didUpdate;
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/Files/FileBase.php:
--------------------------------------------------------------------------------
1 | vendorRelativePath = ltrim($vendorRelativePath, '/\\');
30 | $this->dependency = $dependency;
31 | }
32 |
33 | public function getDependency(): ComposerPackage
34 | {
35 | return $this->dependency;
36 | }
37 |
38 | /**
39 | * The target path to (maybe) copy the file to, and the target path to perform replacements in (which may be the
40 | * original path).
41 | */
42 |
43 | /**
44 | * Record the autoloader it is found in. Which could be all of them.
45 | */
46 | public function addAutoloader(string $autoloaderType): void
47 | {
48 | $this->autoloaderTypes = array_unique(array_merge($this->autoloaderTypes, array($autoloaderType)));
49 | }
50 |
51 | public function isFilesAutoloaderFile(): bool
52 | {
53 | return in_array('files', $this->autoloaderTypes, true);
54 | }
55 |
56 | public function getVendorRelativePath(): string
57 | {
58 | return $this->vendorRelativePath;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Files/HasDependency.php:
--------------------------------------------------------------------------------
1 | flysystem = $flysystem;
39 | $this->normalizer = new StripProtocolPathNormalizer('mem');
40 |
41 | $this->workingDir = $workingDir;
42 | }
43 |
44 | /**
45 | * @param string[] $fileAndDirPaths
46 | *
47 | * @return string[]
48 | */
49 | public function findAllFilesAbsolutePaths(array $fileAndDirPaths): array
50 | {
51 | $files = [];
52 |
53 | foreach ($fileAndDirPaths as $path) {
54 | if (!$this->directoryExists($path)) {
55 | $files[] = $path;
56 | continue;
57 | }
58 |
59 | $directoryListing = $this->listContents(
60 | $path,
61 | FilesystemReader::LIST_DEEP
62 | );
63 |
64 | /** @var FileAttributes[] $files */
65 | $fileAttributesArray = $directoryListing->toArray();
66 |
67 | $f = array_map(fn($file) => '/'.$file->path(), $fileAttributesArray);
68 |
69 | $files = array_merge($files, $f);
70 | }
71 |
72 | return $files;
73 | }
74 |
75 | public function getAttributes(string $absolutePath): ?StorageAttributes
76 | {
77 | $fileDirectory = realpath(dirname($absolutePath));
78 |
79 | $absolutePath = $this->normalizer->normalizePath($absolutePath);
80 |
81 | // Unsupported symbolic link encountered at location //home
82 | // \League\Flysystem\SymbolicLinkEncountered
83 | $dirList = $this->listContents($fileDirectory)->toArray();
84 | foreach ($dirList as $file) { // TODO: use the generator.
85 | if ($file->path() === $absolutePath) {
86 | return $file;
87 | }
88 | }
89 |
90 | return null;
91 | }
92 |
93 | public function fileExists(string $location): bool
94 | {
95 | return $this->flysystem->fileExists(
96 | $this->normalizer->normalizePath($location)
97 | );
98 | }
99 |
100 | public function read(string $location): string
101 | {
102 | return $this->flysystem->read(
103 | $this->normalizer->normalizePath($location)
104 | );
105 | }
106 |
107 | public function readStream(string $location)
108 | {
109 | return $this->flysystem->readStream(
110 | $this->normalizer->normalizePath($location)
111 | );
112 | }
113 |
114 | public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing
115 | {
116 | return $this->flysystem->listContents(
117 | $this->normalizer->normalizePath($location),
118 | $deep
119 | );
120 | }
121 |
122 | public function lastModified(string $path): int
123 | {
124 | return $this->flysystem->lastModified(
125 | $this->normalizer->normalizePath($path)
126 | );
127 | }
128 |
129 | public function fileSize(string $path): int
130 | {
131 | return $this->flysystem->fileSize(
132 | $this->normalizer->normalizePath($path)
133 | );
134 | }
135 |
136 | public function mimeType(string $path): string
137 | {
138 | return $this->flysystem->mimeType(
139 | $this->normalizer->normalizePath($path)
140 | );
141 | }
142 |
143 | public function visibility(string $path): string
144 | {
145 | return $this->flysystem->visibility(
146 | $this->normalizer->normalizePath($path)
147 | );
148 | }
149 |
150 | public function write(string $location, string $contents, array $config = []): void
151 | {
152 | $this->flysystem->write(
153 | $this->normalizer->normalizePath($location),
154 | $contents,
155 | $config
156 | );
157 | }
158 |
159 | public function writeStream(string $location, $contents, array $config = []): void
160 | {
161 | $this->flysystem->writeStream(
162 | $this->normalizer->normalizePath($location),
163 | $contents,
164 | $config
165 | );
166 | }
167 |
168 | public function setVisibility(string $path, string $visibility): void
169 | {
170 | $this->flysystem->setVisibility(
171 | $this->normalizer->normalizePath($path),
172 | $visibility
173 | );
174 | }
175 |
176 | public function delete(string $location): void
177 | {
178 | $this->flysystem->delete(
179 | $this->normalizer->normalizePath($location)
180 | );
181 | }
182 |
183 | public function deleteDirectory(string $location): void
184 | {
185 | $this->flysystem->deleteDirectory(
186 | $this->normalizer->normalizePath($location)
187 | );
188 | }
189 |
190 | public function createDirectory(string $location, array $config = []): void
191 | {
192 | $this->flysystem->createDirectory(
193 | $this->normalizer->normalizePath($location),
194 | $config
195 | );
196 | }
197 |
198 | public function move(string $source, string $destination, array $config = []): void
199 | {
200 | $this->flysystem->move(
201 | $this->normalizer->normalizePath($source),
202 | $this->normalizer->normalizePath($destination),
203 | $config
204 | );
205 | }
206 |
207 | public function copy(string $source, string $destination, array $config = []): void
208 | {
209 | $this->flysystem->copy(
210 | $this->normalizer->normalizePath($source),
211 | $this->normalizer->normalizePath($destination),
212 | $config
213 | );
214 | }
215 |
216 | /**
217 | *
218 | * /path/to/this/dir, /path/to/file.php => ../../file.php
219 | * /path/to/here, /path/to/here/dir/file.php => dir/file.php
220 | *
221 | * @param string $fromAbsoluteDirectory
222 | * @param string $toAbsolutePath
223 | * @return string
224 | */
225 | public function getRelativePath(string $fromAbsoluteDirectory, string $toAbsolutePath): string
226 | {
227 | $fromAbsoluteDirectory = $this->normalizer->normalizePath($fromAbsoluteDirectory);
228 | $toAbsolutePath = $this->normalizer->normalizePath($toAbsolutePath);
229 |
230 | $fromDirectoryParts = array_filter(explode('/', $fromAbsoluteDirectory));
231 | $toPathParts = array_filter(explode('/', $toAbsolutePath));
232 | foreach ($fromDirectoryParts as $key => $part) {
233 | if ($part === $toPathParts[$key]) {
234 | unset($toPathParts[$key]);
235 | unset($fromDirectoryParts[$key]);
236 | } else {
237 | break;
238 | }
239 | if (count($fromDirectoryParts) === 0 || count($toPathParts) === 0) {
240 | break;
241 | }
242 | }
243 |
244 | $relativePath =
245 | str_repeat('../', count($fromDirectoryParts))
246 | . implode('/', $toPathParts);
247 |
248 | if ($this->directoryExists($toAbsolutePath)) {
249 | $relativePath .= '/';
250 | }
251 |
252 | return $relativePath;
253 | }
254 |
255 |
256 | /**
257 | * Check does the filepath point to a file outside the working directory.
258 | * If `realpath()` fails to resolve the path, assume it's a symlink.
259 | */
260 | public function isSymlinkedFile(FileBase $file): bool
261 | {
262 | $realpath = realpath($file->getSourcePath());
263 |
264 | return ! $realpath || ! str_starts_with($realpath, $this->workingDir);
265 | }
266 |
267 | /**
268 | * Does the subdir path start with the dir path?
269 | */
270 | public function isSubDirOf(string $dir, string $subdir): bool
271 | {
272 | return str_starts_with(
273 | $this->normalizer->normalizePath($subdir),
274 | $this->normalizer->normalizePath($dir)
275 | );
276 | }
277 |
278 | public function normalize(string $path)
279 | {
280 | return $this->normalizer->normalizePath($path);
281 | }
282 | }
283 |
--------------------------------------------------------------------------------
/src/Helpers/FlysystemBackCompatInterface.php:
--------------------------------------------------------------------------------
1 | flysystem, 'directoryExists')) {
20 | return $this->flysystem->directoryExists($location);
21 | }
22 |
23 | $normalizer = new WhitespacePathNormalizer();
24 | $normalizer = new StripProtocolPathNormalizer(['mem'], $normalizer);
25 | $location = $normalizer->normalizePath($location);
26 |
27 | $parentDirectoryContents = $this->listContents(dirname($location));
28 | /** @var FileAttributes $entry */
29 | foreach ($parentDirectoryContents as $entry) {
30 | if ($entry->path() == $location) {
31 | return $entry->isDir();
32 | }
33 | }
34 |
35 | return false;
36 | }
37 |
38 | // Some version of Flysystem has:
39 | // has
40 | public function has(string $location): bool
41 | {
42 | if (method_exists($this->flysystem, 'has')) {
43 | return $this->flysystem->has($location);
44 | }
45 | return $this->fileExists($location) || $this->directoryExists($location);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Helpers/InMemoryFilesystemAdapter.php:
--------------------------------------------------------------------------------
1 | fileExists($path)) {
15 | // Assume it is a directory.
16 |
17 | // Maybe check does the directory exist.
18 | // $parentDirContents = (array) $this->listContents(dirname($path), false);
19 | // throw UnableToRetrieveMetadata::visibility($path, 'file does not exist');
20 |
21 | return new FileAttributes($path, null, 'public');
22 | }
23 |
24 |
25 | return parent::visibility($path);
26 | }
27 |
28 | public function lastModified(string $path): FileAttributes
29 | {
30 | if (!$this->fileExists($path)) {
31 | // Assume it is a directory
32 | return new FileAttributes($path, null, null, 0);
33 | }
34 |
35 | return parent::lastModified($path);
36 | }
37 |
38 | public function copy(string $source, string $destination, Config $config): void
39 | {
40 | $this->createDirectories($destination, $config);
41 |
42 | parent::copy($source, $destination, $config);
43 | }
44 |
45 | public function write(string $path, string $contents, Config $config): void
46 | {
47 | // Make sure there is a directory for the file to be written to.
48 | if (false === strpos($path, '______DUMMY_FILE_FOR_FORCED_LISTING_IN_FLYSYSTEM_TEST')) {
49 | $this->createDirectories($path, $config);
50 | }
51 |
52 | parent::write($path, $contents, $config);
53 | }
54 |
55 | protected function createDirectories(string $path, Config $config): void
56 | {
57 | $pathDirs = explode('/', dirname($path));
58 | for ($level = 0; $level < count($pathDirs); $level++) {
59 | $dir = implode('/', array_slice($pathDirs, 0, $level + 1));
60 | $this->createDirectory($dir, $config);
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Helpers/NamespaceSort.php:
--------------------------------------------------------------------------------
1 | order = $order;
19 | }
20 |
21 | public function __invoke(string $a, string $b): int
22 | {
23 | $a = trim($a, '\\');
24 | $b = trim($b, '\\');
25 |
26 | return $this->order === self::LONGEST
27 | ? $this->sort($a, $b)
28 | : $this->sort($b, $a);
29 | }
30 |
31 | protected function sort(string $a, string $b): int
32 | {
33 |
34 | $aParts = explode('\\', $a);
35 | $bParts = explode('\\', $b);
36 |
37 | $aPartCount = count($aParts);
38 | $bPartCount = count($bParts);
39 |
40 | if ($aPartCount !== $bPartCount) {
41 | return $bPartCount - $aPartCount;
42 | }
43 |
44 | $bLastPart = array_pop($aParts);
45 | $aLastPart = array_pop($bParts);
46 |
47 | return strlen($aLastPart) - strlen($bLastPart);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Helpers/ReadOnlyFileSystem.php:
--------------------------------------------------------------------------------
1 | filesystem = $filesystem;
34 |
35 | $this->inMemoryFiles = new InMemoryFilesystemAdapter();
36 | $this->deletedFiles = new InMemoryFilesystemAdapter();
37 |
38 | $this->pathNormalizer = $pathNormalizer ?? new WhitespacePathNormalizer();
39 | }
40 |
41 | public function fileExists(string $location): bool
42 | {
43 | if ($this->deletedFiles->fileExists($location)) {
44 | return false;
45 | }
46 | return $this->inMemoryFiles->fileExists($location)
47 | || $this->filesystem->fileExists($location);
48 | }
49 |
50 | public function write(string $location, string $contents, array $config = []): void
51 | {
52 | $config = new \League\Flysystem\Config($config);
53 | $this->inMemoryFiles->write($location, $contents, $config);
54 |
55 | if ($this->deletedFiles->fileExists($location)) {
56 | $this->deletedFiles->delete($location);
57 | }
58 | }
59 |
60 | public function writeStream(string $location, $contents, $config = []): void
61 | {
62 | $config = new \League\Flysystem\Config($config);
63 | $this->rewindStream($contents);
64 | $this->inMemoryFiles->writeStream($location, $contents, $config);
65 |
66 | if ($this->deletedFiles->fileExists($location)) {
67 | $this->deletedFiles->delete($location);
68 | }
69 | }
70 | /**
71 | * @param resource $resource
72 | */
73 | private function rewindStream($resource): void
74 | {
75 | if (ftell($resource) !== 0 && stream_get_meta_data($resource)['seekable']) {
76 | rewind($resource);
77 | }
78 | }
79 |
80 | public function read(string $location): string
81 | {
82 | if ($this->deletedFiles->fileExists($location)) {
83 | throw UnableToReadFile::fromLocation($location);
84 | }
85 | if ($this->inMemoryFiles->fileExists($location)) {
86 | return $this->inMemoryFiles->read($location);
87 | }
88 | return $this->filesystem->read($location);
89 | }
90 |
91 | public function readStream(string $location)
92 | {
93 | if ($this->deletedFiles->fileExists($location)) {
94 | throw UnableToReadFile::fromLocation($location);
95 | }
96 | if ($this->inMemoryFiles->fileExists($location)) {
97 | return $this->inMemoryFiles->readStream($location);
98 | }
99 | return $this->filesystem->readStream($location);
100 | }
101 |
102 | public function delete(string $location): void
103 | {
104 | if ($this->fileExists($location)) {
105 | $file = $this->read($location);
106 | $this->deletedFiles->write($location, $file, new Config([]));
107 | }
108 | if ($this->inMemoryFiles->fileExists($location)) {
109 | $this->inMemoryFiles->delete($location);
110 | }
111 | }
112 |
113 | public function deleteDirectory(string $location): void
114 | {
115 | $location = $this->pathNormalizer->normalizePath($location);
116 |
117 | $this->deletedFiles->createDirectory($location, new Config([]));
118 | $this->inMemoryFiles->deleteDirectory($location);
119 | }
120 |
121 |
122 | public function createDirectory(string $location, array $config = []): void
123 | {
124 | $this->inMemoryFiles->createDirectory($location, new Config($config));
125 |
126 | $this->deletedFiles->deleteDirectory($location);
127 | }
128 |
129 | public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing
130 | {
131 | /** @var FileAttributes[] $actual */
132 | $actual = $this->filesystem->listContents($location, $deep)->toArray();
133 |
134 | $inMemoryFilesGenerator = $this->inMemoryFiles->listContents($location, $deep);
135 | $inMemoryFilesArray = $inMemoryFilesGenerator instanceof Traversable
136 | ? iterator_to_array($inMemoryFilesGenerator, false)
137 | : (array) $inMemoryFilesGenerator;
138 |
139 | $inMemoryFilePaths = array_map(fn($file) => $file->path(), $inMemoryFilesArray);
140 |
141 | $deletedFilesGenerator = $this->deletedFiles->listContents($location, $deep);
142 | $deletedFilesArray = $deletedFilesGenerator instanceof Traversable
143 | ? iterator_to_array($deletedFilesGenerator, false)
144 | : (array) $deletedFilesGenerator;
145 | $deletedFilePaths = array_map(fn($file) => $file->path(), $deletedFilesArray);
146 |
147 | $actual = array_filter($actual, fn($file) => !in_array($file->path(), $inMemoryFilePaths));
148 | $actual = array_filter($actual, fn($file) => !in_array($file->path(), $deletedFilePaths));
149 |
150 | $good = array_merge($actual, $inMemoryFilesArray);
151 |
152 | return new DirectoryListing($good);
153 | }
154 |
155 | public function move(string $source, string $destination, array $config = []): void
156 | {
157 | throw new \BadMethodCallException('Not yet implemented');
158 | }
159 |
160 | public function copy(string $source, string $destination, $config = null): void
161 | {
162 | $sourceFile = $this->read($source);
163 |
164 | $this->inMemoryFiles->write(
165 | $destination,
166 | $sourceFile,
167 | $config instanceof Config ? $config : new Config($config ?? [])
168 | );
169 |
170 | $a = $this->inMemoryFiles->read($destination);
171 | if ($sourceFile !== $a) {
172 | throw new \Exception('Copy failed');
173 | }
174 |
175 | if ($this->deletedFiles->fileExists($destination)) {
176 | $this->deletedFiles->delete($destination);
177 | }
178 | }
179 |
180 | private function getAttributes(string $path): StorageAttributes
181 | {
182 | $parentDirectoryContents = $this->listContents(dirname($path), false);
183 | /** @var FileAttributes $entry */
184 | foreach ($parentDirectoryContents as $entry) {
185 | if ($entry->path() == $path) {
186 | return $entry;
187 | }
188 | }
189 | throw UnableToReadFile::fromLocation($path);
190 | }
191 |
192 | public function lastModified(string $path): int
193 | {
194 | $attributes = $this->getAttributes($path);
195 | return $attributes->lastModified() ?? 0;
196 | }
197 |
198 | public function fileSize(string $path): int
199 | {
200 | $filesize = 0;
201 |
202 | if ($this->inMemoryFiles->fileExists($path)) {
203 | $filesize = $this->inMemoryFiles->fileSize($path);
204 | } elseif ($this->filesystem->fileExists($path)) {
205 | $filesize = $this->filesystem->fileSize($path);
206 | }
207 |
208 | if ($filesize instanceof FileAttributes) {
209 | return $filesize->fileSize();
210 | }
211 |
212 | return $filesize;
213 | }
214 |
215 | public function mimeType(string $path): string
216 | {
217 | throw new \BadMethodCallException('Not yet implemented');
218 | }
219 |
220 | public function setVisibility(string $path, string $visibility): void
221 | {
222 | throw new \BadMethodCallException('Not yet implemented');
223 | }
224 |
225 | public function visibility(string $path): string
226 | {
227 | $path = $this->pathNormalizer->normalizePath($path);
228 |
229 | if (!$this->fileExists($path) && !$this->directoryExists($path)) {
230 | throw UnableToRetrieveMetadata::visibility($path, 'file does not exist');
231 | }
232 |
233 | if ($this->deletedFiles->fileExists($path)) {
234 | throw UnableToRetrieveMetadata::visibility($path, 'file does not exist');
235 | }
236 | if ($this->inMemoryFiles->fileExists($path)) {
237 | $attributes = $this->inMemoryFiles->visibility($path);
238 | return $attributes->visibility();
239 | }
240 | if ($this->filesystem->fileExists($path)) {
241 | return $this->filesystem->visibility($path);
242 | }
243 | return \League\Flysystem\Visibility::PUBLIC;
244 | }
245 |
246 | public function directoryExists(string $location): bool
247 | {
248 | $location = $this->pathNormalizer->normalizePath($location);
249 |
250 | if ($this->directoryExistsIn($location, $this->deletedFiles)) {
251 | return false;
252 | }
253 |
254 | return $this->directoryExistsIn($location, $this->inMemoryFiles)
255 | || $this->directoryExistsIn($location, $this->filesystem);
256 | }
257 |
258 | protected function directoryExistsIn(string $location, $filesystem): bool
259 | {
260 | if (method_exists($filesystem, 'directoryExists')) {
261 | return $filesystem->directoryExists($location);
262 | }
263 |
264 | $parentDirectoryContents = $filesystem->listContents(dirname($location), false);
265 | /** @var FileAttributes $entry */
266 | foreach ($parentDirectoryContents as $entry) {
267 | if ($entry->path() == $location) {
268 | return $entry->isDir();
269 | }
270 | }
271 |
272 | return false;
273 | }
274 |
275 | public function has(string $location): bool
276 | {
277 | throw new \BadMethodCallException('Not yet implemented');
278 | }
279 | }
280 |
--------------------------------------------------------------------------------
/src/Pipeline/Aliases.php:
--------------------------------------------------------------------------------
1 | config = $config;
46 | $this->fileSystem = $fileSystem;
47 | $this->setLogger($logger ?? new NullLogger());
48 | }
49 |
50 | public function writeAliasesFileForSymbols(DiscoveredSymbols $symbols): void
51 | {
52 | $outputFilepath = $this->getAliasFilepath();
53 |
54 | $fileString = $this->buildStringOfAliases($symbols, basename($outputFilepath));
55 |
56 | if (empty($fileString)) {
57 | // TODO: Check if no actual aliases were added (i.e. is it just an empty template).
58 | // Log?
59 | return;
60 | }
61 |
62 | $this->fileSystem->write($outputFilepath, $fileString);
63 | }
64 |
65 | /**
66 | * @return array FQDN => relative path
67 | */
68 | protected function getVendorClassmap(): array
69 | {
70 | $paths = array_map(
71 | function ($file) {
72 | return $this->config->isDryRun()
73 | ? new \SplFileInfo('mem://'.$file->path())
74 | : new \SplFileInfo('/'.$file->path());
75 | },
76 | array_filter(
77 | $this->fileSystem->listContents($this->config->getVendorDirectory(), true)->toArray(),
78 | fn(StorageAttributes $file) => $file->isFile() && in_array(substr($file->path(), -3), ['php', 'inc', '.hh'])
79 | )
80 | );
81 |
82 | $vendorClassmap = ClassMapGenerator::createMap($paths);
83 |
84 | $vendorClassmap = array_map(fn($path) => str_replace('mem://', '', $path), $vendorClassmap);
85 |
86 | return $vendorClassmap;
87 | }
88 |
89 | /**
90 | * @return array FQDN => absolute path
91 | */
92 | protected function getTargetClassmap(): array
93 | {
94 | $paths =
95 | array_map(
96 | function ($file) {
97 | return $this->config->isDryRun()
98 | ? new \SplFileInfo('mem://'.$file->path())
99 | : new \SplFileInfo('/'.$file->path());
100 | },
101 | array_filter(
102 | $this->fileSystem->listContents($this->config->getTargetDirectory(), \League\Flysystem\FilesystemReader::LIST_DEEP)->toArray(),
103 | fn(StorageAttributes $file) => $file->isFile() && in_array(substr($file->path(), -3), ['php', 'inc', '.hh'])
104 | )
105 | );
106 |
107 | $classMap = ClassMapGenerator::createMap($paths);
108 |
109 | // To make it easier when viewing in xdebug.
110 | uksort($classMap, new NamespaceSort());
111 |
112 | $classMap = array_map(fn($path) => str_replace('mem://', '', $path), $classMap);
113 |
114 | return $classMap;
115 | }
116 |
117 | /**
118 | * We will create `vendor/composer/autoload_aliases.php` alongside other autoload files, e.g. `autoload_real.php`.
119 | */
120 | protected function getAliasFilepath(): string
121 | {
122 | return sprintf(
123 | '%scomposer/autoload_aliases.php',
124 | $this->config->getVendorDirectory()
125 | );
126 | }
127 |
128 | /**
129 | * @param DiscoveredSymbol[] $symbols
130 | * @return DiscoveredSymbol[]
131 | */
132 | protected function getModifiedSymbols(array $symbols): array
133 | {
134 | $modifiedSymbols = [];
135 | foreach ($symbols as $symbol) {
136 | if ($symbol->getOriginalSymbol() !== $symbol->getReplacement()) {
137 | $modifiedSymbols[] = $symbol;
138 | }
139 | }
140 | return $modifiedSymbols;
141 | }
142 |
143 | protected function buildStringOfAliases(DiscoveredSymbols $symbols, string $outputFilename): string
144 | {
145 |
146 | $sourceDirClassmap = $this->getVendorClassmap();
147 |
148 | $autoloadAliasesFileString = 'getModifiedSymbols($symbols->getSymbols());
153 |
154 | $functionSymbols = array_filter($modifiedSymbols, fn(DiscoveredSymbol $symbol) => $symbol instanceof FunctionSymbol);
155 | $otherSymbols = array_filter($modifiedSymbols, fn(DiscoveredSymbol $symbol) => !($symbol instanceof FunctionSymbol));
156 |
157 | $targetDirClassmap = $this->getTargetClassmap();
158 |
159 | if (count($otherSymbols)>0) {
160 | $autoloadAliasesFileString .= 'function autoloadAliases( $classname ): void {' . PHP_EOL;
161 | $autoloadAliasesFileString = $this->appendAliasString($otherSymbols, $sourceDirClassmap, $targetDirClassmap, $autoloadAliasesFileString);
162 | $autoloadAliasesFileString .= '}' . PHP_EOL . PHP_EOL;
163 | $autoloadAliasesFileString .= "spl_autoload_register( 'autoloadAliases' );" . PHP_EOL . PHP_EOL;
164 | }
165 |
166 | if (count($functionSymbols)>0) {
167 | $autoloadAliasesFileString = $this->appendFunctionAliases($functionSymbols, $autoloadAliasesFileString);
168 | }
169 |
170 | return $autoloadAliasesFileString;
171 | }
172 |
173 | /**
174 | * @param array $modifiedSymbols
175 | * @param array $sourceDirClassmap
176 | * @param array $targetDirClasssmap
177 | * @param string $autoloadAliasesFileString
178 | * @return string
179 | * @throws \League\Flysystem\FilesystemException
180 | */
181 | protected function appendAliasString(array $modifiedSymbols, array $sourceDirClassmap, array $targetDirClasssmap, string $autoloadAliasesFileString): string
182 | {
183 | $aliasesPhpString = ' switch( $classname ) {' . PHP_EOL;
184 |
185 | foreach ($modifiedSymbols as $symbol) {
186 | $originalSymbol = $symbol->getOriginalSymbol();
187 | $replacementSymbol = $symbol->getReplacement();
188 |
189 | // if (!$symbol->getSourceFile()->isDoDelete()) {
190 | // $this->logger->debug("Skipping {$originalSymbol} because it is not marked for deletion.");
191 | // continue;
192 | // }
193 |
194 | if ($originalSymbol === $replacementSymbol) {
195 | $this->logger->debug("Skipping {$originalSymbol} because it is not being changed.");
196 | continue;
197 | }
198 |
199 | switch (get_class($symbol)) {
200 | case NamespaceSymbol::class:
201 | // TODO: namespaced constants?
202 | $namespace = $symbol->getOriginalSymbol();
203 |
204 | $symbolSourceFiles = $symbol->getSourceFiles();
205 |
206 | $namespacesInOriginalClassmap = array_filter(
207 | $sourceDirClassmap,
208 | fn($filepath) => in_array($filepath, array_keys($symbolSourceFiles))
209 | );
210 |
211 | foreach ($namespacesInOriginalClassmap as $originalFqdnClassName => $absoluteFilePath) {
212 | if ($symbol->getOriginalSymbol() === $symbol->getReplacement()) {
213 | continue;
214 | }
215 |
216 | $localName = array_reverse(explode('\\', $originalFqdnClassName))[0];
217 |
218 | if (0 !== strpos($originalFqdnClassName, $symbol->getReplacement())) {
219 | $newFqdnClassName = $symbol->getReplacement() . '\\' . $localName;
220 | } else {
221 | $newFqdnClassName = $originalFqdnClassName;
222 | }
223 |
224 | if (!isset($targetDirClasssmap[$newFqdnClassName]) && !isset($sourceDirClassmap[$originalFqdnClassName])) {
225 | $a = $symbol->getSourceFiles();
226 | /** @var File $b */
227 | $b = array_pop($a); // There's gotta be at least one.
228 |
229 | throw new \Exception("errorrrr " . ' ' . basename($b->getAbsoluteTargetPath()) . ' ' . $originalFqdnClassName . ' ' . $newFqdnClassName . PHP_EOL . PHP_EOL);
230 | }
231 |
232 | $symbolFilepath = $targetDirClasssmap[$newFqdnClassName] ?? $sourceDirClassmap[$originalFqdnClassName];
233 | $symbolFileString = $this->fileSystem->read($symbolFilepath);
234 |
235 | // This should be improved with a check for non-class-valid characters after the name.
236 | // Eventually it should be in the File object itself.
237 | $isClass = 1 === preg_match('/class ' . $localName . '/i', $symbolFileString);
238 | $isInterface = 1 === preg_match('/interface ' . $localName . '/i', $symbolFileString);
239 | $isTrait = 1 === preg_match('/trait ' . $localName . '/i', $symbolFileString);
240 |
241 | if (!$isClass && !$isInterface && !$isTrait) {
242 | $isEnum = 1 === preg_match('/enum ' . $localName . '/', $symbolFileString);
243 |
244 | if ($isEnum) {
245 | $this->logger->warning("Skipping $newFqdnClassName – enum aliasing not yet implemented.");
246 | // TODO: enums
247 | continue;
248 | }
249 |
250 | $this->logger->error("Skipping $newFqdnClassName because it doesn't exist.");
251 | throw new \Exception("Skipping $newFqdnClassName because it doesn't exist.");
252 | }
253 |
254 | $escapedOriginalFqdnClassName = str_replace('\\', '\\\\', $originalFqdnClassName);
255 | $aliasesPhpString .= " case '$escapedOriginalFqdnClassName':" . PHP_EOL;
256 |
257 | if ($isClass) {
258 | $aliasesPhpString .= " class_alias(\\$newFqdnClassName::class, \\$originalFqdnClassName::class);" . PHP_EOL;
259 | } elseif ($isInterface) {
260 | $aliasesPhpString .= " \$includeFile = 'getOriginalSymbol(); // We want the original to continue to work, so it is the alias.
273 | $concreteClass = $symbol->getReplacement();
274 | $aliasesPhpString .= <<getOriginalSymbol();
307 | $replacementSymbol = $symbol->getReplacement();
308 |
309 | // if (!$symbol->getSourceFile()->isDoDelete()) {
310 | // $this->logger->debug("Skipping {$originalSymbol} because it is not marked for deletion.");
311 | // continue;
312 | // }
313 |
314 | if ($originalSymbol === $replacementSymbol) {
315 | $this->logger->debug("Skipping {$originalSymbol} because it is not being changed.");
316 | continue;
317 | }
318 |
319 | switch (get_class($symbol)) {
320 | case FunctionSymbol::class:
321 | // TODO: Do we need to check for `void`? Or will it just be ignored?
322 | // Is it possible to inherit PHPDoc from the original function?
323 | $aliasesPhpString = <<> $discoveredFilesAutoloaders Array of packagePath => array of relativeFilePaths.
33 | */
34 | protected array $discoveredFilesAutoloaders;
35 |
36 | protected string $absoluteTargetDirectory;
37 |
38 | /**
39 | * Autoload constructor.
40 | *
41 | * @param StraussConfig $config
42 | * @param array> $discoveredFilesAutoloaders
43 | */
44 | public function __construct(
45 | AutoloadConfigInterface $config,
46 | array $discoveredFilesAutoloaders,
47 | Filesystem $filesystem,
48 | ?LoggerInterface $logger = null
49 | ) {
50 | $this->config = $config;
51 | $this->discoveredFilesAutoloaders = $discoveredFilesAutoloaders;
52 | $this->filesystem = $filesystem;
53 | $this->setLogger($logger ?? new NullLogger());
54 | }
55 |
56 | public function generate(array $flatDependencyTree, DiscoveredSymbols $discoveredSymbols): void
57 | {
58 | if (!$this->config->isClassmapOutput()) {
59 | $this->logger->debug('Not generating autoload.php because classmap output is disabled.');
60 | return;
61 | }
62 |
63 | $this->logger->info('Generating autoload files for ' . $this->config->getTargetDirectory());
64 |
65 | if ($this->config->getVendorDirectory() !== $this->config->getTargetDirectory()) {
66 | $installedJson = new InstalledJson(
67 | $this->config,
68 | $this->filesystem,
69 | $this->logger
70 | );
71 | $installedJson->createAndCleanTargetDirInstalledJson($flatDependencyTree, $discoveredSymbols);
72 | }
73 |
74 | (new DumpAutoload(
75 | $this->config,
76 | $this->filesystem,
77 | $this->logger
78 | ))->generatedPrefixedAutoloader();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Pipeline/Autoload/ComposerAutoloadGenerator.php:
--------------------------------------------------------------------------------
1 | projectUniqueString = $projectUniqueString;
50 | }
51 |
52 | /**
53 | * Get a unique id for the `files` autoload entry.
54 | *
55 | * `$path` here is `PackageInterface->getTargetDir()`.`PackageInterface::getAutoload()['files'][]`
56 | *
57 | * @override
58 | * @see AutoloadGenerator::getFileIdentifier()
59 | *
60 | * @param PackageInterface $package The package to get the file identifier for.
61 | * @param string $path Relative path from `vendor`.
62 | *
63 | * @return string
64 | */
65 | protected function getFileIdentifier(PackageInterface $package, string $path)
66 | {
67 | return hash('md5', $package->getName() . ':' . $path . ':' . $this->projectUniqueString);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Pipeline/Autoload/DumpAutoload.php:
--------------------------------------------------------------------------------
1 | config = $config;
36 | $this->filesystem = $filesystem;
37 | $this->setLogger($logger ?? new NullLogger());
38 | }
39 |
40 | /**
41 | * Uses `vendor/composer/installed.json` to output autoload files to `vendor-prefixed/composer`.
42 | */
43 | public function generatedPrefixedAutoloader(): void
44 | {
45 | /**
46 | * Unfortunately, `::dump()` creates the target directories if they don't exist, even though it otherwise respects `::setDryRun()`.
47 | */
48 | if ($this->config->isDryRun()) {
49 | return;
50 | }
51 |
52 | $relativeTargetDir = $this->filesystem->getRelativePath(
53 | $this->config->getProjectDirectory(),
54 | $this->config->getTargetDirectory()
55 | );
56 |
57 | $defaultVendorDirBefore = Config::$defaultConfig['vendor-dir'];
58 |
59 | Config::$defaultConfig['vendor-dir'] = $relativeTargetDir;
60 |
61 | $composer = Factory::create(new NullIO(), $this->config->getProjectDirectory() . 'composer.json');
62 | $installationManager = $composer->getInstallationManager();
63 | $package = $composer->getPackage();
64 |
65 | $projectComposerJson = new JsonFile($this->config->getProjectDirectory() . 'composer.json');
66 | $projectComposerJsonArray = $projectComposerJson->read();
67 | if (isset($projectComposerJsonArray['config'], $projectComposerJsonArray['config']['vendor-dir'])) {
68 | unset($projectComposerJsonArray['config']['vendor-dir']);
69 | }
70 |
71 | /**
72 | * Cannot use `$composer->getConfig()`, need to create a new one so the vendor-dir is correct.
73 | */
74 | $config = new \Composer\Config(false, $this->config->getProjectDirectory());
75 |
76 | $config->merge([
77 | 'config' => $projectComposerJsonArray['config'] ?? []
78 | ]);
79 |
80 | $generator = new ComposerAutoloadGenerator(
81 | $this->config->getNamespacePrefix(),
82 | $composer->getEventDispatcher()
83 | );
84 |
85 | $generator->setDryRun($this->config->isDryRun());
86 | $generator->setClassMapAuthoritative(true);
87 | $generator->setRunScripts(false);
88 | // $generator->setApcu($apcu, $apcuPrefix);
89 | // $generator->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input));
90 | $optimize = true; // $input->getOption('optimize') || $config->get('optimize-autoloader');
91 | $generator->setDevMode(false);
92 |
93 | $localRepo = new InstalledFilesystemRepository(new JsonFile($this->config->getTargetDirectory() . 'composer/installed.json'));
94 |
95 | $strictAmbiguous = false; // $input->getOption('strict-ambiguous')
96 |
97 | // This will output the autoload_static.php etc. files to `vendor-prefixed/composer`.
98 | $generator->dump(
99 | $config,
100 | $localRepo,
101 | $package,
102 | $installationManager,
103 | 'composer',
104 | $optimize,
105 | $this->getSuffix(),
106 | $composer->getLocker(),
107 | $strictAmbiguous
108 | );
109 |
110 | /**
111 | * Tests fail if this is absent.
112 | *
113 | * Arguably this should be in ::setUp() and tearDown() in the test classes, but if other tools run after Strauss
114 | * then they might expect it to be unmodified.
115 | */
116 | Config::$defaultConfig['vendor-dir'] = $defaultVendorDirBefore;
117 |
118 | $this->prefixNewAutoloader();
119 | }
120 |
121 | protected function prefixNewAutoloader(): void
122 | {
123 | $this->logger->debug('Prefixing the new Composer autoloader.');
124 |
125 | $projectReplace = new Prefixer(
126 | $this->config,
127 | $this->filesystem,
128 | $this->logger
129 | );
130 |
131 | $fileEnumerator = new FileEnumerator(
132 | $this->config,
133 | $this->filesystem
134 | );
135 |
136 | $projectFiles = $fileEnumerator->compileFileListForPaths([
137 | $this->config->getTargetDirectory() . 'composer',
138 | ]);
139 |
140 | $phpFiles = array_filter(
141 | $projectFiles->getFiles(),
142 | fn($file) => $file->isPhpFile()
143 | );
144 |
145 | $phpFilesAbsolutePaths = array_map(
146 | fn($file) => $file->getSourcePath(),
147 | $phpFiles
148 | );
149 |
150 | $sourceFile = new File(__DIR__);
151 | $composerNamespaceSymbol = new NamespaceSymbol(
152 | 'Composer\\Autoload',
153 | $sourceFile
154 | );
155 | $composerNamespaceSymbol->setReplacement(
156 | $this->config->getNamespacePrefix() . '\\Composer\\Autoload'
157 | );
158 |
159 | $discoveredSymbols = new DiscoveredSymbols();
160 | $discoveredSymbols->add(
161 | $composerNamespaceSymbol
162 | );
163 |
164 | $projectReplace->replaceInProjectFiles($discoveredSymbols, $phpFilesAbsolutePaths);
165 | }
166 |
167 | /**
168 | * If there is an existing autoloader, it will use the same suffix. If there is not, it pulls the suffix from
169 | * {Composer::getLocker()} and clashes with the existing autoloader.
170 | *
171 | * @see AutoloadGenerator::dump() 412:431
172 | * @see https://github.com/composer/composer/blob/ae208dc1e182bd45d99fcecb956501da212454a1/src/Composer/Autoload/AutoloadGenerator.php#L429
173 | */
174 | protected function getSuffix(): ?string
175 | {
176 | return !$this->filesystem->fileExists($this->config->getTargetDirectory() . 'autoload.php')
177 | ? bin2hex(random_bytes(16))
178 | : null;
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/src/Pipeline/Autoload/VendorComposerAutoload.php:
--------------------------------------------------------------------------------
1 | config = $config;
34 | $this->fileSystem = $filesystem;
35 | $this->setLogger($logger);
36 | }
37 |
38 | public function addVendorPrefixedAutoloadToVendorAutoload(): void
39 | {
40 | if ($this->config->getTargetDirectory() === $this->config->getVendorDirectory()) {
41 | $this->logger->info("Target dir is source dir, no autoload.php to add.");
42 | return;
43 | }
44 |
45 | $composerAutoloadPhpFilepath = $this->config->getVendorDirectory() . 'autoload.php';
46 |
47 | if (!$this->fileSystem->fileExists($composerAutoloadPhpFilepath)) {
48 | $this->logger->info("No autoload.php found:" . $composerAutoloadPhpFilepath);
49 | return;
50 | }
51 |
52 | $newAutoloadPhpFilepath = $this->config->getTargetDirectory() . 'autoload.php';
53 |
54 | if (!$this->fileSystem->fileExists($newAutoloadPhpFilepath)) {
55 | $this->logger->warning("No new autoload.php found: " . $newAutoloadPhpFilepath);
56 | }
57 |
58 | $this->logger->info('Modifying original autoload.php to add `' . $newAutoloadPhpFilepath);
59 |
60 | $composerAutoloadPhpFileString = $this->fileSystem->read($composerAutoloadPhpFilepath);
61 |
62 | $newComposerAutoloadPhpFileString = $this->addVendorPrefixedAutoloadToComposerAutoload($composerAutoloadPhpFileString);
63 |
64 | if ($newComposerAutoloadPhpFileString !== $composerAutoloadPhpFileString) {
65 | $this->logger->info('Writing new autoload.php');
66 | $this->fileSystem->write($composerAutoloadPhpFilepath, $newComposerAutoloadPhpFileString);
67 | } else {
68 | $this->logger->debug('No changes to autoload.php');
69 | }
70 | }
71 |
72 | /**
73 | * Given the PHP code string for `vendor/autoload.php`, add a `require_once autoload_aliases.php`
74 | * before require autoload_real.php.
75 | */
76 | public function addAliasesFileToComposer(): void
77 | {
78 | if ($this->isComposerInstalled()) {
79 | $this->logger->info("Strauss installed via Composer, no need to add `autoload_aliases.php` to `vendor/autoload.php`");
80 | return;
81 | }
82 |
83 | $composerAutoloadPhpFilepath = $this->config->getVendorDirectory() . 'autoload.php';
84 |
85 | if (!$this->fileSystem->fileExists($composerAutoloadPhpFilepath)) {
86 | // No `vendor/autoload.php` file to add `autoload_aliases.php` to.
87 | $this->logger->error("No autoload.php found: " . $composerAutoloadPhpFilepath);
88 | // TODO: Should probably throw an exception here.
89 | return;
90 | }
91 |
92 | if ($this->isComposerNoDev()) {
93 | $this->logger->notice("Composer was run with `--no-dev`, no need to add `autoload_aliases.php` to `vendor/autoload.php`");
94 | return;
95 | }
96 |
97 | $this->logger->info('Modifying original autoload.php to add autoload_aliases.php in ' . $this->config->getVendorDirectory());
98 |
99 | $composerAutoloadPhpFileString = $this->fileSystem->read($composerAutoloadPhpFilepath);
100 |
101 | $newComposerAutoloadPhpFileString = $this->addAliasesFileToComposerAutoload($composerAutoloadPhpFileString);
102 |
103 | if ($newComposerAutoloadPhpFileString !== $composerAutoloadPhpFileString) {
104 | $this->logger->info('Writing new autoload.php');
105 | $this->fileSystem->write($composerAutoloadPhpFilepath, $newComposerAutoloadPhpFileString);
106 | } else {
107 | $this->logger->debug('No changes to autoload.php');
108 | }
109 | }
110 |
111 | /**
112 | * Determine is Strauss installed via Composer (otherwise presumably run via phar).
113 | */
114 | protected function isComposerInstalled(): bool
115 | {
116 | if (!$this->fileSystem->fileExists($this->config->getVendorDirectory() . 'composer/installed.json')) {
117 | return false;
118 | }
119 |
120 | $installedJsonArray = json_decode($this->fileSystem->read($this->config->getVendorDirectory() . 'composer/installed.json'), true);
121 |
122 | return isset($installedJsonArray['dev-package-names']['brianhenryie/strauss']);
123 | }
124 |
125 | /**
126 | * Read `vendor/composer/installed.json` to determine if the composer was run with `--no-dev`.
127 | *
128 | * {
129 | * "packages": [],
130 | * "dev": true,
131 | * "dev-package-names": []
132 | * }
133 | */
134 | protected function isComposerNoDev(): bool
135 | {
136 | $installedJson = $this->fileSystem->read($this->config->getVendorDirectory() . 'composer/installed.json');
137 | $installedJsonArray = json_decode($installedJson, true);
138 | return !$installedJsonArray['dev'];
139 | }
140 |
141 | /**
142 | * This is a very over-engineered way to do a string replace.
143 | *
144 | * `require_once __DIR__ . '/composer/autoload_aliases.php';`
145 | */
146 | protected function addAliasesFileToComposerAutoload(string $code): string
147 | {
148 | if (false !== strpos($code, '/composer/autoload_aliases.php')) {
149 | $this->logger->info('vendor/autoload.php already includes autoload_aliases.php');
150 | return $code;
151 | }
152 |
153 | $parser = (new ParserFactory())->createForNewestSupportedVersion();
154 | try {
155 | $ast = $parser->parse($code);
156 | } catch (Error $error) {
157 | $this->logger->error("Parse error: {$error->getMessage()}");
158 | return $code;
159 | }
160 |
161 | $traverser = new NodeTraverser();
162 | $traverser->addVisitor(new class() extends NodeVisitorAbstract {
163 |
164 | public function leaveNode(Node $node)
165 | {
166 | if (get_class($node) === \PhpParser\Node\Stmt\Expression::class) {
167 | $prettyPrinter = new Standard();
168 | $maybeRequireAutoloadReal = $prettyPrinter->prettyPrintExpr($node->expr);
169 |
170 | // Every `vendor/autoload.php` should have this line.
171 | $target = "require_once __DIR__ . '/composer/autoload_real.php'";
172 |
173 | // If this node isn't the one we want to insert before, continue.
174 | if ($maybeRequireAutoloadReal !== $target) {
175 | return $node;
176 | }
177 |
178 | // __DIR__ . '/composer/autoload_aliases.php'
179 | $path = new \PhpParser\Node\Expr\BinaryOp\Concat(
180 | new \PhpParser\Node\Scalar\MagicConst\Dir(),
181 | new \PhpParser\Node\Scalar\String_('/composer/autoload_aliases.php')
182 | );
183 |
184 | // require_once
185 | $requireOnceAutoloadAliases = new Node\Stmt\Expression(
186 | new \PhpParser\Node\Expr\Include_(
187 | $path,
188 | \PhpParser\Node\Expr\Include_::TYPE_REQUIRE_ONCE
189 | )
190 | );
191 |
192 | // if(file_exists()){}
193 | $ifFileExistsRequireOnceAutoloadAliases = new \PhpParser\Node\Stmt\If_(
194 | new \PhpParser\Node\Expr\FuncCall(
195 | new \PhpParser\Node\Name('file_exists'),
196 | [
197 | new \PhpParser\Node\Arg($path)
198 | ],
199 | ),
200 | [
201 | 'stmts' => [
202 | $requireOnceAutoloadAliases
203 | ],
204 | ]
205 | );
206 |
207 | // Add a blank line. Probably not the correct way to do this.
208 | $node->setAttribute('comments', [new \PhpParser\Comment('')]);
209 | $ifFileExistsRequireOnceAutoloadAliases->setAttribute('comments', [new \PhpParser\Comment('')]);
210 |
211 | return [
212 | $ifFileExistsRequireOnceAutoloadAliases,
213 | $node
214 | ];
215 | }
216 | return $node;
217 | }
218 | });
219 |
220 | $modifiedStmts = $traverser->traverse($ast);
221 |
222 | $prettyPrinter = new Standard();
223 |
224 | return $prettyPrinter->prettyPrintFile($modifiedStmts);
225 | }
226 |
227 | /**
228 | * `require_once __DIR__ . '/../vendor-prefixed/autoload.php';`
229 | */
230 | protected function addVendorPrefixedAutoloadToComposerAutoload(string $code): string
231 | {
232 | if ($this->config->getTargetDirectory() === $this->config->getVendorDirectory()) {
233 | $this->logger->info('Vendor directory is target directory, no autoloader to add.');
234 | return $code;
235 | }
236 |
237 | $targetDirAutoload = '/' . $this->fileSystem->getRelativePath($this->config->getVendorDirectory(), $this->config->getTargetDirectory()) . 'autoload.php';
238 |
239 | if (false !== strpos($code, $targetDirAutoload)) {
240 | $this->logger->info('vendor/autoload.php already includes ' . $targetDirAutoload);
241 | return $code;
242 | }
243 |
244 | $parser = (new ParserFactory())->createForNewestSupportedVersion();
245 | try {
246 | $ast = $parser->parse($code);
247 | } catch (Error $error) {
248 | $this->logger->error("Parse error: {$error->getMessage()}");
249 | return $code;
250 | }
251 |
252 | $traverser = new NodeTraverser();
253 | $traverser->addVisitor(new class($targetDirAutoload) extends NodeVisitorAbstract {
254 |
255 | protected bool $added = false;
256 | protected ?string $targetDirectoryAutoload;
257 | public function __construct(?string $targetDirectoryAutoload)
258 | {
259 | $this->targetDirectoryAutoload = $targetDirectoryAutoload;
260 | }
261 |
262 | public function leaveNode(Node $node)
263 | {
264 | if ($this->added) {
265 | return $node;
266 | }
267 |
268 | if (get_class($node) === \PhpParser\Node\Stmt\Expression::class) {
269 | $prettyPrinter = new Standard();
270 | $nodeText = $prettyPrinter->prettyPrintExpr($node->expr);
271 |
272 | $targets = [
273 | "require_once __DIR__ . '/composer/autoload_real.php'",
274 | ];
275 |
276 | if (!in_array($nodeText, $targets)) {
277 | return $node;
278 | }
279 |
280 | // __DIR__ . '../vendor-prefixed/autoload.php'
281 | $path = new \PhpParser\Node\Expr\BinaryOp\Concat(
282 | new \PhpParser\Node\Scalar\MagicConst\Dir(),
283 | new Node\Scalar\String_($this->targetDirectoryAutoload)
284 | );
285 |
286 | // require_once
287 | $requireOnceStraussAutoload = new Node\Stmt\Expression(
288 | new Node\Expr\Include_(
289 | $path,
290 | Node\Expr\Include_::TYPE_REQUIRE_ONCE
291 | )
292 | );
293 |
294 | // if(file_exists()){}
295 | $ifFileExistsRequireOnceStraussAutoload = new \PhpParser\Node\Stmt\If_(
296 | new \PhpParser\Node\Expr\FuncCall(
297 | new \PhpParser\Node\Name('file_exists'),
298 | [
299 | new \PhpParser\Node\Arg($path)
300 | ],
301 | ),
302 | [
303 | 'stmts' => [
304 | $requireOnceStraussAutoload
305 | ],
306 | ]
307 | );
308 |
309 | // Add a blank line. Probably not the correct way to do this.
310 | $node->setAttribute('comments', [new \PhpParser\Comment('')]);
311 | $ifFileExistsRequireOnceStraussAutoload->setAttribute('comments', [new \PhpParser\Comment('')]);
312 |
313 | $this->added = true;
314 |
315 | return [
316 | $ifFileExistsRequireOnceStraussAutoload,
317 | $node
318 | ];
319 | }
320 | return $node;
321 | }
322 | });
323 |
324 | $modifiedStmts = $traverser->traverse($ast);
325 |
326 | $prettyPrinter = new Standard();
327 |
328 | return $prettyPrinter->prettyPrintFile($modifiedStmts);
329 | }
330 | }
331 |
--------------------------------------------------------------------------------
/src/Pipeline/ChangeEnumerator.php:
--------------------------------------------------------------------------------
1 | config = $config;
34 | $this->filesystem = $filesystem;
35 | $this->setLogger($logger ?? new NullLogger());
36 | }
37 |
38 | public function determineReplacements(DiscoveredSymbols $discoveredSymbols): void
39 | {
40 | foreach ($discoveredSymbols->getSymbols() as $symbol) {
41 | // TODO: this is a bit of a mess. Should be reconsidered. Previously there was 1-1 relationship between symbols and files.
42 | $symbolSourceFiles = $symbol->getSourceFiles();
43 | $symbolSourceFile = $symbolSourceFiles[array_key_first($symbolSourceFiles)];
44 | if ($symbolSourceFile instanceof FileWithDependency) {
45 | if (in_array(
46 | $symbolSourceFile->getDependency()->getPackageName(),
47 | $this->config->getExcludePackagesFromPrefixing(),
48 | true
49 | )) {
50 | continue;
51 | }
52 |
53 | foreach ($this->config->getExcludeFilePatternsFromPrefixing() as $excludeFilePattern) {
54 | // TODO: This source relative path should be from the vendor dir.
55 | // TODO: Should the target path be used here?
56 | if (1 === preg_match($excludeFilePattern, $symbolSourceFile->getSourcePath())) {
57 | continue 2;
58 | }
59 | }
60 | }
61 |
62 | if ($symbol instanceof NamespaceSymbol) {
63 | $namespaceReplacementPatterns = $this->config->getNamespaceReplacementPatterns();
64 |
65 | // `namespace_prefix` is just a shorthand for a replacement pattern that applies to all namespaces.
66 |
67 | // TODO: Maybe need to preg_quote and add regex delimiters to the patterns here.
68 | foreach ($namespaceReplacementPatterns as $pattern => $replacement) {
69 | if (substr($pattern, 0, 1) !== substr($pattern, -1, 1)) {
70 | unset($namespaceReplacementPatterns[$pattern]);
71 | $pattern = '~'. preg_quote($pattern, '~') . '~';
72 | $namespaceReplacementPatterns[$pattern] = $replacement;
73 | }
74 | unset($pattern, $replacement);
75 | }
76 |
77 | if (!is_null($this->config->getNamespacePrefix())) {
78 | $stripPattern = '~^('.preg_quote($this->config->getNamespacePrefix(), '~') .'\\\\*)*(.*)~';
79 | $strippedSymbol = preg_replace(
80 | $stripPattern,
81 | '$2',
82 | $symbol->getOriginalSymbol()
83 | );
84 | $namespaceReplacementPatterns[ "~(" . preg_quote($this->config->getNamespacePrefix(), '~') . '\\\\*)*' . preg_quote($strippedSymbol, '~') . '~' ]
85 | = "{$this->config->getNamespacePrefix()}\\{$strippedSymbol}";
86 | unset($stripPattern, $strippedSymbol);
87 | }
88 |
89 | // `namespace_replacement_patterns` should be ordered by priority.
90 | foreach ($namespaceReplacementPatterns as $namespaceReplacementPattern => $replacement) {
91 | $prefixed = preg_replace(
92 | $namespaceReplacementPattern,
93 | $replacement,
94 | $symbol->getOriginalSymbol()
95 | );
96 |
97 | if ($prefixed !== $symbol->getOriginalSymbol()) {
98 | $symbol->setReplacement($prefixed);
99 | continue 2;
100 | }
101 | }
102 | $this->logger->debug("Namespace {$symbol->getOriginalSymbol()} not changed.");
103 | }
104 |
105 | if ($symbol instanceof ClassSymbol) {
106 | // Don't double-prefix classnames.
107 | if (str_starts_with($symbol->getOriginalSymbol(), $this->config->getClassmapPrefix())) {
108 | continue;
109 | }
110 |
111 | $symbol->setReplacement($this->config->getClassmapPrefix() . $symbol->getOriginalSymbol());
112 | }
113 |
114 | if ($symbol instanceof FunctionSymbol) {
115 | // TODO: Add its own config option.
116 | $functionPrefix = strtolower($this->config->getClassmapPrefix());
117 | if (str_starts_with($symbol->getOriginalSymbol(), $functionPrefix)) {
118 | continue;
119 | }
120 |
121 | $symbol->setReplacement($functionPrefix . $symbol->getOriginalSymbol());
122 | }
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/Pipeline/Cleanup.php:
--------------------------------------------------------------------------------
1 | config = $config;
36 | $this->logger = $logger;
37 |
38 | $this->isDeleteVendorFiles = $config->isDeleteVendorFiles() && $config->getTargetDirectory() !== $config->getVendorDirectory();
39 | $this->isDeleteVendorPackages = $config->isDeleteVendorPackages() && $config->getTargetDirectory() !== $config->getVendorDirectory();
40 |
41 | $this->filesystem = $filesystem;
42 | }
43 |
44 | /**
45 | * Maybe delete the source files that were copied (depending on config),
46 | * then delete empty directories.
47 | *
48 | * @param File[] $files
49 | *
50 | * @throws FilesystemException
51 | */
52 | public function cleanup(array $files): void
53 | {
54 | if (!$this->isDeleteVendorPackages && !$this->isDeleteVendorFiles) {
55 | $this->logger->info('No cleanup required.');
56 | return;
57 | }
58 |
59 | $this->logger->info('Beginning cleanup.');
60 |
61 | if ($this->isDeleteVendorPackages) {
62 | $this->doIsDeleteVendorPackages($files);
63 | } elseif ($this->isDeleteVendorFiles) {
64 | $this->doIsDeleteVendorFiles($files);
65 | }
66 |
67 | $this->deleteEmptyDirectories($files);
68 | }
69 |
70 | /** @param array $flatDependencyTree */
71 | public function cleanupVendorInstalledJson(array $flatDependencyTree, DiscoveredSymbols $discoveredSymbols): void
72 | {
73 | $installedJson = new InstalledJson(
74 | $this->config,
75 | $this->filesystem,
76 | $this->logger
77 | );
78 |
79 | if ($this->config->getTargetDirectory() !== $this->config->getVendorDirectory()
80 | && !$this->config->isDeleteVendorFiles() && !$this->config->isDeleteVendorPackages()
81 | ) {
82 | $installedJson->createAndCleanTargetDirInstalledJson($flatDependencyTree, $discoveredSymbols);
83 | } elseif ($this->config->getTargetDirectory() !== $this->config->getVendorDirectory()
84 | &&
85 | ($this->config->isDeleteVendorFiles() ||$this->config->isDeleteVendorPackages())
86 | ) {
87 | $installedJson->createAndCleanTargetDirInstalledJson($flatDependencyTree, $discoveredSymbols);
88 | $installedJson->cleanupVendorInstalledJson($flatDependencyTree, $discoveredSymbols);
89 | } elseif ($this->config->getTargetDirectory() === $this->config->getVendorDirectory()) {
90 | $installedJson->cleanupVendorInstalledJson($flatDependencyTree, $discoveredSymbols);
91 | }
92 | }
93 |
94 | /**
95 | * @throws FilesystemException
96 | */
97 | protected function deleteEmptyDirectories(array $files)
98 | {
99 | $this->logger->info('Deleting empty directories.');
100 |
101 | $sourceFiles = array_map(
102 | fn($file) => $file->getSourcePath(),
103 | $files
104 | );
105 |
106 | // Get the root folders of the moved files.
107 | $rootSourceDirectories = [];
108 | foreach ($sourceFiles as $sourceFile) {
109 | $arr = explode("/", $sourceFile, 2);
110 | $dir = $arr[0];
111 | $rootSourceDirectories[ $dir ] = $dir;
112 | }
113 | $rootSourceDirectories = array_map(
114 | function (string $path): string {
115 | return $this->config->getVendorDirectory() . $path;
116 | },
117 | array_keys($rootSourceDirectories)
118 | );
119 |
120 | foreach ($rootSourceDirectories as $rootSourceDirectory) {
121 | if (!$this->filesystem->directoryExists($rootSourceDirectory) || is_link($rootSourceDirectory)) {
122 | continue;
123 | }
124 |
125 | $dirList = $this->filesystem->listContents($rootSourceDirectory, true);
126 |
127 | $allFilePaths = array_map(
128 | fn($file) => $file->path(),
129 | $dirList->toArray()
130 | );
131 |
132 | // Sort by longest path first, so subdirectories are deleted before the parent directories are checked.
133 | usort(
134 | $allFilePaths,
135 | fn($a, $b) => count(explode('/', $b)) - count(explode('/', $a))
136 | );
137 |
138 | foreach ($allFilePaths as $filePath) {
139 | if ($this->filesystem->directoryExists($filePath)
140 | && $this->dirIsEmpty($filePath)
141 | ) {
142 | $this->logger->debug('Deleting empty directory ' . $filePath);
143 | $this->filesystem->deleteDirectory($filePath);
144 | }
145 | }
146 | }
147 |
148 | // foreach ($this->filesystem->listContents($this->getAbsoluteVendorDir()) as $dirEntry) {
149 | // if ($dirEntry->isDir() && $this->dirIsEmpty($dirEntry->path()) && !is_link($dirEntry->path())) {
150 | // $this->logger->info('Deleting empty directory ' . $dirEntry->path());
151 | // $this->filesystem->deleteDirectory($dirEntry->path());
152 | // } else {
153 | // $this->logger->debug('Skipping non-empty directory ' . $dirEntry->path());
154 | // }
155 | // }
156 | }
157 |
158 | // TODO: Move to FileSystem class.
159 | protected function dirIsEmpty(string $dir): bool
160 | {
161 | // TODO BUG this deletes directories with only symlinks inside. How does it behave with hidden files?
162 | return empty($this->filesystem->listContents($dir)->toArray());
163 | }
164 |
165 | /**
166 | * @param array $files
167 | */
168 | protected function doIsDeleteVendorPackages(array $files)
169 | {
170 | $this->logger->info('Deleting original vendor packages.');
171 |
172 | $packages = [];
173 | foreach ($files as $file) {
174 | if ($file instanceof FileWithDependency) {
175 | $packages[ $file->getDependency()->getPackageName() ] = $file->getDependency();
176 | }
177 | }
178 |
179 | /** @var ComposerPackage $package */
180 | foreach ($packages as $package) {
181 | // Normal package.
182 | if ($this->filesystem->isSubDirOf($this->config->getVendorDirectory(), $package->getPackageAbsolutePath())) {
183 | $this->logger->info('Deleting ' . $package->getPackageAbsolutePath());
184 |
185 | $this->filesystem->deleteDirectory($package->getPackageAbsolutePath());
186 | } else {
187 | // TODO: log _where_ the symlink is pointing to.
188 | $this->logger->info('Deleting symlink at ' . $package->getRelativePath());
189 |
190 | // If it's a symlink, remove the symlink in the directory
191 | $symlinkPath =
192 | rtrim(
193 | $this->config->getVendorDirectory() . $package->getRelativePath(),
194 | '/'
195 | );
196 |
197 | if (false !== strpos('WIN', PHP_OS)) {
198 | /**
199 | * `unlink()` will not work on Windows. `rmdir()` will not work if there are files in the directory.
200 | * "On windows, take care that `is_link()` returns false for Junctions."
201 | *
202 | * @see https://www.php.net/manual/en/function.is-link.php#113263
203 | * @see https://stackoverflow.com/a/18262809/336146
204 | */
205 | rmdir($symlinkPath);
206 | } else {
207 | unlink($symlinkPath);
208 | }
209 | }
210 | if ($this->dirIsEmpty(dirname($package->getPackageAbsolutePath()))) {
211 | $this->logger->info('Deleting empty directory ' . dirname($package->getPackageAbsolutePath()));
212 | $this->filesystem->deleteDirectory(dirname($package->getPackageAbsolutePath()));
213 | }
214 | }
215 | }
216 |
217 | /**
218 | * @param array $files
219 | *
220 | * @throws FilesystemException
221 | */
222 | public function doIsDeleteVendorFiles(array $files)
223 | {
224 | $this->logger->info('Deleting original vendor files.');
225 |
226 | foreach ($files as $file) {
227 | if (! $file->isDoDelete()) {
228 | $this->logger->debug('Skipping/preserving ' . $file->getSourcePath());
229 | continue;
230 | }
231 |
232 | $sourceRelativePath = $file->getSourcePath();
233 |
234 | $this->logger->info('Deleting ' . $sourceRelativePath);
235 |
236 | $this->filesystem->delete($file->getSourcePath());
237 |
238 | $file->setDidDelete(true);
239 | }
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/src/Pipeline/Copier.php:
--------------------------------------------------------------------------------
1 | files = $files;
51 | $this->config = $config;
52 | $this->logger = $logger;
53 | $this->filesystem = $filesystem;
54 | }
55 |
56 | /**
57 | * If the target dir does not exist, create it.
58 | * If it already exists, delete any files we're about to copy.
59 | *
60 | * @return void
61 | * @throws FilesystemException
62 | */
63 | public function prepareTarget(): void
64 | {
65 | if (! $this->filesystem->directoryExists($this->config->getTargetDirectory())) {
66 | $this->logger->info('Creating directory at ' . $this->config->getTargetDirectory());
67 | $this->filesystem->createDirectory($this->config->getTargetDirectory());
68 | }
69 |
70 | foreach ($this->files->getFiles() as $file) {
71 | if (!$file->isDoCopy()) {
72 | $this->logger->debug('Skipping ' . $file->getSourcePath());
73 | continue;
74 | }
75 |
76 | $targetAbsoluteFilepath = $file->getAbsoluteTargetPath();
77 |
78 | if ($this->filesystem->fileExists($targetAbsoluteFilepath)) {
79 | $this->logger->info('Deleting existing destination file at ' . $targetAbsoluteFilepath);
80 | $this->filesystem->delete($targetAbsoluteFilepath);
81 | }
82 | }
83 | }
84 |
85 | /**
86 | * @throws FilesystemException
87 | */
88 | public function copy(): void
89 | {
90 | $this->logger->notice('Copying files');
91 |
92 | /**
93 | * @var File $file
94 | */
95 | foreach ($this->files->getFiles() as $file) {
96 | if (!$file->isDoCopy()) {
97 | $this->logger->debug('Skipping ' . $file->getSourcePath());
98 | continue;
99 | }
100 |
101 | $sourceAbsoluteFilepath = $file->getSourcePath();
102 | $targetAbsolutePath = $file->getAbsoluteTargetPath();
103 |
104 | if ($this->filesystem->directoryExists($sourceAbsoluteFilepath)) {
105 | $this->logger->info(sprintf(
106 | 'Creating directory at %s',
107 | $file->getAbsoluteTargetPath()
108 | ));
109 | $this->filesystem->createDirectory($targetAbsolutePath);
110 | } else {
111 | $this->logger->info(sprintf(
112 | 'Copying file to %s',
113 | $file->getAbsoluteTargetPath()
114 | ));
115 | $this->filesystem->copy($sourceAbsoluteFilepath, $targetAbsolutePath);
116 | }
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/Pipeline/DependenciesEnumerator.php:
--------------------------------------------------------------------------------
1 | */
34 | protected array $flatDependencyTree = array();
35 |
36 | /**
37 | * Record the files autoloaders for later use in building our own autoloader.
38 | *
39 | * Package-name: [ dir1, file1, file2, ... ].
40 | *
41 | * @var array
42 | */
43 | protected array $filesAutoloaders = [];
44 |
45 | /**
46 | * @var array{}|array,classmap?:array,"psr-4":array>}> $overrideAutoload
47 | */
48 | protected array $overrideAutoload = array();
49 | protected StraussConfig $config;
50 |
51 | /**
52 | * Constructor.
53 | *
54 | * @param StraussConfig $config
55 | */
56 | public function __construct(
57 | StraussConfig $config,
58 | FileSystem $filesystem,
59 | ?LoggerInterface $logger = null
60 | ) {
61 | $this->overrideAutoload = $config->getOverrideAutoload();
62 | $this->requiredPackageNames = $config->getPackages();
63 |
64 | $this->filesystem = $filesystem;
65 | $this->config = $config;
66 |
67 | $this->setLogger($logger ?? new NullLogger());
68 | }
69 |
70 | /**
71 | * @return array Packages indexed by package name.
72 | * @throws Exception
73 | */
74 | public function getAllDependencies(): array
75 | {
76 | $this->recursiveGetAllDependencies($this->requiredPackageNames);
77 |
78 | return $this->flatDependencyTree;
79 | }
80 |
81 | /**
82 | * @param string[] $requiredPackageNames
83 | */
84 | protected function recursiveGetAllDependencies(array $requiredPackageNames): void
85 | {
86 | $requiredPackageNames = array_filter($requiredPackageNames, array( $this, 'removeVirtualPackagesFilter' ));
87 |
88 | foreach ($requiredPackageNames as $requiredPackageName) {
89 | // Avoid infinite recursion.
90 | if (isset($this->flatDependencyTree[$requiredPackageName])) {
91 | continue;
92 | }
93 |
94 | $packageComposerFile = sprintf(
95 | '%s%s/composer.json',
96 | $this->config->getVendorDirectory(),
97 | $requiredPackageName
98 | );
99 | $packageComposerFile = str_replace('mem://', '/', $packageComposerFile);
100 |
101 | $overrideAutoload = $this->overrideAutoload[ $requiredPackageName ] ?? null;
102 |
103 | if ($this->filesystem->fileExists($packageComposerFile)) {
104 | $requiredComposerPackage = ComposerPackage::fromFile($packageComposerFile, $overrideAutoload);
105 | } else {
106 | // Some packages download with NO `composer.json`! E.g. woocommerce/action-scheduler.
107 | // Some packages download to a different directory than the package name.
108 | $this->logger->debug('Could not find ' . $requiredPackageName . '\'s composer.json in vendor dir, trying composer.lock');
109 |
110 | // TODO: These (.json, .lock) should be read once and reused.
111 | $composerJsonString = $this->filesystem->read($this->config->getProjectDirectory() . 'composer.json');
112 | $composerJson = json_decode($composerJsonString, true);
113 |
114 | if (isset($composerJson['provide']) && in_array($requiredPackageName, array_keys($composerJson['provide']))) {
115 | $this->logger->info('Skipping ' . $requiredPackageName . ' as it is in the composer.json provide list');
116 | continue;
117 | }
118 |
119 | $composerLockString = $this->filesystem->read($this->config->getProjectDirectory() . 'composer.lock');
120 | $composerLock = json_decode($composerLockString, true);
121 |
122 | $requiredPackageComposerJson = null;
123 | foreach ($composerLock['packages'] as $packageJson) {
124 | if ($requiredPackageName === $packageJson['name']) {
125 | $requiredPackageComposerJson = $packageJson;
126 | break;
127 | }
128 | }
129 |
130 | if (is_null($requiredPackageComposerJson)) {
131 | // e.g. composer-plugin-api.
132 | $this->logger->info('Skipping ' . $requiredPackageName . ' as it is not in composer.lock');
133 | continue;
134 | }
135 |
136 | if (!isset($requiredPackageComposerJson['autoload'])
137 | && empty($requiredPackageComposerJson['require'])
138 | && $requiredPackageComposerJson['type'] != 'metapackage'
139 | && ! $this->filesystem->directoryExists(dirname($packageComposerFile))
140 | ) {
141 | // e.g. symfony/polyfill-php72 when installed on PHP 7.2 or later.
142 | $this->logger->info('Skipping ' . $requiredPackageName . ' as it is has no autoload key (possibly a polyfill unnecessary for this version of PHP).');
143 | continue;
144 | }
145 |
146 | $requiredComposerPackage = ComposerPackage::fromComposerJsonArray($requiredPackageComposerJson, $overrideAutoload);
147 | }
148 |
149 | $this->logger->info('Analysing package ' . $requiredComposerPackage->getPackageName());
150 | $this->flatDependencyTree[$requiredComposerPackage->getPackageName()] = $requiredComposerPackage;
151 |
152 | $nextRequiredPackageNames = $requiredComposerPackage->getRequiresNames();
153 |
154 | if (0 !== count($nextRequiredPackageNames)) {
155 | $packageRequiresString = $requiredComposerPackage->getPackageName() . ' requires packages: ';
156 | $this->logger->debug($packageRequiresString . implode(', ', $nextRequiredPackageNames));
157 | } else {
158 | $this->logger->debug($requiredComposerPackage->getPackageName() . ' requires no packages.');
159 | continue;
160 | }
161 |
162 | $newPackages = array_diff($nextRequiredPackageNames, array_keys($this->flatDependencyTree));
163 |
164 | $newPackagesString = implode(', ', $newPackages);
165 | if (!empty($newPackagesString)) {
166 | $this->logger->debug(sprintf(
167 | 'New packages: %s%s',
168 | str_repeat(' ', strlen($packageRequiresString) - strlen('New packages: ')),
169 | $newPackagesString
170 | ));
171 | } else {
172 | $this->logger->debug('No new packages.');
173 | continue;
174 | }
175 |
176 | $this->recursiveGetAllDependencies($newPackages);
177 | }
178 | }
179 |
180 | /**
181 | * Get the recorded files autoloaders.
182 | *
183 | * @return array>
184 | */
185 | public function getAllFilesAutoloaders(): array
186 | {
187 | $filesAutoloaders = array();
188 | foreach ($this->flatDependencyTree as $packageName => $composerPackage) {
189 | if (isset($composerPackage->getAutoload()['files'])) {
190 | $filesAutoloaders[$packageName] = $composerPackage->getAutoload()['files'];
191 | }
192 | }
193 | return $filesAutoloaders;
194 | }
195 |
196 | /**
197 | * Unset PHP, ext-*, ...
198 | *
199 | * @param string $requiredPackageName
200 | */
201 | protected function removeVirtualPackagesFilter(string $requiredPackageName): bool
202 | {
203 | return ! (
204 | 0 === strpos($requiredPackageName, 'ext')
205 | // E.g. `php`, `php-64bit`.
206 | || (0 === strpos($requiredPackageName, 'php') && false === strpos($requiredPackageName, '/'))
207 | || in_array($requiredPackageName, $this->virtualPackages)
208 | );
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/src/Pipeline/FileCopyScanner.php:
--------------------------------------------------------------------------------
1 | config = $config;
45 | $this->fileSystem = $filesystem;
46 |
47 | $this->setLogger($logger ?? new NullLogger());
48 | }
49 |
50 | public function scanFiles(DiscoveredFiles $files): void
51 | {
52 | /** @var FileBase $file */
53 | foreach ($files->getFiles() as $file) {
54 | $copy = true;
55 |
56 | if ($file instanceof FileWithDependency) {
57 | if (in_array($file->getDependency()->getPackageName(), $this->config->getExcludePackagesFromCopy(), true)) {
58 | $this->logger->debug("File {$file->getSourcePath()} will not be copied because {$file->getDependency()->getPackageName()} is excluded from copy.");
59 | $copy = false;
60 | }
61 | }
62 |
63 | if ($this->config->getTargetDirectory() === $this->config->getVendorDirectory()) {
64 | $this->logger->debug("The target directory is the same as the vendor directory."); // TODO: surely this should be outside the loop/class.
65 | $copy = false;
66 | }
67 |
68 | /** @var DiscoveredSymbol $symbol */
69 | foreach ($file->getDiscoveredSymbols() as $symbol) {
70 | foreach ($this->config->getExcludeNamespacesFromCopy() as $namespace) {
71 | if (in_array($file->getSourcePath(), array_keys($symbol->getSourceFiles()), true)
72 | && $symbol instanceof NamespaceSymbol
73 | && str_starts_with($symbol->getOriginalSymbol(), $namespace)
74 | ) {
75 | $this->logger->debug("File {$file->getSourcePath()} will not be copied because namespace {$namespace} is excluded from copy.");
76 | $copy = false;
77 | }
78 | }
79 | }
80 |
81 | $filePath = $file->getSourcePath();
82 | foreach ($this->config->getExcludeFilePatternsFromCopy() as $pattern) {
83 | if (1 == preg_match($pattern, $filePath)) {
84 | $this->logger->debug("File {$file->getSourcePath()} will not be copied because it matches pattern {$pattern}.");
85 | $copy = false;
86 | }
87 | }
88 |
89 | if ($copy) {
90 | $this->logger->debug("Marking file {$file->getSourcePath()} to be copied.");
91 | }
92 |
93 | $file->setDoCopy($copy);
94 |
95 | $target = $copy && $file instanceof FileWithDependency
96 | ? $this->config->getTargetDirectory() . $file->getVendorRelativePath()
97 | : $file->getSourcePath();
98 |
99 | $file->setAbsoluteTargetPath($target);
100 |
101 | $shouldDelete = $this->config->isDeleteVendorFiles() && ! $this->fileSystem->isSymlinkedFile($file);
102 | $file->setDoDelete($shouldDelete);
103 | };
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/Pipeline/FileEnumerator.php:
--------------------------------------------------------------------------------
1 |
46 | */
47 | protected array $filesAutoloaders = [];
48 |
49 | protected FileEnumeratorConfig $config;
50 |
51 | /**
52 | * Copier constructor.
53 | */
54 | public function __construct(
55 | FileEnumeratorConfig $config,
56 | FileSystem $filesystem,
57 | ?LoggerInterface $logger = null
58 | ) {
59 | $this->discoveredFiles = new DiscoveredFiles();
60 |
61 | $this->vendorDir = $config->getVendorDirectory();
62 |
63 | $this->config = $config;
64 |
65 | $this->excludeNamespaces = $config->getExcludeNamespacesFromCopy();
66 | $this->excludePackageNames = $config->getExcludePackagesFromCopy();
67 | $this->excludeFilePatterns = $config->getExcludeFilePatternsFromCopy();
68 |
69 | $this->filesystem = $filesystem;
70 |
71 | $this->logger = $logger ?? new NullLogger();
72 | }
73 |
74 | /**
75 | * Read the autoload keys of the dependencies and generate a list of the files referenced.
76 | *
77 | * Includes all files in the directories and subdirectories mentioned in the autoloaders.
78 | *
79 | * @param ComposerPackage[] $dependencies
80 | */
81 | public function compileFileListForDependencies(array $dependencies): DiscoveredFiles
82 | {
83 | foreach ($dependencies as $dependency) {
84 | if (in_array($dependency->getPackageName(), $this->excludePackageNames)) {
85 | $this->logger->info("Excluding package " . $dependency->getPackageName());
86 | continue;
87 | }
88 | $this->logger->info("Scanning for files for package " . $dependency->getPackageName());
89 |
90 | /**
91 | * Where $dependency->autoload is ~
92 | *
93 | * [ "psr-4" => [ "BrianHenryIE\Strauss" => "src" ] ]
94 | * Exclude "exclude-from-classmap"
95 | * @see https://getcomposer.org/doc/04-schema.md#exclude-files-from-classmaps
96 | */
97 | $autoloaders = array_filter($dependency->getAutoload(), function ($type) {
98 | return 'exclude-from-classmap' !== $type;
99 | }, ARRAY_FILTER_USE_KEY);
100 |
101 | foreach ($autoloaders as $type => $value) {
102 | // Might have to switch/case here.
103 |
104 | if ('files' === $type) {
105 | // TODO: This is not in use.
106 | $this->filesAutoloaders[$dependency->getRelativePath()] = $value;
107 | }
108 |
109 | foreach ($value as $namespace => $namespace_relative_paths) {
110 | if (!empty($namespace) && in_array($namespace, $this->excludeNamespaces)) {
111 | $this->logger->info("Excluding namespace " . $namespace);
112 | continue;
113 | }
114 |
115 | $namespace_relative_paths = (array) $namespace_relative_paths;
116 | // if (! is_array($namespace_relative_paths)) {
117 | // $namespace_relative_paths = array( $namespace_relative_paths );
118 | // }
119 |
120 | foreach ($namespace_relative_paths as $namespaceRelativePath) {
121 | $sourceAbsoluteDirPath = in_array($namespaceRelativePath, ['.','./'])
122 | ? $dependency->getPackageAbsolutePath()
123 | : $dependency->getPackageAbsolutePath() . $namespaceRelativePath;
124 |
125 | if ($this->filesystem->directoryExists($sourceAbsoluteDirPath)) {
126 | $fileList = $this->filesystem->listContents($sourceAbsoluteDirPath, true);
127 | $actualFileList = $fileList->toArray();
128 |
129 | foreach ($actualFileList as $foundFile) {
130 | $sourceAbsoluteFilepath = '/'. $foundFile->path();
131 | // No need to record the directory itself.
132 | if (!$this->filesystem->fileExists($sourceAbsoluteFilepath)
133 | ||
134 | $this->filesystem->directoryExists($sourceAbsoluteFilepath)
135 | ) {
136 | continue;
137 | }
138 |
139 | $this->addFileWithDependency(
140 | $dependency,
141 | $sourceAbsoluteFilepath,
142 | $type
143 | );
144 | }
145 | } else {
146 | $this->addFileWithDependency($dependency, $sourceAbsoluteDirPath, $type);
147 | }
148 | }
149 | }
150 | }
151 | }
152 |
153 | $this->discoveredFiles->sort();
154 | return $this->discoveredFiles;
155 | }
156 |
157 | /**
158 | * @param ComposerPackage $dependency
159 | * @param string $sourceAbsoluteFilepath
160 | * @param string $autoloaderType
161 | *
162 | * @throws FilesystemException
163 | * @uses \BrianHenryIE\Strauss\Files\DiscoveredFiles::add()
164 | *
165 | */
166 | protected function addFileWithDependency(
167 | ComposerPackage $dependency,
168 | string $sourceAbsoluteFilepath,
169 | string $autoloaderType
170 | ): void {
171 | $vendorRelativePath = substr(
172 | $sourceAbsoluteFilepath,
173 | strpos($sourceAbsoluteFilepath, $dependency->getRelativePath() ?: 0)
174 | );
175 |
176 | if ($vendorRelativePath === $sourceAbsoluteFilepath) {
177 | $vendorRelativePath = $dependency->getRelativePath() . str_replace($dependency->getPackageAbsolutePath(), '', $sourceAbsoluteFilepath);
178 | }
179 |
180 | $isOutsideProjectDir = 0 !== strpos($sourceAbsoluteFilepath, $this->config->getVendorDirectory());
181 |
182 | /** @var FileWithDependency $f */
183 | $f = $this->discoveredFiles->getFile($sourceAbsoluteFilepath)
184 | ?? new FileWithDependency($dependency, $vendorRelativePath, $sourceAbsoluteFilepath);
185 |
186 | $f->setAbsoluteTargetPath($this->config->getVendorDirectory() . $vendorRelativePath);
187 |
188 | $f->addAutoloader($autoloaderType);
189 | $f->setDoDelete($isOutsideProjectDir);
190 |
191 | foreach ($this->excludeFilePatterns as $excludePattern) {
192 | if (1 === preg_match($excludePattern, $vendorRelativePath)) {
193 | $f->setDoCopy(false);
194 | }
195 | }
196 |
197 | $this->discoveredFiles->add($f);
198 |
199 | $this->logger->info("Found file " . $f->getAbsoluteTargetPath());
200 | }
201 |
202 | /**
203 | * @param string[] $paths
204 | */
205 | public function compileFileListForPaths(array $paths): DiscoveredFiles
206 | {
207 | $absoluteFilePaths = $this->filesystem->findAllFilesAbsolutePaths($paths);
208 |
209 | foreach ($absoluteFilePaths as $sourceAbsolutePath) {
210 | $f = $this->discoveredFiles->getFile($sourceAbsolutePath)
211 | ?? new File($sourceAbsolutePath);
212 |
213 | $this->discoveredFiles->add($f);
214 | }
215 |
216 | $this->discoveredFiles->sort();
217 | return $this->discoveredFiles;
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/src/Pipeline/FileSymbolScanner.php:
--------------------------------------------------------------------------------
1 | discoveredSymbols = new DiscoveredSymbols();
55 | $this->excludeNamespacesFromPrefixing = $config->getExcludeNamespacesFromPrefixing();
56 |
57 | $this->filesystem = $filesystem;
58 | $this->logger = $logger ?? new NullLogger();
59 | }
60 |
61 | protected function add(DiscoveredSymbol $symbol): void
62 | {
63 | $this->discoveredSymbols->add($symbol);
64 |
65 | $level = in_array($symbol->getOriginalSymbol(), $this->loggedSymbols) ? 'debug' : 'info';
66 | $newText = in_array($symbol->getOriginalSymbol(), $this->loggedSymbols) ? '' : 'new ';
67 | $noNewText = in_array($symbol->getOriginalSymbol(), $this->loggedSymbols) ? ' ' : '';
68 |
69 | $this->loggedSymbols[] = $symbol->getOriginalSymbol();
70 |
71 | switch (get_class($symbol)) {
72 | case NamespaceSymbol::class:
73 | $this->logger->log($level, "Found {$newText}namespace: {$noNewText}" . $symbol->getOriginalSymbol());
74 | break;
75 | case ConstantSymbol::class:
76 | $this->logger->log($level, "Found {$newText}constant: {$noNewText}" . $symbol->getOriginalSymbol());
77 | break;
78 | case ClassSymbol::class:
79 | $this->logger->log($level, "Found {$newText}class. {$noNewText}" . $symbol->getOriginalSymbol());
80 | break;
81 | case FunctionSymbol::class:
82 | $this->logger->log($level, "Found {$newText}function {$noNewText}" . $symbol->getOriginalSymbol());
83 | break;
84 | default:
85 | $this->logger->log($level, "Found {$newText} " . get_class($symbol) . $noNewText . ' ' . $symbol->getOriginalSymbol());
86 | }
87 | }
88 |
89 | /**
90 | * @param DiscoveredFiles $files
91 | */
92 | public function findInFiles(DiscoveredFiles $files): DiscoveredSymbols
93 | {
94 | foreach ($files->getFiles() as $file) {
95 | if (!$file->isPhpFile()) {
96 | $this->logger->debug('Skipping non-PHP file: ' . $file->getSourcePath());
97 | continue;
98 | }
99 |
100 | $this->logger->info('Scanning file: ' . $file->getSourcePath());
101 | $this->find(
102 | $this->filesystem->read($file->getSourcePath()),
103 | $file
104 | );
105 | }
106 |
107 | return $this->discoveredSymbols;
108 | }
109 |
110 | /**
111 | * TODO: Don't use preg_replace_callback!
112 | *
113 | * @uses self::addDiscoveredNamespaceChange()
114 | * @uses self::addDiscoveredClassChange()
115 | */
116 | protected function find(string $contents, File $file): void
117 | {
118 | // If the entire file is under one namespace, all we want is the namespace.
119 | // If there were more than one namespace, it would appear as `namespace MyNamespace { ...`,
120 | // a file with only a single namespace will appear as `namespace MyNamespace;`.
121 | $singleNamespacePattern = '/
122 | ([0-9A-Za-z_\x7f-\xff\\\\]+)[\s\n]*; # Match a single namespace in the file.
125 | /x'; // # x: ignore whitespace in regex.
126 | if (1 === preg_match($singleNamespacePattern, $contents, $matches)) {
127 | $this->addDiscoveredNamespaceChange($matches['namespace'], $file);
128 |
129 | return;
130 | }
131 |
132 | if (0 < preg_match_all('/\s*define\s*\(\s*["\']([^"\']*)["\']\s*,\s*["\'][^"\']*["\']\s*\)\s*;/', $contents, $constants)) {
133 | foreach ($constants[1] as $constant) {
134 | $constantObj = new ConstantSymbol($constant, $file);
135 | $this->add($constantObj);
136 | }
137 | }
138 |
139 | // TODO traits
140 |
141 | // TODO: Is the ";" in this still correct since it's being taken care of in the regex just above?
142 | // Looks like with the preceding regex, it will never match.
143 |
144 | preg_replace_callback(
145 | '
146 | ~ # Start the pattern
147 | /\*[\s\S]*?\*/ | # Skip multiline comments
148 | \s*//.* | # Skip single line comments
149 | [\r\n]*\s*namespace\s+([a-zA-Z0-9_\x7f-\xff\\\\]+)[;{\s\n]{1}[\s\S]*?(?=namespace|$)
150 | # Look for a preceding namespace declaration,
151 | # followed by a semicolon, open curly bracket, space or new line
152 | # up until a
153 | # potential second namespace declaration or end of file.
154 | # if found, match that much before continuing the search on
155 | | # the remainder of the string.
156 | \s* # Whitespace is allowed before
157 | (?:abstract\sclass|class|interface)\s+ # Look behind for class, abstract class, interface
158 | ([a-zA-Z0-9_\x7f-\xff]+) # Match the word until the first non-classname-valid character
159 | \s? # Allow a space after
160 | (?:{|extends|implements|\n|$) # Class declaration can be followed by {, extends, implements
161 | # or a new line
162 | ~x', // # x: ignore whitespace in regex.
163 | function ($matches) use ($file) {
164 |
165 | // If we're inside a namespace other than the global namespace:
166 | if (1 === preg_match('/^\s*namespace\s+[a-zA-Z0-9_\x7f-\xff\\\\]+[;{\s\n]{1}.*/', $matches[0])) {
167 | $this->addDiscoveredNamespaceChange($matches[1], $file);
168 |
169 | return $matches[0];
170 | }
171 |
172 | if (count($matches) < 3) {
173 | return $matches[0];
174 | }
175 |
176 | // TODO: Why is this [2] and not [1] (which seems to be always empty).
177 | $this->addDiscoveredClassChange($matches[2], $file);
178 |
179 | return $matches[0];
180 | },
181 | $contents
182 | );
183 |
184 | $parser = (new ParserFactory())->createForNewestSupportedVersion();
185 | $ast = $parser->parse($contents);
186 |
187 | $traverser = new NodeTraverser();
188 | $visitor = new class extends \PhpParser\NodeVisitorAbstract {
189 | protected array $functions = [];
190 | public function enterNode(Node $node)
191 | {
192 | if ($node instanceof Node\Stmt\Function_) {
193 | $this->functions[] = $node->name->name;
194 | }
195 | return $node;
196 | }
197 |
198 | /**
199 | * @return string[] Function names.
200 | */
201 | public function getFunctions(): array
202 | {
203 | return $this->functions;
204 | }
205 | };
206 | $traverser->addVisitor($visitor);
207 |
208 | /** @var Node $node */
209 | foreach ((array) $ast as $node) {
210 | $traverser->traverse([ $node ]);
211 | }
212 | foreach ($visitor->getFunctions() as $functionName) {
213 | if (in_array($functionName, $this->getBuiltIns())) {
214 | continue;
215 | }
216 | $functionSymbol = new FunctionSymbol($functionName, $file);
217 | $this->add($functionSymbol);
218 | }
219 | }
220 |
221 | protected function addDiscoveredClassChange(string $classname, File $file): void
222 | {
223 | // TODO: This should be included but marked not to prefix.
224 | if (in_array($classname, $this->getBuiltIns())) {
225 | return;
226 | }
227 |
228 | $classSymbol = new ClassSymbol($classname, $file);
229 | $this->add($classSymbol);
230 | }
231 |
232 | protected function addDiscoveredNamespaceChange(string $namespace, File $file): void
233 | {
234 |
235 | foreach ($this->excludeNamespacesFromPrefixing as $excludeNamespace) {
236 | if (0 === strpos($namespace, $excludeNamespace)) {
237 | // TODO: Log.
238 | return;
239 | }
240 | }
241 |
242 | $namespaceObj = $this->discoveredSymbols->getNamespace($namespace);
243 | if ($namespaceObj) {
244 | $namespaceObj->addSourceFile($file);
245 | $file->addDiscoveredSymbol($namespaceObj);
246 | return;
247 | } else {
248 | $namespaceObj = new NamespaceSymbol($namespace, $file);
249 | }
250 |
251 | $this->add($namespaceObj);
252 | }
253 |
254 | /**
255 | * Get a list of PHP built-in classes etc. so they are not prefixed.
256 | *
257 | * Polyfilled classes were being prefixed, but the polyfills are only active when the PHP version is below X,
258 | * so calls to those prefixed polyfilled classnames would fail on newer PHP versions.
259 | *
260 | * NB: This list is not exhaustive. Any unloaded PHP extensions are not included.
261 | *
262 | * @see https://github.com/BrianHenryIE/strauss/issues/79
263 | *
264 | * ```
265 | * array_filter(
266 | * get_declared_classes(),
267 | * function(string $className): bool {
268 | * $reflector = new \ReflectionClass($className);
269 | * return empty($reflector->getFileName());
270 | * }
271 | * );
272 | * ```
273 | *
274 | * @return string[]
275 | */
276 | protected function getBuiltIns(): array
277 | {
278 | if (empty($this->builtIns)) {
279 | $this->loadBuiltIns();
280 | }
281 |
282 | return $this->builtIns;
283 | }
284 |
285 | /**
286 | * Load the file containing the built-in PHP classes etc. and flatten to a single array of strings and store.
287 | */
288 | protected function loadBuiltIns(): void
289 | {
290 | $builtins = include __DIR__ . '/FileSymbol/builtinsymbols.php';
291 |
292 | $flatArray = array();
293 | array_walk_recursive(
294 | $builtins,
295 | function ($array) use (&$flatArray) {
296 | if (is_array($array)) {
297 | $flatArray = array_merge($flatArray, array_values($array));
298 | } else {
299 | $flatArray[] = $array;
300 | }
301 | }
302 | );
303 |
304 | $this->builtIns = $flatArray;
305 | }
306 | }
307 |
--------------------------------------------------------------------------------
/src/Pipeline/Licenser.php:
--------------------------------------------------------------------------------
1 | dependencies = $dependencies;
71 | $this->author = $author;
72 |
73 | $this->includeModifiedDate = $config->isIncludeModifiedDate();
74 | $this->includeAuthor = $config->isIncludeAuthor();
75 |
76 | $this->filesystem = $filesystem;
77 |
78 | $this->config = $config;
79 |
80 | $this->setLogger($logger ?? new NullLogger());
81 | }
82 |
83 | /**
84 | * @throws FilesystemException
85 | */
86 | public function copyLicenses(): void
87 | {
88 | $this->findLicenseFiles();
89 |
90 | foreach ($this->getDiscoveredLicenseFiles() as $licenseFile) {
91 | $targetLicenseFile = str_replace(
92 | $this->config->getVendorDirectory(),
93 | $this->config->getTargetDirectory(),
94 | $licenseFile
95 | );
96 |
97 | $targetLicenseFileDir = dirname($targetLicenseFile);
98 |
99 | // Don't try copy it if it's already there.
100 | if ($this->filesystem->fileExists($targetLicenseFile)) {
101 | $this->logger->debug(sprintf(
102 | "Skipping %s because it already exists at %s",
103 | basename($licenseFile),
104 | $targetLicenseFile
105 | ));
106 | continue;
107 | }
108 |
109 | // Don't add licenses to non-existent directories – there were no files copied there!
110 | if (! $this->filesystem->directoryExists($targetLicenseFileDir)) {
111 | $this->logger->debug(sprintf(
112 | "Skipping %s because the directory %s does not exist",
113 | basename($licenseFile),
114 | $targetLicenseFileDir
115 | ));
116 | continue;
117 | }
118 |
119 | $this->logger->info(
120 | sprintf(
121 | "Copying license file from %s to %s",
122 | basename($licenseFile),
123 | $targetLicenseFile
124 | )
125 | );
126 | $this->filesystem->copy(
127 | $licenseFile,
128 | $targetLicenseFile
129 | );
130 | }
131 | }
132 |
133 | /**
134 | * @see https://www.phpliveregex.com/p/A5y
135 | */
136 | public function findLicenseFiles(): void
137 | {
138 | // Include all license files in the dependency path.
139 |
140 | /** @var ComposerPackage $dependency */
141 | foreach ($this->dependencies as $dependency) {
142 | $packagePath = $dependency->getPackageAbsolutePath();
143 |
144 | $files = $this->filesystem->listContents($packagePath, true)
145 | ->filter(fn (StorageAttributes $attributes) => $attributes->isFile());
146 | foreach ($files as $file) {
147 | $filePath = '/' . $file->path();
148 |
149 | // If packages happen to have their vendor dir, i.e. locally required packages, don't included the licenses
150 | // from their vendor dir (they should be included otherwise anyway).
151 | // I.e. in symlinked packages, the vendor dir might still exist.
152 | if (0 === strpos($packagePath . '/vendor', $filePath)) {
153 | continue;
154 | }
155 |
156 | if (!preg_match('/^.*licen.e[^\\/]*$/i', $filePath)) {
157 | continue;
158 | }
159 |
160 | $this->discoveredLicenseFiles[$filePath] = $dependency->getPackageName();
161 | }
162 | }
163 | }
164 | /**
165 | * @return string[]
166 | */
167 | public function getDiscoveredLicenseFiles(): array
168 | {
169 | return array_keys($this->discoveredLicenseFiles);
170 | }
171 |
172 | /**
173 | * @param array $modifiedFiles
174 | *
175 | * @throws \Exception
176 | * @throws FilesystemException
177 | */
178 | public function addInformationToUpdatedFiles(array $modifiedFiles): void
179 | {
180 | // E.g. "25-April-2021".
181 | $date = gmdate("d-F-Y", time());
182 |
183 | foreach ($modifiedFiles as $relativeFilePath => $package) {
184 | $filepath = $this->config->getTargetDirectory() . $relativeFilePath;
185 |
186 | if (!$this->filesystem->fileExists($filepath)) {
187 | continue;
188 | }
189 |
190 | $contents = $this->filesystem->read($filepath);
191 |
192 | $updatedContents = $this->addChangeDeclarationToPhpString(
193 | $contents,
194 | $date,
195 | $package->getPackageName(),
196 | $package->getLicense()
197 | );
198 |
199 | if ($updatedContents !== $contents) {
200 | $this->logger->info("Adding change declaration to {$filepath}");
201 | $this->filesystem->write($filepath, $updatedContents);
202 | }
203 | }
204 | }
205 |
206 | /**
207 | * Given a php file as a string, edit its header phpdoc, or add a header, to include:
208 | *
209 | * "Modified by {author} on {date} using Strauss.
210 | * @see https://github.com/BrianHenryIE/strauss"
211 | *
212 | * Should probably include the original license in each file since it'll often be a mix, with the parent
213 | * project often being a GPL WordPress plugin.
214 | *
215 | * Find the string between the end of php-opener and the first valid code.
216 | * First valid code will be a line whose first non-whitespace character is not / or * ?... NO!
217 | * If the first non whitespace string after php-opener is multiline-comment-opener, find the
218 | * closing multiline-comment-closer
219 | * / If there's already a comment, work within that comment
220 | * If there is no mention in the header of the license already, add it.
221 | * Add a note that changes have been made.
222 | *
223 | * @param string $phpString Code.
224 | */
225 | public function addChangeDeclarationToPhpString(
226 | string $phpString,
227 | string $modifiedDate,
228 | string $packageName,
229 | string $packageLicense
230 | ) : string {
231 |
232 | $author = $this->author;
233 |
234 | $licenseDeclaration = "@license {$packageLicense}";
235 | $modifiedDeclaration = 'Modified';
236 | if ($this->includeAuthor) {
237 | $modifiedDeclaration .= " by {$author}";
238 | }
239 | if ($this->includeModifiedDate) {
240 | $modifiedDeclaration .= " on {$modifiedDate}";
241 | }
242 | $straussLink = 'https://github.com/BrianHenryIE/strauss';
243 | $modifiedDeclaration .= " using {@see {$straussLink}}.";
244 |
245 | $startOfFileArray = [];
246 | $tokenizeString = token_get_all($phpString);
247 |
248 | foreach ($tokenizeString as $token) {
249 | if (is_array($token) && !in_array($token[1], ['namespace', '/*', ' /*'])) {
250 | $startOfFileArray[] = $token[1];
251 | $token = array_shift($tokenizeString);
252 |
253 | if (is_array($token) && stristr($token[1], 'strauss')) {
254 | return $phpString;
255 | }
256 | } elseif (!is_array($token)) {
257 | $startOfFileArray[] = $token;
258 | }
259 | }
260 | // Not in use yet (because all tests are passing) but the idea of capturing the file header and only editing
261 | // that seems more reasonable than searching the whole file.
262 | $startOfFile = implode('', $startOfFileArray);
263 |
264 | // php-open followed by some whitespace and new line until the first ...
265 | $noCommentBetweenPhpOpenAndFirstCodePattern = '~<\?php[\s\n]*[\w\\\?]+~';
266 |
267 | $multilineCommentCapturePattern = '
268 | ~ # Start the pattern
269 | (
270 | <\?php[\S\s]* # match the beginning of the files php-open and following whitespace
271 | )
272 | (
273 | \*[\S\s.]* # followed by a multiline-comment-open
274 | )
275 | (
276 | \*/ # Capture the multiline-comment-close separately
277 | )
278 | ~Ux'; // U: Non-greedy matching, x: ignore whitespace in pattern.
279 |
280 | $replaceInMultilineCommentFunction = function ($matches) use (
281 | $licenseDeclaration,
282 | $modifiedDeclaration
283 | ) {
284 | // Find the line prefix and use it, i.e. could be none, asterisk or space-asterisk.
285 | $commentLines = explode("\n", $matches[2]);
286 |
287 | if (isset($commentLines[1])&& 1 === preg_match('/^([\s\\\*]*)/', $commentLines[1], $output_array)) {
288 | $lineStart = $output_array[1];
289 | } else {
290 | $lineStart = ' * ';
291 | }
292 |
293 | $appendString = "*\n";
294 |
295 | // If the license is not already specified in the header, add it.
296 | if (false === strpos($matches[2], 'licen')) {
297 | $appendString .= "{$lineStart}{$licenseDeclaration}\n";
298 | }
299 |
300 | $appendString .= "{$lineStart}{$modifiedDeclaration}\n";
301 |
302 | $commentEnd = rtrim(rtrim($lineStart, ' '), '*').'*/';
303 |
304 | $replaceWith = $matches[1] . $matches[2] . $appendString . $commentEnd;
305 |
306 | return $replaceWith;
307 | };
308 |
309 | // If it's a simple case where there is no existing header, add the existing license.
310 | if (1 === preg_match($noCommentBetweenPhpOpenAndFirstCodePattern, $phpString)) {
311 | $modifiedComment = "/**\n * {$licenseDeclaration}\n *\n * {$modifiedDeclaration}\n */";
312 | $updatedPhpString = preg_replace('~<\?php~', " $sourceFiles */
14 | protected array $sourceFiles = [];
15 |
16 | protected string $symbol;
17 |
18 | protected string $replacement;
19 |
20 | /**
21 | * @param string $symbol The classname / namespace etc.
22 | * @param File $sourceFile The file it was discovered in.
23 | */
24 | public function __construct(string $symbol, File $sourceFile)
25 | {
26 | $this->symbol = $symbol;
27 |
28 | $this->addSourceFile($sourceFile);
29 | $sourceFile->addDiscoveredSymbol($this);
30 | }
31 |
32 | public function getOriginalSymbol(): string
33 | {
34 | return $this->symbol;
35 | }
36 |
37 | /**
38 | * @return File[]
39 | */
40 | public function getSourceFiles(): array
41 | {
42 | return $this->sourceFiles;
43 | }
44 |
45 | /**
46 | * @param File $sourceFile
47 | *
48 | * @see FileSymbolScanner
49 | */
50 | public function addSourceFile(File $sourceFile): void
51 | {
52 | $this->sourceFiles[$sourceFile->getSourcePath()] = $sourceFile;
53 | }
54 |
55 | public function getReplacement(): string
56 | {
57 | return $this->replacement ?? $this->symbol;
58 | }
59 |
60 | public function setReplacement(string $replacement): void
61 | {
62 | $this->replacement = $replacement;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Types/DiscoveredSymbols.php:
--------------------------------------------------------------------------------
1 | , T_CONST:array, T_CLASS:array}
14 | */
15 | protected array $types = [];
16 |
17 | public function __construct()
18 | {
19 | $this->types = [
20 | T_CLASS => [],
21 | T_CONST => [],
22 | T_NAMESPACE => [],
23 | T_FUNCTION => [],
24 | ];
25 | }
26 |
27 | /**
28 | * @param DiscoveredSymbol $symbol
29 | */
30 | public function add(DiscoveredSymbol $symbol): void
31 | {
32 | switch (get_class($symbol)) {
33 | case NamespaceSymbol::class:
34 | $type = T_NAMESPACE;
35 | break;
36 | case ConstantSymbol::class:
37 | $type = T_CONST;
38 | break;
39 | case ClassSymbol::class:
40 | $type = T_CLASS;
41 | break;
42 | case FunctionSymbol::class:
43 | $type = T_FUNCTION;
44 | break;
45 | default:
46 | throw new \InvalidArgumentException('Unknown symbol type: ' . get_class($symbol));
47 | }
48 | $this->types[$type][$symbol->getOriginalSymbol()] = $symbol;
49 | }
50 |
51 | /**
52 | * @return DiscoveredSymbol[]
53 | */
54 | public function getSymbols(): array
55 | {
56 | return array_merge(
57 | array_values($this->getNamespaces()),
58 | array_values($this->getClasses()),
59 | array_values($this->getConstants()),
60 | array_values($this->getDiscoveredFunctions()),
61 | );
62 | }
63 |
64 | /**
65 | * @return array
66 | */
67 | public function getConstants()
68 | {
69 | return $this->types[T_CONST];
70 | }
71 |
72 | /**
73 | * @return array
74 | */
75 | public function getNamespaces(): array
76 | {
77 | return $this->types[T_NAMESPACE];
78 | }
79 |
80 | public function getNamespace(string $namespace): ?NamespaceSymbol
81 | {
82 | return $this->types[T_NAMESPACE][$namespace] ?? null;
83 | }
84 |
85 | /**
86 | * @return array
87 | */
88 | public function getClasses(): array
89 | {
90 | return $this->types[T_CLASS];
91 | }
92 |
93 | /**
94 | * TODO: Order by longest string first. (or instead, record classnames with their namespaces)
95 | *
96 | * @return array
97 | */
98 | public function getDiscoveredNamespaces(?string $namespacePrefix = ''): array
99 | {
100 | $discoveredNamespaceReplacements = [];
101 |
102 | // When running subsequent times, try to discover the original namespaces.
103 | // This is naive: it will not work where namespace replacement patterns have been used.
104 | foreach ($this->getNamespaces() as $key => $value) {
105 | $discoveredNamespaceReplacements[ $value->getOriginalSymbol() ] = $value;
106 | }
107 |
108 | uksort($discoveredNamespaceReplacements, function ($a, $b) {
109 | return strlen($a) <=> strlen($b);
110 | });
111 |
112 | return $discoveredNamespaceReplacements;
113 | }
114 |
115 | /**
116 | * TODO: should be called getGlobalClasses?
117 | *
118 | * @return string[]
119 | */
120 | public function getDiscoveredClasses(?string $classmapPrefix = ''): array
121 | {
122 | $discoveredClasses = $this->getClasses();
123 |
124 | $discoveredClasses = array_filter(
125 | array_keys($discoveredClasses),
126 | function (string $replacement) use ($classmapPrefix) {
127 | return empty($classmapPrefix) || ! str_starts_with($replacement, $classmapPrefix);
128 | }
129 | );
130 |
131 | return $discoveredClasses;
132 | }
133 |
134 | /**
135 | * @return string[]
136 | */
137 | public function getDiscoveredConstants(?string $constantsPrefix = ''): array
138 | {
139 | $discoveredConstants = $this->getConstants();
140 | $discoveredConstants = array_filter(
141 | array_keys($discoveredConstants),
142 | function (string $replacement) use ($constantsPrefix) {
143 | return empty($constantsPrefix) || ! str_starts_with($replacement, $constantsPrefix);
144 | }
145 | );
146 |
147 | return $discoveredConstants;
148 | }
149 |
150 | public function getDiscoveredFunctions()
151 | {
152 | return $this->types[T_FUNCTION];
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/Types/FunctionSymbol.php:
--------------------------------------------------------------------------------
1 |