├── .envrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bin
└── php-scoper
├── composer.json
├── composer.lock
├── phpstan-src.neon
├── phpstan-tests.neon
├── res
├── create-scoper-phpstorm-stubs-map-patcher.php
├── create-symfony-php-services-patcher.php
├── get-scoper-phpstorm-stubs.php
└── scoper.inc.php.tpl
├── src
├── Autoload
│ ├── ComposerFileHasher.php
│ └── ScoperAutoloadGenerator.php
├── Configuration
│ ├── Configuration.php
│ ├── ConfigurationFactory.php
│ ├── ConfigurationKeys.php
│ ├── Prefix.php
│ ├── PrefixValidator.php
│ ├── RegexChecker.php
│ ├── SymbolsConfiguration.php
│ ├── SymbolsConfigurationFactory.php
│ └── Throwable
│ │ ├── InvalidConfiguration.php
│ │ ├── InvalidConfigurationFile.php
│ │ ├── InvalidConfigurationValue.php
│ │ └── UnknownConfigurationKey.php
├── Console
│ ├── Application.php
│ ├── Command
│ │ ├── AddPrefixCommand.php
│ │ ├── ChangeableDirectory.php
│ │ ├── InitCommand.php
│ │ ├── InspectCommand.php
│ │ ├── InspectSymbolCommand.php
│ │ └── SymbolType.php
│ ├── ConfigLoader.php
│ ├── ConsoleScoper.php
│ ├── File.php
│ ├── InputOption
│ │ └── PhpVersionInputOption.php
│ └── ScoperLogger.php
├── Container.php
├── NotInstantiable.php
├── Patcher
│ ├── ComposerPatcher.php
│ ├── NullPatcher.php
│ ├── Patcher.php
│ ├── PatcherChain.php
│ ├── SymfonyParentTraitPatcher.php
│ └── SymfonyPatcher.php
├── PhpParser
│ ├── Node
│ │ ├── ClassAliasFuncCall.php
│ │ ├── FullyQualifiedFactory.php
│ │ ├── NameFactory.php
│ │ └── NamedIdentifier.php
│ ├── NodeTraverser.php
│ ├── NodeVisitor
│ │ ├── AttributeAppender
│ │ │ ├── IdentifierNameAppender.php
│ │ │ └── ParentNodeAppender.php
│ │ ├── ClassAliasStmtAppender.php
│ │ ├── ClassIdentifierRecorder.php
│ │ ├── ConstStmtReplacer.php
│ │ ├── EvalPrefixer.php
│ │ ├── ExcludedFunctionExistsEnricher.php
│ │ ├── ExcludedFunctionExistsStringNodeStack.php
│ │ ├── FunctionIdentifierRecorder.php
│ │ ├── MultiConstStmtReplacer.php
│ │ ├── NameStmtPrefixer.php
│ │ ├── NamespaceStmt
│ │ │ ├── NamespaceManipulator.php
│ │ │ ├── NamespaceStmtCollection.php
│ │ │ └── NamespaceStmtPrefixer.php
│ │ ├── NewdocPrefixer.php
│ │ ├── Resolver
│ │ │ ├── IdentifierResolver.php
│ │ │ └── OriginalNameResolver.php
│ │ ├── StringScalarPrefixer.php
│ │ └── UseStmt
│ │ │ ├── UseStmtCollection.php
│ │ │ ├── UseStmtCollector.php
│ │ │ ├── UseStmtManipulator.php
│ │ │ └── UseStmtPrefixer.php
│ ├── Parser
│ │ ├── ParserFactory.php
│ │ └── StandardParserFactory.php
│ ├── Printer
│ │ ├── Printer.php
│ │ ├── PrinterFactory.php
│ │ ├── StandardPrinter.php
│ │ └── StandardPrinterFactory.php
│ ├── StringNodePrefixer.php
│ ├── TraverserFactory.php
│ ├── UnexpectedParsingScenario.php
│ └── UseStmtName.php
├── Scoper
│ ├── Composer
│ │ ├── AutoloadPrefixer.php
│ │ ├── InstalledPackagesScoper.php
│ │ └── JsonFileScoper.php
│ ├── Factory
│ │ ├── ScoperFactory.php
│ │ └── StandardScoperFactory.php
│ ├── NullScoper.php
│ ├── PatchScoper.php
│ ├── PhpScoper.php
│ ├── Scoper.php
│ ├── ScoperFactory.php
│ ├── Symfony
│ │ ├── XmlScoper.php
│ │ └── YamlScoper.php
│ └── SymfonyScoper.php
├── Symbol
│ ├── EnrichedReflector.php
│ ├── EnrichedReflectorFactory.php
│ ├── NamespaceRegistry.php
│ ├── Reflector.php
│ ├── SymbolRegistry.php
│ └── SymbolsRegistry.php
├── Throwable
│ └── Exception
│ │ ├── ParsingException.php
│ │ └── RuntimeException.php
└── functions.php
└── vendor-hotfix
└── .gitkeep
/.envrc:
--------------------------------------------------------------------------------
1 | source .composer-root-version
2 |
3 | export XDEBUG_MODE=off
4 |
5 | use nix --packages \
6 | gnumake
7 |
8 | [[ -f .envrc.local ]] && source_env .envrc.local
9 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at theo.fidry@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing
2 |
3 | The project provides a `Makefile` in which the most common commands have been
4 | registered such as fixing the coding style or running the test.
5 |
6 | ```bash
7 | # Print the list of available commands
8 | make
9 | # or
10 | make help
11 | ```
12 |
13 | This project has a certain number of unit tests. However, because it is tightly
14 | coupled to [PHP-Parser][php-parser] and since [node visitors][node-visitors]
15 | behaviour and effects depends a lot on how they are combined, you can find an
16 | extensive integration test suite for the scoping of PHP files in
17 | [PhpScoperTest][PhpScoperTest] and in peculiar `test_can_scope_valid_files()`.
18 | This tests will collect "spec files" from `_specs` or if none are found in there
19 | `specs`. Those files have the following structure:
20 |
21 | ```php
22 | [
26 | 'title' => 'Title of the specification: this is used to quickly identify what is tested/covered by this file',
27 |
28 | // Default configuration values for this file
29 | 'prefix' => 'Humbug',
30 | 'expose-global-constants' => false,
31 | 'expose-global-classes' => false,
32 | 'expose-global-functions' => false,
33 | 'expose-namespaces' => [],
34 | 'expose-constants' => [],
35 | 'expose-classes' => [],
36 | 'expose-functions' => [],
37 | 'exclude-namespaces' => [],
38 | 'exclude-constants' => [],
39 | 'exclude-classes' => [],
40 | 'exclude-functions' => [],
41 | 'expected-recorded-classes' => [],
42 | 'expected-recorded-functions' => [],
43 | ],
44 |
45 | // List of specifications
46 | [
47 | 'spec' => <<<'SPEC'
48 | This is a multiline spec description.
49 | It can also be a simple string when more readable.
50 | SPEC
51 | ,
52 |
53 | // Each configuration setting defined in "meta" can be overridden
54 | // here for this specification
55 | 'expose-global-constants' => true,
56 |
57 | // Content of the specification: this should be the content of a plain
58 | // PHP file as you can notice by the presence of the opening ` <<<'PHP'
63 | <<<'PHP'
84 |
123 |
124 |
125 | « [Back to Table of Contents](README.md#table-of-contents) »
126 |
127 |
128 | [node-visitors]: https://github.com/humbug/php-scoper/tree/master/src/PhpParser/NodeVisitor
129 | [php-parser]: https://github.com/nikic/PHP-Parser
130 | [PhpScoperTest]: tests/Scoper/PhpScoperTest.php
131 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Bernhard Schussek (pre-2017), Théo Fidry, Pádraic Brady
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | 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, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/bin/php-scoper:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | ,
10 | * Pádraic Brady
11 | *
12 | * For the full copyright and license information, please view the LICENSE
13 | * file that was distributed with this source code.
14 | */
15 |
16 | namespace Humbug\PhpScoper;
17 |
18 | use ErrorException;
19 | use Fidry\Console\Application\ApplicationRunner;
20 | use Humbug\PhpScoper\Console\Application;
21 | use Isolated\Symfony\Component\Finder\Finder as IsolatedFinder;
22 | use RuntimeException;
23 | use Symfony\Component\Finder\Finder;
24 | use const PHP_EOL;
25 | use const PHP_SAPI;
26 |
27 | if (PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg') {
28 | echo PHP_EOL.'PHP-Scoper may only be invoked from a command line'.PHP_EOL;
29 |
30 | exit(1);
31 | }
32 |
33 | set_error_handler(
34 | static function (int $code, string $message, string $file = '', int $line = -1): void {
35 | if (error_reporting() & $code) {
36 | throw new ErrorException($message, 0, $code, $file, $line);
37 | }
38 | },
39 | );
40 |
41 | $findAutoload = static function () {
42 | if (file_exists($autoload = __DIR__.'/../../../autoload.php')) {
43 | // Is installed via composer
44 | return $autoload;
45 | }
46 |
47 | if (file_exists($autoload = __DIR__.'/../vendor/autoload.php')) {
48 | // Is installed locally
49 | return $autoload;
50 | }
51 |
52 | throw new RuntimeException('Unable to find the Composer autoloader.');
53 | };
54 |
55 | $autoload = $findAutoload();
56 |
57 | require $autoload;
58 |
59 | // Exposes the finder used by PHP-Scoper PHAR to allow its usage in the configuration file.
60 | if (false === class_exists(IsolatedFinder::class)) {
61 | class_alias(Finder::class, IsolatedFinder::class);
62 | }
63 |
64 | ApplicationRunner::runApplication(Application::create());
65 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "humbug/php-scoper",
3 | "description": "Prefixes all PHP namespaces in a file or directory.",
4 | "license": "MIT",
5 | "authors": [
6 | {
7 | "name": "Bernhard Schussek",
8 | "email": "bschussek@gmail.com"
9 | },
10 | {
11 | "name": "Théo Fidry",
12 | "email": "theo.fidry@gmail.com"
13 | },
14 | {
15 | "name": "Pádraic Brady",
16 | "email": "padraic.brady@gmail.com"
17 | }
18 | ],
19 | "require": {
20 | "php": "^8.2",
21 | "fidry/console": "^0.6.10",
22 | "fidry/filesystem": "^1.1",
23 | "jetbrains/phpstorm-stubs": "^2024.1",
24 | "nikic/php-parser": "^5.0",
25 | "symfony/console": "^6.4 || ^7.0",
26 | "symfony/filesystem": "^6.4 || ^7.0",
27 | "symfony/finder": "^6.4 || ^7.0",
28 | "symfony/var-dumper": "^7.1",
29 | "thecodingmachine/safe": "^3.0"
30 | },
31 | "require-dev": {
32 | "bamarni/composer-bin-plugin": "^1.1",
33 | "ergebnis/composer-normalize": "^2.28",
34 | "fidry/makefile": "^1.0",
35 | "humbug/box": "^4.6.2",
36 | "phpspec/prophecy-phpunit": "^2.0",
37 | "phpunit/phpunit": "^10.0 || ^11.0",
38 | "symfony/yaml": "^6.4 || ^7.0"
39 | },
40 | "minimum-stability": "dev",
41 | "prefer-stable": true,
42 | "autoload": {
43 | "psr-4": {
44 | "Humbug\\PhpScoper\\": "src/"
45 | },
46 | "classmap": [
47 | "vendor-hotfix/"
48 | ],
49 | "files": [
50 | "src/functions.php"
51 | ]
52 | },
53 | "autoload-dev": {
54 | "psr-4": {
55 | "Humbug\\PhpScoper\\": "tests/"
56 | },
57 | "files": [
58 | "tests/functions.php"
59 | ]
60 | },
61 | "bin": [
62 | "bin/php-scoper"
63 | ],
64 | "config": {
65 | "allow-plugins": {
66 | "bamarni/composer-bin-plugin": true,
67 | "ergebnis/composer-normalize": true
68 | },
69 | "bin-dir": "bin",
70 | "platform": {
71 | "php": "8.2.0"
72 | },
73 | "sort-packages": true
74 | },
75 | "extra": {
76 | "bamarni-bin": {
77 | "bin-links": false,
78 | "forward-command": false
79 | },
80 | "branch-alias": {
81 | "dev-master": "1.0-dev"
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/phpstan-src.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 9
3 |
4 | ignoreErrors:
5 | - identifier: missingType.iterableValue
6 | path: 'src/Scoper/Composer/AutoloadPrefixer.php'
7 | - identifier: missingType.iterableValue
8 | message: '#\$config#'
9 | path: 'src/Configuration/ConfigurationFactory.php'
10 | - message: '#Cannot cast array\\|string to string\.#'
11 | path: 'src/Patcher/SymfonyPatcher.php'
12 | - message: '#Parameter \#1 \$nodes of method PhpParser\\NodeTraverserInterface::traverse\(\) expects array\, array\\|null given\.#'
13 | path: 'src/Scoper/PhpScoper.php'
14 | - message: '#UseStmtManipulator::getOriginalName\(\) should return#'
15 | path: 'src/PhpParser/NodeVisitor/UseStmt/UseStmtManipulator.php'
16 | - message: '#IdentifierResolver::resolveIdentifier\(\) should return#'
17 | path: 'src/PhpParser/NodeVisitor/Resolver/IdentifierResolver.php'
18 | - message: '#ParentNodeAppender::getParent\(\) should return#'
19 | path: 'src/PhpParser/NodeVisitor/AttributeAppender/ParentNodeAppender.php'
20 | - message: '#ParentNodeAppender::findParent\(\) should return#'
21 | path: 'src/PhpParser/NodeVisitor/AttributeAppender/ParentNodeAppender.php'
22 | - message: '#OriginalNameResolver::getOriginalName\(\) should return#'
23 | path: 'src/PhpParser/NodeVisitor/Resolver/OriginalNameResolver.php'
24 | - message: '#NamespaceManipulator::getOriginalName\(\) should return#'
25 | path: 'src/PhpParser/NodeVisitor/NamespaceStmt/NamespaceManipulator.php'
26 | - message: '#::concat\(\) should return .+Name but returns .+\|null#'
27 | path: 'src/PhpParser/Node/NameFactory.php'
28 | - message: '#concat\(\) should return .+FullyQualified but returns .+\|null#'
29 | path: 'src/PhpParser/Node/FullyQualifiedFactory.php'
30 | - message: '#Scoper::scope\(\) expects string\, mixed given\.#'
31 | path: 'src/Scoper/PatchScoper.php'
32 | - message: '#Scoper::scope\(\) expects string\, mixed given\.#'
33 | path: 'src/Scoper/PhpScoper.php'
34 | - message: '#Scoper::scope\(\) expects string\, mixed given\.#'
35 | path: 'src/Scoper/Symfony/XmlScoper.php'
36 | - message: '#Scoper::scope\(\) expects string\, mixed given\.#'
37 | path: 'src/Scoper/Symfony/YamlScoper.php'
38 | - message: '#Scoper::scope\(\) expects string\, mixed given\.#'
39 | path: 'src/Scoper/SymfonyScoper.php'
40 | - message: '#unserialize#'
41 | path: 'src/Symbol/SymbolsRegistry.php'
42 | - message: '#Stmt\:\:\$stmts#'
43 | path: 'src/PhpParser/NodeVisitor/ClassAliasStmtAppender.php'
44 | # Fixed in https://github.com/nikic/PHP-Parser/pull/1003
45 | - message: '#Standard constructor expects array#'
46 | path: 'src/Container.php'
47 |
--------------------------------------------------------------------------------
/phpstan-tests.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 9
3 |
4 | excludePaths:
5 | - tests/Autoload/AutoloadDumperTest.php
6 |
7 | ignoreErrors:
8 | - identifier: missingType.iterableValue
9 | - identifier: assign.propertyType
10 | - message: '#Cannot cast array\\|string to string\.#'
11 | path: 'src/Patcher/SymfonyPatcher.php'
12 | - message: '#Parameter \#1 \$nodes of method PhpParser\\NodeTraverserInterface::traverse\(\) expects array\, array\\|null given\.#'
13 | path: 'src/Scoper/PhpScoper.php'
14 | - message: '#UseStmtManipulator::getOriginalName\(\) should return#'
15 | path: 'src/PhpParser/NodeVisitor/UseStmt/UseStmtManipulator.php'
16 | - message: '#IdentifierResolver::resolveIdentifier\(\) should return#'
17 | path: 'src/PhpParser/NodeVisitor/Resolver/IdentifierResolver.php'
18 | - message: '#ParentNodeAppender::getParent\(\) should return#'
19 | path: 'src/PhpParser/NodeVisitor/AttributeAppender/ParentNodeAppender.php'
20 | - message: '#ParentNodeAppender::findParent\(\) should return#'
21 | path: 'src/PhpParser/NodeVisitor/AttributeAppender/ParentNodeAppender.php'
22 | - message: '#OriginalNameResolver::getOriginalName\(\) should return#'
23 | path: 'src/PhpParser/NodeVisitor/Resolver/OriginalNameResolver.php'
24 | - message: '#NamespaceManipulator::getOriginalName\(\) should return#'
25 | path: 'src/PhpParser/NodeVisitor/NamespaceStmt/NamespaceManipulator.php'
26 | - message: '#Anonymous function should return string but returns array#'
27 | path: 'tests/Console/Command/AddPrefixCommandIntegrationTest.php'
28 | - message: '#AddPrefixCommandIntegrationTest\:\:getNormalizeDisplay\(\) should return string but returns array#'
29 | path: 'tests/Console/Command/AddPrefixCommandIntegrationTest.php'
30 | - message: '#Property .* does not accept#'
31 | path: 'src/PhpParser/NodeVisitor/UseStmt/UseStmtCollection.php'
32 | - message: '#::concat\(\) should return .+Name but returns .+\|null#'
33 | path: 'src/PhpParser/Node/NameFactory.php'
34 | - message: '#concat\(\) should return .+FullyQualified but returns .+\|null#'
35 | path: 'src/PhpParser/Node/FullyQualifiedFactory.php'
36 | - message: '#retrieveElements\(\) should return#'
37 | path: 'src/Configuration/SymbolsConfigurationFactory.php'
38 | - message: '#Scoper::scope\(\) expects string\, mixed given\.#'
39 | path: 'src/Scoper/PatchScoper.php'
40 | - message: '#Scoper::scope\(\) expects string\, mixed given\.#'
41 | path: 'src/Scoper/PhpScoper.php'
42 | - message: '#Scoper::scope\(\) expects string\, mixed given\.#'
43 | path: 'src/Scoper/Symfony/XmlScoper.php'
44 | - message: '#Scoper::scope\(\) expects string\, mixed given\.#'
45 | path: 'src/Scoper/Symfony/YamlScoper.php'
46 | - message: '#Scoper::scope\(\) expects string\, mixed given\.#'
47 | path: 'src/Scoper/SymfonyScoper.php'
48 | - message: '#Class Isolated\\Symfony\\Component\\Finder\\Finder not found\.#'
49 | path: 'tests/Configuration/DefaultConfigurationTest.php'
50 | - message: '#Cannot access offset#'
51 | path: 'tests/AutoReview/GAE2ECollector.php'
52 | - message: '#normalizeSymbolsRegistryReference#'
53 | path: 'tests/Console/Command/AddInspectCommandIntegrationTest.php'
54 | - message: '#unserialize#'
55 | path: 'src/Symbol/SymbolsRegistry.php'
56 | - message: '#Stmt\:\:\$stmts#'
57 | path: 'src/PhpParser/NodeVisitor/ClassAliasStmtAppender.php'
58 | - message: '#PhpVersion::fromComponents#'
59 | path: 'tests/SpecFramework/SpecScenario.php'
60 | # Fixed in https://github.com/nikic/PHP-Parser/pull/1003
61 | - message: '#Standard constructor expects array#'
62 | path: 'src/Container.php'
63 |
--------------------------------------------------------------------------------
/res/create-scoper-phpstorm-stubs-map-patcher.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | // should be kept intact except for its namespace, which needs to be prefixed, as otherwise,
16 | // its autoloading would break.
17 |
18 | return static function (
19 | ?string $stubsMapPath = null,
20 | ?string $stubsMapVendorPath = null,
21 | ): Closure {
22 | $stubsMapVendorPath ??= 'vendor/jetbrains/phpstorm-stubs/PhpStormStubsMap.php';
23 | $stubsMapPath ??= __DIR__.'/../'.$stubsMapVendorPath;
24 |
25 | $stubsMapOriginalContent = file_get_contents($stubsMapPath);
26 |
27 | if (!preg_match('/class PhpStormStubsMap([\s\S]+)/', $stubsMapOriginalContent, $matches)) {
28 | throw new InvalidArgumentException('Could not capture the map original content.');
29 | }
30 |
31 | $stubsMapClassOriginalContent = $matches[1];
32 |
33 | return static function (string $filePath, string $prefix, string $contents) use (
34 | $stubsMapVendorPath,
35 | $stubsMapClassOriginalContent,
36 | ): string {
37 | if ($filePath !== $stubsMapVendorPath) {
38 | return $contents;
39 | }
40 |
41 | return preg_replace(
42 | '/class PhpStormStubsMap([\s\S]+)/',
43 | 'class PhpStormStubsMap'.$stubsMapClassOriginalContent,
44 | $contents,
45 | );
46 | };
47 | };
48 |
--------------------------------------------------------------------------------
/res/create-symfony-php-services-patcher.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | /**
16 | * Creates a patcher able to fix the paths of the Symfony PHP configuration files.
17 | *
18 | * @param string|array $filesPath
19 | */
20 | return static function (array|string $fileOrFilesPath): Closure {
21 | $filesPath = (array) $fileOrFilesPath;
22 |
23 | return static function (string $filePath, string $prefix, string $contents) use ($filesPath): string {
24 | if (!in_array($filePath, $filesPath, true)) {
25 | return $contents;
26 | }
27 |
28 | return preg_replace(
29 | '/(.*->load\((?:\n\s+)?\')(.+?\\\\)(\',.*)/',
30 | '$1'.$prefix.'\\\\$2$3',
31 | $contents,
32 | );
33 | };
34 | };
35 |
--------------------------------------------------------------------------------
/res/get-scoper-phpstorm-stubs.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | // excluded, see scoper-phpstorm-stubs-map-patcher.php for more information.
16 |
17 | $defaultSource = __DIR__.'/../vendor/jetbrains/phpstorm-stubs';
18 |
19 | return static function (?string $stubsDir = null) use ($defaultSource): array {
20 | $packageDir = $stubsDir ?? $defaultSource;
21 | $ignoredDirectories = [
22 | $packageDir.'/tests',
23 | $packageDir.'/meta',
24 | ];
25 | $files = [];
26 |
27 | $collectFiles = static function (RecursiveIteratorIterator $iterator) use (&$files, $ignoredDirectories): void {
28 | foreach ($iterator as $fileInfo) {
29 | /** @var SplFileInfo $fileInfo */
30 | if (str_starts_with($fileInfo->getFilename(), '.')
31 | || $fileInfo->isDir()
32 | || !$fileInfo->isReadable()
33 | || 'php' !== $fileInfo->getExtension()
34 | // The map needs to be excluded from "exclude-files" as otherwise its namespace cannot be corrected
35 | // via a patcher
36 | || $fileInfo->getFilename() === 'PhpStormStubsMap.php'
37 | ) {
38 | continue;
39 | }
40 |
41 | foreach ($ignoredDirectories as $ignoredDirectory) {
42 | if (str_starts_with($fileInfo->getPathname(), $ignoredDirectory)) {
43 | continue 2;
44 | }
45 | }
46 |
47 | $files[] = $fileInfo->getPathName();
48 | }
49 | };
50 |
51 | $collectFiles(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($packageDir)));
52 |
53 | return $files;
54 | };
55 |
--------------------------------------------------------------------------------
/res/scoper.inc.php.tpl:
--------------------------------------------------------------------------------
1 | $fileInfo->getPathName(),
19 | // iterator_to_array(
20 | // $finder::create()->files()->in(__DIR__),
21 | // false,
22 | // ),
23 | // );
24 | $excludedFiles = [];
25 |
26 | return [
27 | // The prefix configuration. If a non-null value is used, a random prefix
28 | // will be generated instead.
29 | //
30 | // For more see: https://github.com/humbug/php-scoper/blob/master/docs/configuration.md#prefix
31 | 'prefix' => null,
32 |
33 | // The base output directory for the prefixed files.
34 | // This will be overridden by the 'output-dir' command line option if present.
35 | 'output-dir' => null,
36 |
37 | // By default when running php-scoper add-prefix, it will prefix all relevant code found in the current working
38 | // directory. You can however define which files should be scoped by defining a collection of Finders in the
39 | // following configuration key.
40 | //
41 | // This configuration entry is completely ignored when using Box.
42 | //
43 | // For more see: https://github.com/humbug/php-scoper/blob/master/docs/configuration.md#finders-and-paths
44 | 'finders' => [
45 | /*
46 | $finder::create()->files()->in('src'),
47 | $finder::create()
48 | ->files()
49 | ->ignoreVCS(true)
50 | ->notName('/LICENSE|.*\\.md|.*\\.dist|Makefile|composer\\.json|composer\\.lock/')
51 | ->exclude([
52 | 'doc',
53 | 'test',
54 | 'test_old',
55 | 'tests',
56 | 'Tests',
57 | 'vendor-bin',
58 | ])
59 | ->in('vendor'),
60 | $finder::create()->append([
61 | 'composer.json',
62 | ]),
63 | */
64 | ],
65 |
66 | // List of excluded files, i.e. files for which the content will be left untouched.
67 | // Paths are relative to the configuration file unless if they are already absolute
68 | //
69 | // For more see: https://github.com/humbug/php-scoper/blob/master/docs/configuration.md#patchers
70 | 'exclude-files' => [
71 | // 'src/an-excluded-file.php',
72 | ...$excludedFiles,
73 | ],
74 |
75 | // PHP version (e.g. `'7.2'`) in which the PHP parser and printer will be configured into. This will affect what
76 | // level of code it will understand and how the code will be printed.
77 | // If none (or `null`) is configured, then the host version will be used.
78 | 'php-version' => null,
79 |
80 | // When scoping PHP files, there will be scenarios where some of the code being scoped indirectly references the
81 | // original namespace. These will include, for example, strings or string manipulations. PHP-Scoper has limited
82 | // support for prefixing such strings. To circumvent that, you can define patchers to manipulate the file to your
83 | // heart contents.
84 | //
85 | // For more see: https://github.com/humbug/php-scoper/blob/master/docs/configuration.md#patchers
86 | 'patchers' => [
87 | static function (string $filePath, string $prefix, string $contents): string {
88 | // Change the contents here.
89 |
90 | return $contents;
91 | },
92 | ],
93 |
94 | // List of symbols to consider internal i.e. to leave untouched.
95 | //
96 | // For more information see: https://github.com/humbug/php-scoper/blob/master/docs/configuration.md#excluded-symbols
97 | 'exclude-namespaces' => [
98 | // 'Acme\Foo' // The Acme\Foo namespace (and sub-namespaces)
99 | // '~^PHPUnit\\\\Framework$~', // The whole namespace PHPUnit\Framework (but not sub-namespaces)
100 | // '~^$~', // The root namespace only
101 | // '', // Any namespace
102 | ],
103 | 'exclude-classes' => [
104 | // 'ReflectionClassConstant',
105 | ],
106 | 'exclude-functions' => [
107 | // 'mb_str_split',
108 | ],
109 | 'exclude-constants' => [
110 | // 'STDIN',
111 | ],
112 |
113 | // List of symbols to expose.
114 | //
115 | // For more information see: https://github.com/humbug/php-scoper/blob/master/docs/configuration.md#exposed-symbols
116 | 'expose-global-constants' => true,
117 | 'expose-global-classes' => true,
118 | 'expose-global-functions' => true,
119 | 'expose-namespaces' => [
120 | // 'Acme\Foo' // The Acme\Foo namespace (and sub-namespaces)
121 | // '~^PHPUnit\\\\Framework$~', // The whole namespace PHPUnit\Framework (but not sub-namespaces)
122 | // '~^$~', // The root namespace only
123 | // '', // Any namespace
124 | ],
125 | 'expose-classes' => [],
126 | 'expose-functions' => [],
127 | 'expose-constants' => [],
128 | ];
129 |
--------------------------------------------------------------------------------
/src/Autoload/ComposerFileHasher.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Autoload;
16 |
17 | use Symfony\Component\Filesystem\Path;
18 | use function array_map;
19 | use function md5;
20 | use function preg_match;
21 | use function sprintf;
22 |
23 | final readonly class ComposerFileHasher
24 | {
25 | private const ROOT_PACKAGE_NAME = '__root__';
26 | private const PACKAGE_PATH_REGEX = '~^%s/(?[^/]+?/[^/]+?)/(?.+?)$~';
27 |
28 | /**
29 | * @param string[] $filePaths
30 | */
31 | public static function create(
32 | string $vendorDir,
33 | string $rootDir,
34 | array $filePaths,
35 | ): self {
36 | $vendorDirRelativeToRoot = Path::makeRelative($vendorDir, $rootDir);
37 |
38 | $packagePathRegex = sprintf(
39 | self::PACKAGE_PATH_REGEX,
40 | $vendorDirRelativeToRoot,
41 | );
42 |
43 | return new self(
44 | $rootDir,
45 | $filePaths,
46 | $packagePathRegex,
47 | );
48 | }
49 |
50 | /**
51 | * @param string[] $filePaths
52 | */
53 | public function __construct(
54 | private string $rootDir,
55 | private array $filePaths,
56 | private string $packagePathRegex,
57 | ) {
58 | }
59 |
60 | /**
61 | * @return string[]
62 | */
63 | public function generateHashes(): array
64 | {
65 | return array_map(
66 | $this->generateHash(...),
67 | $this->filePaths,
68 | );
69 | }
70 |
71 | /**
72 | * @see \Composer\Autoload::getFileIdentifier()
73 | */
74 | private function generateHash(string $filePath): string
75 | {
76 | $relativePath = Path::makeRelative($filePath, $this->rootDir);
77 |
78 | if (1 === preg_match($this->packagePathRegex, $relativePath, $matches)) {
79 | $vendor = $matches['vendor'];
80 | $path = $matches['path'];
81 | } else {
82 | $vendor = self::ROOT_PACKAGE_NAME;
83 | $path = $relativePath;
84 | }
85 |
86 | return md5($vendor.':'.$path);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Configuration/Configuration.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Configuration;
16 |
17 | use Humbug\PhpScoper\Configuration\Throwable\InvalidConfigurationValue;
18 | use Humbug\PhpScoper\Patcher\Patcher;
19 | use PhpParser\PhpVersion;
20 |
21 | final class Configuration
22 | {
23 | private readonly Prefix $prefix;
24 |
25 | /**
26 | * @param non-empty-string|null $path Absolute canonical path to the configuration file loaded.
27 | * @param non-empty-string|null $outputDir Absolute canonical path to the output directory.
28 | * @param non-empty-string $prefix The prefix applied.
29 | * @param array $filesWithContents Array of tuple with the
30 | * first argument being the file path and the second
31 | * its contents
32 | * @param array $excludedFilesWithContents Array of tuple
33 | * with the first argument being the file path and
34 | * the second its contents
35 | *
36 | * @throws InvalidConfigurationValue
37 | */
38 | public function __construct(
39 | private ?string $path,
40 | private ?string $outputDir,
41 | string|Prefix $prefix,
42 | private ?PhpVersion $phpVersion,
43 | private array $filesWithContents,
44 | private array $excludedFilesWithContents,
45 | private Patcher $patcher,
46 | private SymbolsConfiguration $symbolsConfiguration
47 | ) {
48 | $this->prefix = $prefix instanceof Prefix
49 | ? $prefix
50 | : new Prefix($prefix);
51 | }
52 |
53 | /**
54 | * @return non-empty-string|null Absolute canonical path
55 | */
56 | public function getPath(): ?string
57 | {
58 | return $this->path;
59 | }
60 |
61 | /**
62 | * @return non-empty-string|null Absolute canonical path
63 | */
64 | public function getOutputDir(): ?string
65 | {
66 | return $this->outputDir;
67 | }
68 |
69 | /**
70 | * @param non-empty-string $prefix
71 | *
72 | * @throws InvalidConfigurationValue
73 | */
74 | public function withPrefix(string $prefix): self
75 | {
76 | return new self(
77 | $this->path,
78 | $this->outputDir,
79 | $prefix,
80 | $this->phpVersion,
81 | $this->filesWithContents,
82 | $this->excludedFilesWithContents,
83 | $this->patcher,
84 | $this->symbolsConfiguration,
85 | );
86 | }
87 |
88 | /**
89 | * @return non-empty-string
90 | */
91 | public function getPrefix(): string
92 | {
93 | return $this->prefix->toString();
94 | }
95 |
96 | /**
97 | * @param array $filesWithContents
98 | */
99 | public function withFilesWithContents(array $filesWithContents): self
100 | {
101 | return new self(
102 | $this->path,
103 | $this->outputDir,
104 | $this->prefix,
105 | $this->phpVersion,
106 | $filesWithContents,
107 | $this->excludedFilesWithContents,
108 | $this->patcher,
109 | $this->symbolsConfiguration,
110 | );
111 | }
112 |
113 | /**
114 | * @return array
115 | */
116 | public function getFilesWithContents(): array
117 | {
118 | return $this->filesWithContents;
119 | }
120 |
121 | /**
122 | * @return array
123 | */
124 | public function getExcludedFilesWithContents(): array
125 | {
126 | return $this->excludedFilesWithContents;
127 | }
128 |
129 | public function withPatcher(Patcher $patcher): self
130 | {
131 | return new self(
132 | $this->path,
133 | $this->outputDir,
134 | $this->prefix,
135 | $this->phpVersion,
136 | $this->filesWithContents,
137 | $this->excludedFilesWithContents,
138 | $patcher,
139 | $this->symbolsConfiguration,
140 | );
141 | }
142 |
143 | public function getPatcher(): Patcher
144 | {
145 | return $this->patcher;
146 | }
147 |
148 | public function getSymbolsConfiguration(): SymbolsConfiguration
149 | {
150 | return $this->symbolsConfiguration;
151 | }
152 |
153 | public function getPhpVersion(): ?PhpVersion
154 | {
155 | return $this->phpVersion;
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/Configuration/ConfigurationKeys.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Configuration;
16 |
17 | use Humbug\PhpScoper\Configuration\Throwable\UnknownConfigurationKey;
18 | use Humbug\PhpScoper\NotInstantiable;
19 |
20 | final class ConfigurationKeys
21 | {
22 | use NotInstantiable;
23 |
24 | public const PREFIX_KEYWORD = 'prefix';
25 | public const PHP_VERSION_KEYWORD = 'php-version';
26 | public const OUTPUT_DIR_KEYWORD = 'output-dir';
27 | public const EXCLUDED_FILES_KEYWORD = 'exclude-files';
28 | public const FINDER_KEYWORD = 'finders';
29 | public const PATCHERS_KEYWORD = 'patchers';
30 |
31 | public const EXPOSE_GLOBAL_CONSTANTS_KEYWORD = 'expose-global-constants';
32 | public const EXPOSE_GLOBAL_CLASSES_KEYWORD = 'expose-global-classes';
33 | public const EXPOSE_GLOBAL_FUNCTIONS_KEYWORD = 'expose-global-functions';
34 |
35 | public const EXPOSE_NAMESPACES_KEYWORD = 'expose-namespaces';
36 | public const EXPOSE_CLASSES_SYMBOLS_KEYWORD = 'expose-classes';
37 | public const EXPOSE_FUNCTIONS_SYMBOLS_KEYWORD = 'expose-functions';
38 | public const EXPOSE_CONSTANTS_SYMBOLS_KEYWORD = 'expose-constants';
39 |
40 | public const EXCLUDE_NAMESPACES_KEYWORD = 'exclude-namespaces';
41 | public const CLASSES_INTERNAL_SYMBOLS_KEYWORD = 'exclude-classes';
42 | public const FUNCTIONS_INTERNAL_SYMBOLS_KEYWORD = 'exclude-functions';
43 | public const CONSTANTS_INTERNAL_SYMBOLS_KEYWORD = 'exclude-constants';
44 |
45 | public const KEYWORDS = [
46 | self::PREFIX_KEYWORD,
47 | self::PHP_VERSION_KEYWORD,
48 | self::OUTPUT_DIR_KEYWORD,
49 | self::EXCLUDED_FILES_KEYWORD,
50 | self::FINDER_KEYWORD,
51 | self::PATCHERS_KEYWORD,
52 | self::EXPOSE_GLOBAL_CONSTANTS_KEYWORD,
53 | self::EXPOSE_GLOBAL_CLASSES_KEYWORD,
54 | self::EXPOSE_GLOBAL_FUNCTIONS_KEYWORD,
55 | self::EXPOSE_NAMESPACES_KEYWORD,
56 | self::EXPOSE_CLASSES_SYMBOLS_KEYWORD,
57 | self::EXPOSE_FUNCTIONS_SYMBOLS_KEYWORD,
58 | self::EXPOSE_CONSTANTS_SYMBOLS_KEYWORD,
59 | self::EXCLUDE_NAMESPACES_KEYWORD,
60 | self::CLASSES_INTERNAL_SYMBOLS_KEYWORD,
61 | self::FUNCTIONS_INTERNAL_SYMBOLS_KEYWORD,
62 | self::CONSTANTS_INTERNAL_SYMBOLS_KEYWORD,
63 | ];
64 |
65 | /**
66 | * @throws UnknownConfigurationKey
67 | */
68 | public static function assertIsValidKey(string $key): void
69 | {
70 | if (!self::isValidateKey($key)) {
71 | throw UnknownConfigurationKey::forKey($key);
72 | }
73 | }
74 |
75 | public static function isValidateKey(string $key): bool
76 | {
77 | return in_array($key, self::KEYWORDS, true);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Configuration/Prefix.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Configuration;
16 |
17 | use Stringable;
18 |
19 | final readonly class Prefix implements Stringable
20 | {
21 | /**
22 | * @var non-empty-string
23 | */
24 | private string $value;
25 |
26 | public function __construct(string $prefix)
27 | {
28 | PrefixValidator::validate($prefix);
29 |
30 | $this->value = $prefix;
31 | }
32 |
33 | /**
34 | * @return non-empty-string
35 | */
36 | public function __toString(): string
37 | {
38 | return $this->value;
39 | }
40 |
41 | /**
42 | * @return non-empty-string
43 | */
44 | public function toString(): string
45 | {
46 | return (string) $this;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Configuration/PrefixValidator.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Configuration;
16 |
17 | use Humbug\PhpScoper\Configuration\Throwable\InvalidConfigurationValue;
18 | use function Safe\preg_match;
19 |
20 | final class PrefixValidator
21 | {
22 | private const PREFIX_PATTERN = '/^[\p{L}\d_\\\\]+$/u';
23 |
24 | /**
25 | * @phpstan-assert non-empty-string $prefix
26 | *
27 | * @throws InvalidConfigurationValue
28 | */
29 | public static function validate(string $prefix): void
30 | {
31 | if (1 !== preg_match(self::PREFIX_PATTERN, $prefix)) {
32 | throw InvalidConfigurationValue::forInvalidPrefixPattern($prefix);
33 | }
34 |
35 | if (preg_match('/\\\{2,}/', $prefix)) {
36 | throw InvalidConfigurationValue::forInvalidNamespaceSeparator($prefix);
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Configuration/RegexChecker.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Configuration;
16 |
17 | use function array_pop;
18 | use function count;
19 | use function explode;
20 | use function in_array;
21 | use function preg_last_error;
22 | use function preg_last_error_msg;
23 | use function preg_match as native_preg_match;
24 | use function sprintf;
25 | use function str_split;
26 | use function strlen;
27 |
28 | final class RegexChecker
29 | {
30 | // Some characters are best to not be allowed as regex delimiters in order
31 | // to not result in some fancy regexes
32 | // See https://github.com/humbug/php-scoper/issues/597
33 | private const INVALID_REGEX_DELIMITERS = [
34 | '\\',
35 | '_',
36 | ];
37 |
38 | // https://www.php.net/manual/en/reference.pcre.pattern.modifiers.php
39 | private const PATTERN_MODIFIERS = [
40 | 'i',
41 | 'm',
42 | 's',
43 | 'x',
44 | 'A',
45 | 'D',
46 | 'S',
47 | 'U',
48 | 'X',
49 | 'J',
50 | 'u',
51 | ];
52 |
53 | public function isRegexLike(string $value): bool
54 | {
55 | $valueLength = strlen($value);
56 |
57 | if ($valueLength < 2) {
58 | return false;
59 | }
60 |
61 | /** @var non-empty-string $firstCharacter */
62 | $firstCharacter = $value[0];
63 |
64 | if (!self::isValidDelimiter($firstCharacter)) {
65 | return false;
66 | }
67 |
68 | $parts = explode($firstCharacter, $value);
69 |
70 | if (count($parts) !== 3) {
71 | return false;
72 | }
73 |
74 | $lastPart = array_pop($parts);
75 |
76 | if (!self::isValidRegexFlags($lastPart)) {
77 | return false;
78 | }
79 |
80 | return true;
81 | }
82 |
83 | public function validateRegex(string $regex): ?string
84 | {
85 | if (@native_preg_match($regex, '') !== false) {
86 | return null;
87 | }
88 |
89 | return sprintf(
90 | 'Invalid regex: %s (code %s)',
91 | preg_last_error_msg(),
92 | preg_last_error(),
93 | );
94 | }
95 |
96 | private static function isValidDelimiter(string $delimiter): bool
97 | {
98 | return !in_array($delimiter, self::INVALID_REGEX_DELIMITERS, true)
99 | && native_preg_match('/^\p{L}$/u', $delimiter) === 0;
100 | }
101 |
102 | private static function isValidRegexFlags(string $value): bool
103 | {
104 | if ('' === $value) {
105 | return true;
106 | }
107 |
108 | $characters = str_split($value);
109 |
110 | foreach ($characters as $character) {
111 | if (!in_array($character, self::PATTERN_MODIFIERS, true)) {
112 | return false;
113 | }
114 | }
115 |
116 | return true;
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/Configuration/SymbolsConfiguration.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Configuration;
16 |
17 | use Humbug\PhpScoper\NotInstantiable;
18 | use Humbug\PhpScoper\Symbol\NamespaceRegistry;
19 | use Humbug\PhpScoper\Symbol\SymbolRegistry;
20 |
21 | final readonly class SymbolsConfiguration
22 | {
23 | use NotInstantiable;
24 |
25 | // To keep in sync with the default configuration set in src/scoper.inc.php.tpl
26 | public static function create(
27 | bool $exposeGlobalConstants = true,
28 | bool $exposeGlobalClasses = true,
29 | bool $exposeGlobalFunctions = true,
30 | ?NamespaceRegistry $excludedNamespaces = null,
31 | // Does not contain the list of excluded symbols which go to the
32 | // Reflector (which has no notion of namespaces)
33 | ?NamespaceRegistry $exposedNamespaces = null,
34 | ?SymbolRegistry $exposedClasses = null,
35 | ?SymbolRegistry $exposedFunctions = null,
36 | ?SymbolRegistry $exposedConstants = null,
37 | ?SymbolRegistry $excludedClasses = null,
38 | ?SymbolRegistry $excludedFunctions = null,
39 | ?SymbolRegistry $excludedConstants = null,
40 | ): self {
41 | return new self(
42 | $exposeGlobalConstants,
43 | $exposeGlobalClasses,
44 | $exposeGlobalFunctions,
45 | $excludedNamespaces ?? NamespaceRegistry::create(),
46 | $exposedNamespaces ?? NamespaceRegistry::create(),
47 | $exposedClasses ?? SymbolRegistry::create(),
48 | $exposedFunctions ?? SymbolRegistry::create(),
49 | $exposedConstants ?? SymbolRegistry::createForConstants(),
50 | $excludedClasses ?? SymbolRegistry::create(),
51 | $excludedFunctions ?? SymbolRegistry::create(),
52 | $excludedConstants ?? SymbolRegistry::createForConstants(),
53 | );
54 | }
55 |
56 | private function __construct(
57 | private bool $exposeGlobalConstants,
58 | private bool $exposeGlobalClasses,
59 | private bool $exposeGlobalFunctions,
60 | private NamespaceRegistry $excludedNamespaces,
61 | private NamespaceRegistry $exposedNamespaces,
62 | private SymbolRegistry $exposedClasses,
63 | private SymbolRegistry $exposedFunctions,
64 | private SymbolRegistry $exposedConstants,
65 | private SymbolRegistry $excludedClasses,
66 | private SymbolRegistry $excludedFunctions,
67 | private SymbolRegistry $excludedConstants,
68 | ) {
69 | }
70 |
71 | public function shouldExposeGlobalConstants(): bool
72 | {
73 | return $this->exposeGlobalConstants;
74 | }
75 |
76 | public function shouldExposeGlobalClasses(): bool
77 | {
78 | return $this->exposeGlobalClasses;
79 | }
80 |
81 | public function shouldExposeGlobalFunctions(): bool
82 | {
83 | return $this->exposeGlobalFunctions;
84 | }
85 |
86 | public function getExcludedNamespaces(): NamespaceRegistry
87 | {
88 | return $this->excludedNamespaces;
89 | }
90 |
91 | public function getExposedNamespaces(): NamespaceRegistry
92 | {
93 | return $this->exposedNamespaces;
94 | }
95 |
96 | public function getExposedClasses(): SymbolRegistry
97 | {
98 | return $this->exposedClasses;
99 | }
100 |
101 | public function getExposedFunctions(): SymbolRegistry
102 | {
103 | return $this->exposedFunctions;
104 | }
105 |
106 | public function getExposedConstants(): SymbolRegistry
107 | {
108 | return $this->exposedConstants;
109 | }
110 |
111 | public function getExcludedClasses(): SymbolRegistry
112 | {
113 | return $this->excludedClasses;
114 | }
115 |
116 | public function getExcludedFunctions(): SymbolRegistry
117 | {
118 | return $this->excludedFunctions;
119 | }
120 |
121 | public function getExcludedConstants(): SymbolRegistry
122 | {
123 | return $this->excludedConstants;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/Configuration/Throwable/InvalidConfiguration.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Configuration\Throwable;
16 |
17 | use Throwable;
18 |
19 | interface InvalidConfiguration extends Throwable
20 | {
21 | }
22 |
--------------------------------------------------------------------------------
/src/Configuration/Throwable/InvalidConfigurationFile.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Configuration\Throwable;
16 |
17 | use UnexpectedValueException;
18 | use function get_debug_type;
19 |
20 | final class InvalidConfigurationFile extends UnexpectedValueException implements InvalidConfiguration
21 | {
22 | public static function forNonAbsolutePath(string $path): self
23 | {
24 | return new self(
25 | sprintf(
26 | 'Expected the path of the configuration file to load to be an absolute path, got "%s" instead',
27 | $path,
28 | ),
29 | );
30 | }
31 |
32 | public static function forFileNotFound(string $path): self
33 | {
34 | return new self(
35 | sprintf(
36 | 'Expected the path of the configuration file to exists but the file "%s" could not be found',
37 | $path,
38 | ),
39 | );
40 | }
41 |
42 | public static function forNotAFile(string $path): self
43 | {
44 | return new self(
45 | sprintf(
46 | 'Expected the path of the configuration file to be a file but "%s" appears to be a directory.',
47 | $path,
48 | ),
49 | );
50 | }
51 |
52 | public static function forInvalidValue(mixed $config): self
53 | {
54 | return new self(
55 | sprintf(
56 | 'Expected configuration to be an array, found "%s" instead.',
57 | get_debug_type($config),
58 | ),
59 | );
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Configuration/Throwable/InvalidConfigurationValue.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Configuration\Throwable;
16 |
17 | use Exception;
18 | use Symfony\Component\Finder\Finder;
19 | use UnexpectedValueException;
20 | use function gettype;
21 | use function sprintf;
22 |
23 | final class InvalidConfigurationValue extends UnexpectedValueException implements InvalidConfiguration
24 | {
25 | public static function forInvalidPatchersType(mixed $patchers): self
26 | {
27 | return new self(
28 | sprintf(
29 | 'Expected patchers to be an array of callables, found "%s" instead.',
30 | gettype($patchers),
31 | ),
32 | );
33 | }
34 |
35 | public static function forInvalidPatcherType(int|string $index, mixed $patcher): self
36 | {
37 | return new self(
38 | sprintf(
39 | 'Expected patchers to be an array of callables, the "%s" element is not (found "%s" instead).',
40 | $index,
41 | gettype($patcher),
42 | ),
43 | );
44 | }
45 |
46 | public static function forInvalidExcludedFilesTypes(mixed $excludedFiles): self
47 | {
48 | return new self(
49 | sprintf(
50 | 'Expected excluded files to be an array of strings, found "%s" instead.',
51 | gettype($excludedFiles),
52 | ),
53 | );
54 | }
55 |
56 | public static function forInvalidExcludedFilePath(int|string $index, mixed $excludedFile): self
57 | {
58 | return new self(
59 | sprintf(
60 | 'Expected excluded files to be an array of string, the "%d" element is not (found "%s" instead).',
61 | $index,
62 | gettype($excludedFile),
63 | ),
64 | );
65 | }
66 |
67 | public static function forInvalidFinderTypes(mixed $finders): self
68 | {
69 | return new self(
70 | sprintf(
71 | 'Expected finders to be an array of "%s", found "%s" instead.',
72 | Finder::class,
73 | gettype($finders),
74 | ),
75 | );
76 | }
77 |
78 | public static function forInvalidFinderType(int|string $index, mixed $finder): self
79 | {
80 | return new self(
81 | sprintf(
82 | 'Expected finders to be an array of "%s", the "%s" element is not (found "%s" instead).',
83 | Finder::class,
84 | $index,
85 | gettype($finder),
86 | ),
87 | );
88 | }
89 |
90 | public static function forFileNotFound(string $path): self
91 | {
92 | return new self(
93 | sprintf(
94 | 'Could not find the file "%s".',
95 | $path,
96 | ),
97 | );
98 | }
99 |
100 | public static function forUnreadableFile(string $path): self
101 | {
102 | return new self(
103 | sprintf(
104 | 'Could not read the file "%s".',
105 | $path,
106 | ),
107 | );
108 | }
109 |
110 | public static function forInvalidPrefixPattern(string $prefix): self
111 | {
112 | return new self(
113 | sprintf(
114 | 'The prefix needs to be composed solely of letters, digits and backslashes (as namespace separators). Got "%s".',
115 | $prefix,
116 | ),
117 | );
118 | }
119 |
120 | public static function forInvalidPhpVersionType(mixed $phpVersion): self
121 | {
122 | return new self(
123 | sprintf(
124 | 'Expected the PHP version to be a string, got "%s" instead.',
125 | gettype($phpVersion),
126 | ),
127 | );
128 | }
129 |
130 | public static function forInvalidPhpVersion(string $stringVersion, Exception $previous): self
131 | {
132 | return new self(
133 | sprintf(
134 | 'Expected the PHP version to of the format ".", e.g. "7.2", got "%s".',
135 | $stringVersion,
136 | ),
137 | previous: $previous,
138 | );
139 | }
140 |
141 | public static function forInvalidNamespaceSeparator(string $prefix): self
142 | {
143 | return new self(
144 | sprintf(
145 | 'Invalid namespace separator sequence. Got "%s".',
146 | $prefix,
147 | ),
148 | );
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/Configuration/Throwable/UnknownConfigurationKey.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Configuration\Throwable;
16 |
17 | use UnexpectedValueException;
18 |
19 | final class UnknownConfigurationKey extends UnexpectedValueException implements InvalidConfiguration
20 | {
21 | public static function forKey(string $key): self
22 | {
23 | return new self(
24 | sprintf(
25 | 'Invalid configuration key value "%s" found.',
26 | $key,
27 | ),
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Console/Application.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Console;
16 |
17 | use Fidry\Console\Application\Application as FidryApplication;
18 | use Humbug\PhpScoper\Console\Command\AddPrefixCommand;
19 | use Humbug\PhpScoper\Console\Command\InitCommand;
20 | use Humbug\PhpScoper\Console\Command\InspectCommand;
21 | use Humbug\PhpScoper\Console\Command\InspectSymbolCommand;
22 | use Humbug\PhpScoper\Container;
23 | use PHPUnit\Framework\Attributes\CodeCoverageIgnore;
24 | use Symfony\Component\Console\Helper\FormatterHelper;
25 | use function Humbug\PhpScoper\get_php_scoper_version;
26 | use function sprintf;
27 | use function str_contains;
28 | use function trim;
29 |
30 | /**
31 | * @private
32 | */
33 | #[CodeCoverageIgnore]
34 | final readonly class Application implements FidryApplication
35 | {
36 | private const LOGO = <<<'ASCII'
37 |
38 | ____ __ ______ _____
39 | / __ \/ / / / __ \ / ___/_________ ____ ___ _____
40 | / /_/ / /_/ / /_/ / \__ \/ ___/ __ \/ __ \/ _ \/ ___/
41 | / ____/ __ / ____/ ___/ / /__/ /_/ / /_/ / __/ /
42 | /_/ /_/ /_/_/ /____/\___/\____/ .___/\___/_/
43 | /_/
44 |
45 |
46 | ASCII;
47 |
48 | private const RELEASE_DATE_PLACEHOLDER = '@release-date@';
49 |
50 | public static function create(): self
51 | {
52 | return new self(
53 | new Container(),
54 | get_php_scoper_version(),
55 | !str_contains(self::RELEASE_DATE_PLACEHOLDER, '@')
56 | ? self::RELEASE_DATE_PLACEHOLDER
57 | : '',
58 | true,
59 | true,
60 | );
61 | }
62 |
63 | public function __construct(
64 | private Container $container,
65 | private string $version,
66 | private string $releaseDate,
67 | private bool $isAutoExitEnabled,
68 | private bool $areExceptionsCaught,
69 | ) {
70 | }
71 |
72 | public function getName(): string
73 | {
74 | return 'PhpScoper';
75 | }
76 |
77 | public function getVersion(): string
78 | {
79 | return $this->version;
80 | }
81 |
82 | public function getLongVersion(): string
83 | {
84 | return trim(
85 | sprintf(
86 | '%s version %s %s',
87 | $this->getName(),
88 | $this->getVersion(),
89 | $this->releaseDate,
90 | ),
91 | );
92 | }
93 |
94 | public function getHelp(): string
95 | {
96 | return self::LOGO.$this->getLongVersion();
97 | }
98 |
99 | public function getCommands(): array
100 | {
101 | return [
102 | new AddPrefixCommand(
103 | $this->container->getFileSystem(),
104 | $this->container->getScoperFactory(),
105 | $this,
106 | $this->container->getConfigurationFactory(),
107 | ),
108 | new InspectCommand(
109 | $this->container->getFileSystem(),
110 | $this->container->getScoperFactory(),
111 | $this->container->getConfigurationFactory(),
112 | ),
113 | new InspectSymbolCommand(
114 | $this->container->getFileSystem(),
115 | $this->container->getConfigurationFactory(),
116 | $this->container->getEnrichedReflectorFactory(),
117 | ),
118 | new InitCommand(
119 | $this->container->getFileSystem(),
120 | new FormatterHelper(),
121 | ),
122 | ];
123 | }
124 |
125 | public function getDefaultCommand(): string
126 | {
127 | return 'list';
128 | }
129 |
130 | public function isAutoExitEnabled(): bool
131 | {
132 | return $this->isAutoExitEnabled;
133 | }
134 |
135 | public function areExceptionsCaught(): bool
136 | {
137 | return $this->areExceptionsCaught;
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/Console/Command/ChangeableDirectory.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Console\Command;
16 |
17 | use Fidry\Console\IO;
18 | use Humbug\PhpScoper\NotInstantiable;
19 | use InvalidArgumentException;
20 | use PHPUnit\Framework\Attributes\CodeCoverageIgnore;
21 | use Symfony\Component\Console\Exception\RuntimeException;
22 | use Symfony\Component\Console\Input\InputOption;
23 | use function chdir as native_chdir;
24 | use function file_exists;
25 | use function Safe\getcwd;
26 | use function sprintf;
27 |
28 | /**
29 | * @private
30 | */
31 | #[CodeCoverageIgnore]
32 | final class ChangeableDirectory
33 | {
34 | use NotInstantiable;
35 |
36 | private const WORKING_DIR_OPT = 'working-dir';
37 |
38 | public static function createOption(): InputOption
39 | {
40 | return new InputOption(
41 | self::WORKING_DIR_OPT,
42 | 'd',
43 | InputOption::VALUE_REQUIRED,
44 | 'If specified, use the given directory as working directory.',
45 | null,
46 | );
47 | }
48 |
49 | public static function changeWorkingDirectory(IO $io): void
50 | {
51 | $workingDir = $io->getTypedOption(self::WORKING_DIR_OPT)->asNullableString();
52 |
53 | if (null === $workingDir) {
54 | return;
55 | }
56 |
57 | if (!file_exists($workingDir)) {
58 | throw new InvalidArgumentException(
59 | sprintf(
60 | 'Could not change the working directory to "%s": directory does not exists.',
61 | $workingDir,
62 | ),
63 | );
64 | }
65 |
66 | if (!native_chdir($workingDir)) {
67 | throw new RuntimeException(
68 | sprintf(
69 | 'Failed to change the working directory to "%s" from "%s".',
70 | $workingDir,
71 | getcwd(),
72 | ),
73 | );
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Console/Command/InitCommand.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Console\Command;
16 |
17 | use Fidry\Console\Command\Command;
18 | use Fidry\Console\Command\Configuration as CommandConfiguration;
19 | use Fidry\Console\IO;
20 | use PHPUnit\Framework\Attributes\CodeCoverageIgnore;
21 | use Symfony\Component\Console\Helper\FormatterHelper;
22 | use Symfony\Component\Console\Input\InputOption;
23 | use Symfony\Component\Filesystem\Filesystem;
24 | use function file_exists;
25 | use function Safe\getcwd;
26 | use function sprintf;
27 | use const DIRECTORY_SEPARATOR;
28 |
29 | /**
30 | * @private
31 | */
32 | #[CodeCoverageIgnore]
33 | final readonly class InitCommand implements Command
34 | {
35 | private const CONFIG_FILE_OPT = 'config';
36 | private const CONFIG_FILE_TEMPLATE = __DIR__.'/../../../res/scoper.inc.php.tpl';
37 | private const CONFIG_FILE_DEFAULT = 'scoper.inc.php';
38 |
39 | public function __construct(
40 | private Filesystem $fileSystem,
41 | private FormatterHelper $formatterHelper,
42 | ) {
43 | }
44 |
45 | public function getConfiguration(): CommandConfiguration
46 | {
47 | return new CommandConfiguration(
48 | 'init',
49 | 'Generates a configuration file.',
50 | '',
51 | [],
52 | [
53 | ChangeableDirectory::createOption(),
54 | new InputOption(
55 | self::CONFIG_FILE_OPT,
56 | 'c',
57 | InputOption::VALUE_REQUIRED,
58 | sprintf(
59 | 'Configuration file. Will use "%s" if found by default.',
60 | self::CONFIG_FILE_DEFAULT,
61 | ),
62 | null,
63 | ),
64 | ],
65 | );
66 | }
67 |
68 | public function execute(IO $io): int
69 | {
70 | ChangeableDirectory::changeWorkingDirectory($io);
71 |
72 | $io->newLine();
73 | $io->writeln(
74 | $this->formatterHelper->formatSection(
75 | 'PHP-Scoper configuration generate',
76 | 'Welcome!',
77 | ),
78 | );
79 |
80 | $configFile = $this->retrieveConfig($io);
81 |
82 | if (null === $configFile) {
83 | $io->writeln('Skipping configuration file generator.');
84 |
85 | return 0;
86 | }
87 |
88 | $this->fileSystem->copy(self::CONFIG_FILE_TEMPLATE, $configFile);
89 |
90 | $io->writeln([
91 | '',
92 | sprintf(
93 | 'Generated the configuration file "%s".',
94 | $configFile,
95 | ),
96 | '',
97 | ]);
98 |
99 | return 0;
100 | }
101 |
102 | private function retrieveConfig(IO $io): ?string
103 | {
104 | $configFile = $io->getTypedOption(self::CONFIG_FILE_OPT)->asNullableNonEmptyString();
105 |
106 | $configFile = (null === $configFile)
107 | ? $this->makeAbsolutePath(self::CONFIG_FILE_DEFAULT)
108 | : $this->makeAbsolutePath($configFile);
109 |
110 | if (file_exists($configFile)) {
111 | $canDeleteFile = $io->confirm(
112 | sprintf(
113 | 'The configuration file "%s" already exists. Are you sure you want to '
114 | .'replace it?',
115 | $configFile,
116 | ),
117 | false,
118 | );
119 |
120 | if (!$canDeleteFile) {
121 | $io->writeln('Skipped file generation.');
122 |
123 | return $configFile;
124 | }
125 |
126 | $this->fileSystem->remove($configFile);
127 | } else {
128 | $createConfig = $io->confirm('No configuration file found. Do you want to create one?');
129 |
130 | if (!$createConfig) {
131 | return null;
132 | }
133 | }
134 |
135 | return $configFile;
136 | }
137 |
138 | private function makeAbsolutePath(string $path): string
139 | {
140 | if (!$this->fileSystem->isAbsolutePath($path)) {
141 | $path = getcwd().DIRECTORY_SEPARATOR.$path;
142 | }
143 |
144 | return $path;
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/Console/Command/SymbolType.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Console\Command;
16 |
17 | use function array_column;
18 |
19 | enum SymbolType: string
20 | {
21 | case CLASS_TYPE = 'class';
22 | case FUNCTION_TYPE = 'function';
23 | case CONSTANT_TYPE = 'constant';
24 | case ANY_TYPE = 'any';
25 |
26 | public const ALL = [
27 | self::CLASS_TYPE,
28 | self::FUNCTION_TYPE,
29 | self::CONSTANT_TYPE,
30 | self::ANY_TYPE,
31 | ];
32 |
33 | /**
34 | * @return list
35 | */
36 | public static function getAllSpecificTypes(): array
37 | {
38 | return [
39 | self::CLASS_TYPE,
40 | self::FUNCTION_TYPE,
41 | self::CONSTANT_TYPE,
42 | ];
43 | }
44 |
45 | /**
46 | * @return list
47 | */
48 | public static function values(): array
49 | {
50 | return array_column(
51 | self::cases(),
52 | 'value',
53 | );
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Console/File.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Console;
16 |
17 | /**
18 | * @internal
19 | */
20 | final readonly class File
21 | {
22 | public function __construct(
23 | public string $inputFilePath,
24 | public string $inputContents,
25 | public string $outputFilePath,
26 | ) {
27 | }
28 |
29 | public function withScopedContent(string $scopedContent): self
30 | {
31 | return new self(
32 | $this->inputFilePath,
33 | $scopedContent,
34 | $this->outputFilePath,
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Console/InputOption/PhpVersionInputOption.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Console\InputOption;
16 |
17 | use Fidry\Console\IO;
18 | use Humbug\PhpScoper\NotInstantiable;
19 | use PhpParser\PhpVersion;
20 | use Symfony\Component\Console\Input\InputOption;
21 |
22 | /**
23 | * @private
24 | */
25 | final class PhpVersionInputOption
26 | {
27 | use NotInstantiable;
28 |
29 | private const PHP_VERSION_OPT = 'php-version';
30 |
31 | public static function createInputOption(): InputOption
32 | {
33 | return new InputOption(
34 | self::PHP_VERSION_OPT,
35 | null,
36 | InputOption::VALUE_REQUIRED,
37 | 'PHP version in which the PHP parser and printer will be configured, e.g. "7.2", or "host" for the current PHP version.',
38 | );
39 | }
40 |
41 | public static function getPhpVersion(IO $io): ?PhpVersion
42 | {
43 | $version = $io
44 | ->getTypedOption(self::PHP_VERSION_OPT)
45 | ->asNullableString();
46 |
47 | if ('host' === $version) {
48 | return PhpVersion::getHostVersion();
49 | }
50 |
51 | return null === $version
52 | ? $version
53 | : PhpVersion::fromString($version);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Console/ScoperLogger.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Console;
16 |
17 | use Fidry\Console\Application\Application as FidryApplication;
18 | use Fidry\Console\IO;
19 | use Humbug\PhpScoper\Throwable\Exception\ParsingException;
20 | use PHPUnit\Framework\Attributes\CodeCoverageIgnore;
21 | use Symfony\Component\Console\Helper\ProgressBar;
22 | use Symfony\Component\Console\Output\NullOutput;
23 | use Symfony\Component\Console\Output\OutputInterface;
24 | use function count;
25 | use function memory_get_peak_usage;
26 | use function memory_get_usage;
27 | use function microtime;
28 | use function round;
29 | use function sprintf;
30 |
31 | /**
32 | * @private
33 | * @final
34 | */
35 | #[CodeCoverageIgnore]
36 | class ScoperLogger
37 | {
38 | private readonly float $startTime;
39 | private ProgressBar $progressBar;
40 |
41 | public function __construct(
42 | private readonly FidryApplication $application,
43 | private readonly IO $io,
44 | ) {
45 | $this->startTime = microtime(true);
46 | $this->progressBar = new ProgressBar(new NullOutput());
47 | }
48 |
49 | /**
50 | * @param string[] $paths
51 | */
52 | public function outputScopingStart(?string $prefix, array $paths): void
53 | {
54 | $this->io->writeln($this->application->getHelp());
55 |
56 | $newLine = 1;
57 |
58 | if ($this->io->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) {
59 | $this->io->section('Input');
60 |
61 | $this->io->writeln(
62 | sprintf(
63 | 'Prefix: %s',
64 | $prefix,
65 | ),
66 | );
67 |
68 | $this->io->write('Paths:');
69 |
70 | if (0 === count($paths)) {
71 | $this->io->writeln(' Loaded from config');
72 | } else {
73 | $this->io->writeln('');
74 | $this->io->listing($paths);
75 | }
76 |
77 | $this->io->section('Processing');
78 | $newLine = 0;
79 | }
80 |
81 | $this->io->newLine($newLine);
82 | }
83 |
84 | /**
85 | * Output file count message if relevant.
86 | */
87 | public function outputFileCount(int $count): void
88 | {
89 | if (OutputInterface::VERBOSITY_NORMAL === $this->io->getVerbosity()) {
90 | $this->progressBar = $this->io->createProgressBar($count);
91 | $this->progressBar->start();
92 | } elseif ($this->io->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
93 | $this->progressBar = new ProgressBar(new NullOutput());
94 | }
95 | }
96 |
97 | /**
98 | * Output scoping success message.
99 | */
100 | public function outputSuccess(string $path): void
101 | {
102 | if ($this->io->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
103 | $this->io->writeln(
104 | sprintf(
105 | ' * [OK] %s',
106 | $path,
107 | ),
108 | );
109 | }
110 |
111 | $this->progressBar->advance();
112 | }
113 |
114 | public function outputWarnOfFailure(string $path, ParsingException $exception): void
115 | {
116 | if ($this->io->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
117 | $this->io->writeln(
118 | sprintf(
119 | ' * [NO] %s',
120 | $path,
121 | ),
122 | );
123 | }
124 |
125 | if ($this->io->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE) {
126 | $this->io->writeln(
127 | sprintf(
128 | "\t".'%s: %s',
129 | $exception->getMessage(),
130 | (string) $exception->getPrevious(),
131 | ),
132 | );
133 | }
134 |
135 | $this->progressBar->advance();
136 | }
137 |
138 | public function outputScopingEnd(): void
139 | {
140 | $this->finish(false);
141 | }
142 |
143 | public function outputScopingEndWithFailure(): void
144 | {
145 | $this->finish(true);
146 | }
147 |
148 | private function finish(bool $failed): void
149 | {
150 | $this->progressBar->finish();
151 | $this->io->newLine(2);
152 |
153 | if (!$failed) {
154 | $this->io->success(
155 | sprintf(
156 | 'Successfully prefixed %d files.',
157 | $this->progressBar->getMaxSteps(),
158 | ),
159 | );
160 | }
161 |
162 | if ($this->io->getVerbosity() >= OutputInterface::VERBOSITY_NORMAL) {
163 | $this->io->comment(
164 | sprintf(
165 | 'Memory usage: %.2fMB (peak: %.2fMB), time: %.2fs',
166 | round(memory_get_usage() / 1024 / 1024, 2),
167 | round(memory_get_peak_usage() / 1024 / 1024, 2),
168 | round(microtime(true) - $this->startTime, 2),
169 | ),
170 | );
171 | }
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/Container.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper;
16 |
17 | use Humbug\PhpScoper\Configuration\ConfigurationFactory;
18 | use Humbug\PhpScoper\Configuration\RegexChecker;
19 | use Humbug\PhpScoper\Configuration\SymbolsConfigurationFactory;
20 | use Humbug\PhpScoper\PhpParser\Parser\ParserFactory;
21 | use Humbug\PhpScoper\PhpParser\Parser\StandardParserFactory;
22 | use Humbug\PhpScoper\PhpParser\Printer\Printer;
23 | use Humbug\PhpScoper\PhpParser\Printer\PrinterFactory;
24 | use Humbug\PhpScoper\PhpParser\Printer\StandardPrinter;
25 | use Humbug\PhpScoper\PhpParser\Printer\StandardPrinterFactory;
26 | use Humbug\PhpScoper\Scoper\Factory\ScoperFactory;
27 | use Humbug\PhpScoper\Scoper\Factory\StandardScoperFactory;
28 | use Humbug\PhpScoper\Symbol\EnrichedReflectorFactory;
29 | use Humbug\PhpScoper\Symbol\Reflector;
30 | use PhpParser\Parser;
31 | use PhpParser\PhpVersion;
32 | use PhpParser\PrettyPrinter\Standard;
33 | use Symfony\Component\Filesystem\Filesystem;
34 | use Webmozart\Assert\Assert;
35 |
36 | final class Container
37 | {
38 | private Filesystem $filesystem;
39 | private ConfigurationFactory $configFactory;
40 | private ParserFactory $parserFactory;
41 | private Parser $parser;
42 | private ?PhpVersion $parserPhpVersion = null;
43 | private ?PhpVersion $printerPhpVersion = null;
44 | private Reflector $reflector;
45 | private ScoperFactory $scoperFactory;
46 | private EnrichedReflectorFactory $enrichedReflectorFactory;
47 | private PrinterFactory $printerFactory;
48 | private Printer $printer;
49 |
50 | public function getFileSystem(): Filesystem
51 | {
52 | if (!isset($this->filesystem)) {
53 | $this->filesystem = new Filesystem();
54 | }
55 |
56 | return $this->filesystem;
57 | }
58 |
59 | public function getConfigurationFactory(): ConfigurationFactory
60 | {
61 | if (!isset($this->configFactory)) {
62 | $this->configFactory = new ConfigurationFactory(
63 | $this->getFileSystem(),
64 | new SymbolsConfigurationFactory(
65 | new RegexChecker(),
66 | ),
67 | );
68 | }
69 |
70 | return $this->configFactory;
71 | }
72 |
73 | public function getScoperFactory(): ScoperFactory
74 | {
75 | if (!isset($this->scoperFactory)) {
76 | $this->scoperFactory = new StandardScoperFactory(
77 | $this->getEnrichedReflectorFactory(),
78 | $this->getParserFactory(),
79 | $this->getPrinterFactory(),
80 | );
81 | }
82 |
83 | return $this->scoperFactory;
84 | }
85 |
86 | /**
87 | * @deprecated Use ::getParserFactory() instead.
88 | */
89 | public function getParser(?PhpVersion $phpVersion = null): Parser
90 | {
91 | if (!isset($this->parser)) {
92 | $this->parserPhpVersion = $phpVersion;
93 | $this->parser = $this->getParserFactory()->createParser($phpVersion);
94 | }
95 |
96 | self::checkSamePhpVersion($this->parserPhpVersion, $phpVersion);
97 |
98 | return $this->parser;
99 | }
100 |
101 | public function getParserFactory(): ParserFactory
102 | {
103 | if (!isset($this->parserFactory)) {
104 | $this->parserFactory = new StandardParserFactory();
105 | }
106 |
107 | return $this->parserFactory;
108 | }
109 |
110 | public function getReflector(): Reflector
111 | {
112 | if (!isset($this->reflector)) {
113 | $this->reflector = Reflector::createWithPhpStormStubs();
114 | }
115 |
116 | return $this->reflector;
117 | }
118 |
119 | public function getEnrichedReflectorFactory(): EnrichedReflectorFactory
120 | {
121 | if (!isset($this->enrichedReflectorFactory)) {
122 | $this->enrichedReflectorFactory = new EnrichedReflectorFactory(
123 | $this->getReflector(),
124 | );
125 | }
126 |
127 | return $this->enrichedReflectorFactory;
128 | }
129 |
130 | /**
131 | * @deprecated use ::getPrinterFactory() instead.
132 | */
133 | public function getPrinter(?PhpVersion $phpVersion = null): Printer
134 | {
135 | if (!isset($this->printer)) {
136 | $this->printerPhpVersion = $phpVersion;
137 | $this->printer = new StandardPrinter(
138 | new Standard([
139 | 'phpVersion' => $phpVersion,
140 | ]),
141 | );
142 | }
143 |
144 | self::checkSamePhpVersion($this->printerPhpVersion, $phpVersion);
145 |
146 | return $this->printer;
147 | }
148 |
149 | public function getPrinterFactory(): PrinterFactory
150 | {
151 | if (!isset($this->printerFactory)) {
152 | $this->printerFactory = new StandardPrinterFactory();
153 | }
154 |
155 | return $this->printerFactory;
156 | }
157 |
158 | private static function checkSamePhpVersion(
159 | ?PhpVersion $versionUsed,
160 | ?PhpVersion $versionRequest,
161 | ): void {
162 | $parserMessage = 'Cannot use the existing parser: its PHP version is different than the one requested.';
163 |
164 | if (null === $versionUsed) {
165 | Assert::null($versionRequest, $parserMessage);
166 | } else {
167 | Assert::notNull($versionRequest, $parserMessage);
168 | Assert::true($versionUsed->equals($versionRequest), $parserMessage);
169 | }
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/NotInstantiable.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper;
16 |
17 | /**
18 | * @private
19 | */
20 | trait NotInstantiable
21 | {
22 | private function __construct()
23 | {
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Patcher/ComposerPatcher.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Patcher;
16 |
17 | use function str_contains;
18 | use function str_replace;
19 |
20 | final class ComposerPatcher implements Patcher
21 | {
22 | private const PATHS = [
23 | 'src/Composer/Package/Loader/ArrayLoader.php',
24 | 'src/Composer/Package/Loader/RootPackageLoader.php',
25 | ];
26 |
27 | public function __invoke(string $filePath, string $prefix, string $contents): string
28 | {
29 | if (!self::isSupportedFile($filePath)) {
30 | return $contents;
31 | }
32 |
33 | return str_replace(
34 | [
35 | '\'Composer\\Package\\RootPackage\'',
36 | '\'Composer\\\\Package\\\\RootPackage\'',
37 | ' Composer\\Package\\RootPackage ',
38 |
39 | '\'Composer\\Package\\CompletePackage\'',
40 | '\'Composer\\\\Package\\\\CompletePackage\'',
41 | ' Composer\\Package\\CompletePackage ',
42 | ],
43 | [
44 | '\''.$prefix.'\\Composer\\Package\\RootPackage\'',
45 | '\''.$prefix.'\\\\Composer\\\\Package\\\\RootPackage\'',
46 | ' '.$prefix.'\\Composer\\Package\\RootPackage ',
47 |
48 | '\''.$prefix.'\\Composer\\Package\\CompletePackage\'',
49 | '\''.$prefix.'\\\\Composer\\\\Package\\\\CompletePackage\'',
50 | ' '.$prefix.'\\Composer\\Package\\CompletePackage ',
51 | ],
52 | $contents,
53 | );
54 | }
55 |
56 | private static function isSupportedFile(string $filePath): bool
57 | {
58 | foreach (self::PATHS as $path) {
59 | if (str_contains($filePath, $path)) {
60 | return true;
61 | }
62 | }
63 |
64 | return false;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Patcher/NullPatcher.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Patcher;
16 |
17 | final class NullPatcher implements Patcher
18 | {
19 | public function __invoke(string $filePath, string $prefix, string $contents): string
20 | {
21 | return $contents;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Patcher/Patcher.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Patcher;
16 |
17 | /**
18 | * @phpstan-type PatcherCallable callable(string $filePath, string $prefix, string $contents): string
19 | */
20 | interface Patcher
21 | {
22 | public function __invoke(string $filePath, string $prefix, string $contents): string;
23 | }
24 |
--------------------------------------------------------------------------------
/src/Patcher/PatcherChain.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Patcher;
16 |
17 | use function array_reduce;
18 |
19 | /**
20 | * @phpstan-import-type PatcherCallable from Patcher
21 | */
22 | final readonly class PatcherChain implements Patcher
23 | {
24 | /**
25 | * @param array $patchers
26 | */
27 | public function __construct(private array $patchers = [])
28 | {
29 | }
30 |
31 | public function __invoke(string $filePath, string $prefix, string $contents): string
32 | {
33 | return array_reduce(
34 | $this->patchers,
35 | static fn (string $contents, callable $patcher) => $patcher($filePath, $prefix, $contents),
36 | $contents,
37 | );
38 | }
39 |
40 | /**
41 | * @return array
42 | */
43 | public function getPatchers(): array
44 | {
45 | return $this->patchers;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Patcher/SymfonyParentTraitPatcher.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Patcher;
16 |
17 | use function array_key_exists;
18 | use function str_contains;
19 | use function str_replace;
20 | use function strlen;
21 |
22 | final class SymfonyParentTraitPatcher implements Patcher
23 | {
24 | private const PATHS = [
25 | 'src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/ParentTrait.php',
26 | 'symfony/dependency-injection/Loader/Configurator/Traits/ParentTrait.php',
27 | ];
28 |
29 | /**
30 | * @var array>
31 | */
32 | private array $replacement = [];
33 |
34 | public function __invoke(string $filePath, string $prefix, string $contents): string
35 | {
36 | if (!self::isSupportedFile($filePath)) {
37 | return $contents;
38 | }
39 |
40 | return str_replace(
41 | [
42 | '$definition = \substr_replace($definition, \'53\', 2, 2);',
43 | '$definition = substr_replace($definition, \'53\', 2, 2);',
44 | '$definition = \substr_replace($definition, \'Child\', 44, 0);',
45 | '$definition = substr_replace($definition, \'Child\', 44, 0);',
46 | ],
47 | $this->getReplacement($prefix),
48 | $contents,
49 | );
50 | }
51 |
52 | private static function isSupportedFile(string $filePath): bool
53 | {
54 | foreach (self::PATHS as $path) {
55 | if (str_contains($filePath, $path)) {
56 | return true;
57 | }
58 | }
59 |
60 | return false;
61 | }
62 |
63 | /**
64 | * @return list
65 | */
66 | private function getReplacement(string $prefix): array
67 | {
68 | if (!array_key_exists($prefix, $this->replacement)) {
69 | $this->replacement[$prefix] = self::generateReplacement($prefix);
70 | }
71 |
72 | return $this->replacement[$prefix];
73 | }
74 |
75 | /**
76 | * @return list
77 | */
78 | private static function generateReplacement(string $prefix): array
79 | {
80 | $prefixLength = strlen($prefix);
81 | $newDefinitionFQCNLength = 53 + $prefixLength + 1;
82 | $newShortClassNameDefinitionStartPosition = 44 + $prefixLength + 1;
83 |
84 | return [
85 | str_replace(
86 | '\'53\'',
87 | '\''.$newDefinitionFQCNLength.'\'',
88 | '$definition = \substr_replace($definition, \'53\', 2, 2);',
89 | ),
90 | str_replace(
91 | '\'53\'',
92 | '\''.$newDefinitionFQCNLength.'\'',
93 | '$definition = substr_replace($definition, \'53\', 2, 2);',
94 | ),
95 | str_replace(
96 | '44',
97 | (string) $newShortClassNameDefinitionStartPosition,
98 | '$definition = \substr_replace($definition, \'Child\', 44, 0);',
99 | ),
100 | str_replace(
101 | '44',
102 | (string) $newShortClassNameDefinitionStartPosition,
103 | '$definition = substr_replace($definition, \'Child\', 44, 0);',
104 | ),
105 | ];
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/Patcher/SymfonyPatcher.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Patcher;
16 |
17 | use function Safe\preg_replace;
18 | use function sprintf;
19 | use function str_contains;
20 |
21 | final class SymfonyPatcher implements Patcher
22 | {
23 | private const PATHS = [
24 | 'src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php',
25 | 'symfony/dependency-injection/Dumper/PhpDumper.php',
26 | ];
27 |
28 | public function __invoke(string $filePath, string $prefix, string $contents): string
29 | {
30 | if (!self::isSupportedFile($filePath)) {
31 | return $contents;
32 | }
33 |
34 | return (string) preg_replace(
35 | '/use (Symfony(\\\\(?:\\\\)?)Component\\\\.+?;)/',
36 | sprintf(
37 | 'use %s$2$1',
38 | $prefix,
39 | ),
40 | $contents,
41 | );
42 | }
43 |
44 | private static function isSupportedFile(string $filePath): bool
45 | {
46 | foreach (self::PATHS as $path) {
47 | if (str_contains($filePath, $path)) {
48 | return true;
49 | }
50 | }
51 |
52 | return false;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/PhpParser/Node/ClassAliasFuncCall.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\Node;
16 |
17 | use PhpParser\Node\Arg;
18 | use PhpParser\Node\Expr\ConstFetch;
19 | use PhpParser\Node\Expr\FuncCall;
20 | use PhpParser\Node\Name\FullyQualified;
21 | use PhpParser\Node\Scalar\String_;
22 |
23 | final class ClassAliasFuncCall extends FuncCall
24 | {
25 | public function __construct(FullyQualified $prefixedName, FullyQualified $originalName, array $attributes = [])
26 | {
27 | parent::__construct(
28 | new FullyQualified('class_alias'),
29 | [
30 | new Arg(
31 | new String_((string) $prefixedName),
32 | ),
33 | new Arg(
34 | new String_((string) $originalName),
35 | ),
36 | new Arg(
37 | new ConstFetch(
38 | new FullyQualified('false'),
39 | ),
40 | ),
41 | ],
42 | $attributes,
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/PhpParser/Node/FullyQualifiedFactory.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\Node;
16 |
17 | use Humbug\PhpScoper\NotInstantiable;
18 | use InvalidArgumentException;
19 | use PhpParser\Node\Name;
20 | use PhpParser\Node\Name\FullyQualified;
21 |
22 | /**
23 | * @phpstan-import-type Attributes from NameFactory
24 | */
25 | final class FullyQualifiedFactory
26 | {
27 | use NotInstantiable;
28 |
29 | /**
30 | * @param string|Name|string[]|null $name1
31 | * @param string|Name|string[]|null $name2
32 | * @param Attributes|null $attributes
33 | */
34 | public static function concat(
35 | array|Name|string|null $name1,
36 | array|Name|string|null $name2,
37 | ?array $attributes = null,
38 | ): FullyQualified {
39 | if (null === $name1 && null === $name2) {
40 | throw new InvalidArgumentException('Expected one of the names to not be null');
41 | }
42 |
43 | $newAttributes = NameFactory::getConcatenatedNamesAttributes($name1, $name2, $attributes);
44 |
45 | return FullyQualified::concat($name1, $name2, $newAttributes);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/PhpParser/Node/NameFactory.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\Node;
16 |
17 | use Humbug\PhpScoper\NotInstantiable;
18 | use InvalidArgumentException;
19 | use PhpParser\Node\Name;
20 |
21 | /**
22 | * @phpstan-type Attributes array
23 | */
24 | final class NameFactory
25 | {
26 | use NotInstantiable;
27 |
28 | /**
29 | * @param string|Name|string[]|null $name1
30 | * @param string|Name|string[]|null $name2
31 | * @param Attributes|null $attributes
32 | */
33 | public static function concat(
34 | array|Name|string|null $name1,
35 | array|Name|string|null $name2,
36 | ?array $attributes = null,
37 | ): Name {
38 | if (null === $name1 && null === $name2) {
39 | throw new InvalidArgumentException('Expected one of the names to not be null');
40 | }
41 |
42 | $newAttributes = self::getConcatenatedNamesAttributes($name1, $name2, $attributes);
43 |
44 | return Name::concat($name1, $name2, $newAttributes);
45 | }
46 |
47 | /**
48 | * @param string|string[]|Name|null $name1
49 | * @param string|string[]|Name|null $name2
50 | * @param Attributes|null $attributes
51 | *
52 | * @return Attributes
53 | */
54 | public static function getConcatenatedNamesAttributes(
55 | string|array|Name|null $name1,
56 | string|array|Name|null $name2,
57 | ?array $attributes = null,
58 | ): array {
59 | return match (true) {
60 | $name2 instanceof Name => $attributes ?? $name2->getAttributes(),
61 | $name1 instanceof Name => $attributes ?? $name1->getAttributes(),
62 | default => $attributes ?? [],
63 | };
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/PhpParser/Node/NamedIdentifier.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\Node;
16 |
17 | use PhpParser\Node\Identifier;
18 | use PhpParser\Node\Name;
19 |
20 | /**
21 | * Small wrapper to treat an identifier as a name node.
22 | */
23 | final class NamedIdentifier extends Name
24 | {
25 | private Identifier $originalNode;
26 |
27 | public static function create(Identifier $node): self
28 | {
29 | $instance = new self($node->name, $node->getAttributes());
30 | $instance->originalNode = $node;
31 |
32 | return $instance;
33 | }
34 |
35 | public function getOriginalNode(): Identifier
36 | {
37 | return $this->originalNode;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeTraverser.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser;
16 |
17 | use Humbug\PhpScoper\PhpParser\Node\NameFactory;
18 | use PhpParser\Node;
19 | use PhpParser\Node\Stmt;
20 | use PhpParser\Node\Stmt\Declare_;
21 | use PhpParser\Node\Stmt\GroupUse;
22 | use PhpParser\Node\Stmt\InlineHTML;
23 | use PhpParser\Node\Stmt\Namespace_;
24 | use PhpParser\Node\Stmt\Use_;
25 | use PhpParser\Node\UseItem;
26 | use PhpParser\NodeTraverserInterface;
27 | use PhpParser\NodeVisitor;
28 | use function array_map;
29 | use function array_slice;
30 | use function array_splice;
31 | use function array_values;
32 | use function count;
33 | use function current;
34 |
35 | /**
36 | * @private
37 | */
38 | final readonly class NodeTraverser implements NodeTraverserInterface
39 | {
40 | public function __construct(private NodeTraverserInterface $decoratedTraverser)
41 | {
42 | }
43 |
44 | public function addVisitor(NodeVisitor $visitor): void
45 | {
46 | $this->decoratedTraverser->addVisitor($visitor);
47 | }
48 |
49 | public function removeVisitor(NodeVisitor $visitor): void
50 | {
51 | $this->decoratedTraverser->removeVisitor($visitor);
52 | }
53 |
54 | public function traverse(array $nodes): array
55 | {
56 | $nodes = $this->wrapInNamespace($nodes);
57 | $nodes = $this->replaceGroupUseStatements($nodes);
58 |
59 | return $this->decoratedTraverser->traverse($nodes);
60 | }
61 |
62 | /**
63 | * Wrap the statements in a namespace when necessary:.
64 | *
65 | * ```php
66 | * #!/usr/bin/env php
67 | * $node) {
107 | if ($node instanceof Declare_ || $node instanceof InlineHTML) {
108 | continue;
109 | }
110 |
111 | $firstRealStatementIndex = $i;
112 | /** @var Stmt[] $realStatements */
113 | $realStatements = array_slice($nodes, $i);
114 |
115 | break;
116 | }
117 |
118 | $firstRealStatement = current($realStatements);
119 |
120 | if (false !== $firstRealStatement
121 | && !($firstRealStatement instanceof Namespace_)
122 | ) {
123 | $wrappedStatements = new Namespace_(null, $realStatements);
124 |
125 | array_splice(
126 | $nodes,
127 | $firstRealStatementIndex,
128 | count($realStatements),
129 | [$wrappedStatements],
130 | );
131 | }
132 |
133 | return $nodes;
134 | }
135 |
136 | /**
137 | * @param Node[] $nodes
138 | *
139 | * @return Node[]
140 | */
141 | private function replaceGroupUseStatements(array $nodes): array
142 | {
143 | foreach ($nodes as $node) {
144 | if (!($node instanceof Namespace_)) {
145 | continue;
146 | }
147 |
148 | $statements = $node->stmts;
149 |
150 | $newStatements = [];
151 |
152 | foreach ($statements as $statement) {
153 | if ($statement instanceof GroupUse) {
154 | $uses_ = $this->createUses_($statement);
155 |
156 | array_splice(
157 | $newStatements,
158 | count($newStatements),
159 | 0,
160 | $uses_,
161 | );
162 | } else {
163 | $newStatements[] = $statement;
164 | }
165 | }
166 |
167 | $node->stmts = $newStatements;
168 | }
169 |
170 | return $nodes;
171 | }
172 |
173 | /**
174 | * @return Use_[]
175 | */
176 | private function createUses_(GroupUse $node): array
177 | {
178 | return array_map(
179 | static fn (UseItem $use): Use_ => self::createUseNode($use, $node),
180 | $node->uses,
181 | );
182 | }
183 |
184 | private static function createUseNode(UseItem $use, GroupUse $groupUse): Use_
185 | {
186 | $newUseItem = self::prefixName($use, $groupUse);
187 |
188 | return new Use_(
189 | [$newUseItem],
190 | $groupUse->type,
191 | $groupUse->getAttributes(),
192 | );
193 | }
194 |
195 | private static function prefixName(UseItem $use, GroupUse $groupUse): UseItem
196 | {
197 | $prefixedName = NameFactory::concat(
198 | $groupUse->prefix,
199 | $use->name,
200 | );
201 |
202 | return new UseItem(
203 | $prefixedName,
204 | $use->alias,
205 | $use->type,
206 | $use->getAttributes(),
207 | );
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeVisitor/AttributeAppender/IdentifierNameAppender.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor\AttributeAppender;
16 |
17 | use Humbug\PhpScoper\PhpParser\NodeVisitor\Resolver\IdentifierResolver;
18 | use PhpParser\Node;
19 | use PhpParser\Node\Stmt\Class_;
20 | use PhpParser\Node\Stmt\Interface_;
21 | use PhpParser\NodeVisitorAbstract;
22 |
23 | /**
24 | * In some contexts we need to resolve identifiers but they can no longer be
25 | * resolved on the fly. For those, we store the resolved identifier as an
26 | * attribute.
27 | *
28 | * @see ClassAliasStmtAppender
29 | *
30 | * @private
31 | */
32 | final class IdentifierNameAppender extends NodeVisitorAbstract
33 | {
34 | public function __construct(private readonly IdentifierResolver $identifierResolver)
35 | {
36 | }
37 |
38 | public function enterNode(Node $node): ?Node
39 | {
40 | if (!($node instanceof Class_ || $node instanceof Interface_)) {
41 | return null;
42 | }
43 |
44 | $name = $node->name;
45 |
46 | if (null === $name) {
47 | return null;
48 | }
49 |
50 | $resolvedName = $this->identifierResolver->resolveIdentifier($name);
51 |
52 | $name->setAttribute('resolvedName', $resolvedName);
53 |
54 | return null;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeVisitor/AttributeAppender/ParentNodeAppender.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor\AttributeAppender;
16 |
17 | use PhpParser\Node;
18 | use PhpParser\NodeVisitorAbstract;
19 | use function array_pop;
20 | use function count;
21 |
22 | /**
23 | * Appends the parent node as an attribute to each node. This allows to have
24 | * more context in the other visitors when inspecting a node.
25 | *
26 | * @private
27 | */
28 | final class ParentNodeAppender extends NodeVisitorAbstract
29 | {
30 | private const PARENT_ATTRIBUTE = 'parent';
31 |
32 | /**
33 | * @var Node[]
34 | */
35 | private array $stack;
36 |
37 | public static function setParent(Node $node, Node $parent): void
38 | {
39 | $node->setAttribute(self::PARENT_ATTRIBUTE, $parent);
40 | }
41 |
42 | public static function hasParent(Node $node): bool
43 | {
44 | return $node->hasAttribute(self::PARENT_ATTRIBUTE);
45 | }
46 |
47 | public static function getParent(Node $node): Node
48 | {
49 | return $node->getAttribute(self::PARENT_ATTRIBUTE);
50 | }
51 |
52 | public static function findParent(Node $node): ?Node
53 | {
54 | return $node->hasAttribute(self::PARENT_ATTRIBUTE)
55 | ? $node->getAttribute(self::PARENT_ATTRIBUTE)
56 | : null;
57 | }
58 |
59 | /**
60 | * @param Node[] $nodes
61 | *
62 | * @return Node[]
63 | */
64 | public function beforeTraverse(array $nodes): array
65 | {
66 | $this->stack = [];
67 |
68 | return $nodes;
69 | }
70 |
71 | public function enterNode(Node $node): Node
72 | {
73 | if ([] !== $this->stack) {
74 | self::setParent($node, $this->stack[count($this->stack) - 1]);
75 |
76 | // In some cases, e.g. to replace a node content, we need to access
77 | // the child nodes early (i.e. before NodeVisitor::enterNode()) in
78 | // which case without the following they cannot be accessed to
79 | // with their parent node
80 | if ($node instanceof Node\Stmt\Const_) {
81 | foreach ($node->consts as $const) {
82 | self::setParent($const, $node);
83 | self::setParent($const->name, $const);
84 | }
85 | }
86 |
87 | if ($node instanceof Node\Stmt\ClassLike) {
88 | $name = $node->name;
89 |
90 | if (null !== $name) {
91 | self::setParent($name, $node);
92 | }
93 | }
94 | }
95 |
96 | $this->stack[] = $node;
97 |
98 | return $node;
99 | }
100 |
101 | public function leaveNode(Node $node): Node
102 | {
103 | array_pop($this->stack);
104 |
105 | return $node;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeVisitor/ClassAliasStmtAppender.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor;
16 |
17 | use Humbug\PhpScoper\PhpParser\Node\ClassAliasFuncCall;
18 | use Humbug\PhpScoper\PhpParser\NodeVisitor\AttributeAppender\ParentNodeAppender;
19 | use Humbug\PhpScoper\PhpParser\NodeVisitor\Resolver\IdentifierResolver;
20 | use Humbug\PhpScoper\PhpParser\UnexpectedParsingScenario;
21 | use Humbug\PhpScoper\Symbol\SymbolsRegistry;
22 | use PhpParser\Node;
23 | use PhpParser\Node\Name\FullyQualified;
24 | use PhpParser\Node\Stmt;
25 | use PhpParser\Node\Stmt\Class_;
26 | use PhpParser\Node\Stmt\Expression;
27 | use PhpParser\Node\Stmt\If_;
28 | use PhpParser\Node\Stmt\Interface_;
29 | use PhpParser\Node\Stmt\Switch_;
30 | use PhpParser\Node\Stmt\TryCatch;
31 | use PhpParser\NodeVisitorAbstract;
32 | use function array_reduce;
33 | use function in_array;
34 |
35 | /**
36 | * Appends a `class_alias` statement to the exposed classes.
37 | *
38 | * ```
39 | * namespace A;
40 | *
41 | * class Foo
42 | * {
43 | * }
44 | * ```
45 | *
46 | * =>
47 | *
48 | * ```
49 | * namespace Humbug\A;
50 | *
51 | * class Foo
52 | * {
53 | * }
54 | *
55 | * class_alias('Humbug\A\Foo', 'A\Foo', false);
56 | * ```
57 | *
58 | * @internal
59 | */
60 | final class ClassAliasStmtAppender extends NodeVisitorAbstract
61 | {
62 | public function __construct(
63 | private readonly IdentifierResolver $identifierResolver,
64 | private readonly SymbolsRegistry $symbolsRegistry,
65 | ) {
66 | }
67 |
68 | /**
69 | * @param Node[] $nodes
70 | *
71 | * @return Node[]
72 | */
73 | public function afterTraverse(array $nodes): array
74 | {
75 | $this->traverseNodes($nodes);
76 |
77 | return $nodes;
78 | }
79 |
80 | /**
81 | * @param Node[] $nodes
82 | */
83 | private function traverseNodes(array $nodes): void
84 | {
85 | foreach ($nodes as $node) {
86 | if (self::isNodeAStatementWithStatements($node)) {
87 | $this->updateStatements($node);
88 | }
89 | }
90 | }
91 |
92 | /**
93 | * @phpstan-assert-if-true Stmt $node
94 | */
95 | private static function isNodeAStatementWithStatements(Node $node): bool
96 | {
97 | return $node instanceof Stmt && in_array('stmts', $node->getSubNodeNames(), true);
98 | }
99 |
100 | /**
101 | * @template T of Stmt
102 | *
103 | * @param T|null $statement
104 | */
105 | private function updateStatements(?Stmt $statement): void
106 | {
107 | if (null === $statement || null === $statement->stmts) {
108 | return;
109 | }
110 |
111 | $statement->stmts = array_reduce(
112 | $statement->stmts,
113 | fn (array $stmts, Stmt $stmt) => $this->appendClassAliasStmtIfApplicable($stmts, $stmt),
114 | [],
115 | );
116 | }
117 |
118 | /**
119 | * @param Stmt[] $stmts
120 | *
121 | * @return Stmt[]
122 | */
123 | private function appendClassAliasStmtIfApplicable(array $stmts, Stmt $stmt): array
124 | {
125 | $stmts[] = $stmt;
126 |
127 | $isClassOrInterface = $stmt instanceof Class_ || $stmt instanceof Interface_;
128 |
129 | if ($isClassOrInterface) {
130 | return $this->appendClassAliasStmtIfNecessary($stmts, $stmt);
131 | }
132 |
133 | /** @phpstan-ignore staticMethod.alreadyNarrowedType */
134 | if (self::isNodeAStatementWithStatements($stmt)) {
135 | $this->updateStatements($stmt);
136 | }
137 |
138 | if ($stmt instanceof If_) {
139 | $this->updateStatements($stmt->else);
140 | $this->traverseNodes($stmt->elseifs);
141 | } elseif ($stmt instanceof Switch_) {
142 | $this->traverseNodes($stmt->cases);
143 | } elseif ($stmt instanceof TryCatch) {
144 | $this->traverseNodes($stmt->catches);
145 | $this->updateStatements($stmt->finally);
146 | }
147 |
148 | return $stmts;
149 | }
150 |
151 | /**
152 | * @param Stmt[] $stmts
153 | *
154 | * @return Stmt[]
155 | */
156 | private function appendClassAliasStmtIfNecessary(array $stmts, Class_|Interface_ $stmt): array
157 | {
158 | $name = $stmt->name;
159 |
160 | if (null === $name) {
161 | throw UnexpectedParsingScenario::create();
162 | }
163 |
164 | $resolvedName = $this->identifierResolver->resolveIdentifier($name);
165 |
166 | if (!($resolvedName instanceof FullyQualified)) {
167 | return $stmts;
168 | }
169 |
170 | $record = $this->symbolsRegistry->getRecordedClass((string) $resolvedName);
171 |
172 | if (null !== $record) {
173 | $stmts[] = self::createAliasStmt($record[0], $record[1], $stmt);
174 | }
175 |
176 | return $stmts;
177 | }
178 |
179 | private static function createAliasStmt(
180 | string $originalName,
181 | string $prefixedName,
182 | Node $stmt
183 | ): Expression {
184 | $call = new ClassAliasFuncCall(
185 | new FullyQualified($prefixedName),
186 | new FullyQualified($originalName),
187 | $stmt->getAttributes(),
188 | );
189 |
190 | $expression = new Expression(
191 | $call,
192 | $stmt->getAttributes(),
193 | );
194 |
195 | ParentNodeAppender::setParent($call, $expression);
196 |
197 | return $expression;
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeVisitor/ClassIdentifierRecorder.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor;
16 |
17 | use Humbug\PhpScoper\PhpParser\Node\FullyQualifiedFactory;
18 | use Humbug\PhpScoper\PhpParser\NodeVisitor\AttributeAppender\ParentNodeAppender;
19 | use Humbug\PhpScoper\PhpParser\NodeVisitor\Resolver\IdentifierResolver;
20 | use Humbug\PhpScoper\PhpParser\UnexpectedParsingScenario;
21 | use Humbug\PhpScoper\Symbol\EnrichedReflector;
22 | use Humbug\PhpScoper\Symbol\SymbolsRegistry;
23 | use PhpParser\Node;
24 | use PhpParser\Node\Identifier;
25 | use PhpParser\Node\Name\FullyQualified;
26 | use PhpParser\Node\Stmt\Class_;
27 | use PhpParser\Node\Stmt\Interface_;
28 | use PhpParser\NodeVisitorAbstract;
29 |
30 | /**
31 | * Records the classes that need to be aliased.
32 | *
33 | * @private
34 | */
35 | final class ClassIdentifierRecorder extends NodeVisitorAbstract
36 | {
37 | public function __construct(
38 | private readonly string $prefix,
39 | private readonly IdentifierResolver $identifierResolver,
40 | private readonly SymbolsRegistry $symbolsRegistry,
41 | private readonly EnrichedReflector $enrichedReflector,
42 | ) {
43 | }
44 |
45 | public function enterNode(Node $node): Node
46 | {
47 | if (!($node instanceof Identifier) || !ParentNodeAppender::hasParent($node)) {
48 | return $node;
49 | }
50 |
51 | $parent = ParentNodeAppender::getParent($node);
52 |
53 | $isClassOrInterface = $parent instanceof Class_ || $parent instanceof Interface_;
54 |
55 | if (!$isClassOrInterface) {
56 | return $node;
57 | }
58 |
59 | if (null === $parent->name) {
60 | throw UnexpectedParsingScenario::create();
61 | }
62 |
63 | $resolvedName = $this->identifierResolver->resolveIdentifier($node);
64 |
65 | if (!($resolvedName instanceof FullyQualified)) {
66 | throw UnexpectedParsingScenario::create();
67 | }
68 |
69 | if ($this->shouldBeAliased($resolvedName->toString())) {
70 | $this->symbolsRegistry->recordClass(
71 | $resolvedName,
72 | FullyQualifiedFactory::concat($this->prefix, $resolvedName),
73 | );
74 | }
75 |
76 | return $node;
77 | }
78 |
79 | private function shouldBeAliased(string $resolvedName): bool
80 | {
81 | if ($this->enrichedReflector->isExposedClass($resolvedName)) {
82 | return true;
83 | }
84 |
85 | // Excluded global classes (for which we found a declaration) need to be
86 | // aliased since otherwise any usage will not point to the prefixed
87 | // version (since it's an alias) but the declaration will now declare
88 | // a prefixed version (due to the namespace).
89 | return $this->enrichedReflector->belongsToGlobalNamespace($resolvedName)
90 | && $this->enrichedReflector->isClassExcluded($resolvedName);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeVisitor/ConstStmtReplacer.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor;
16 |
17 | use Humbug\PhpScoper\PhpParser\NodeVisitor\Resolver\IdentifierResolver;
18 | use Humbug\PhpScoper\Symbol\EnrichedReflector;
19 | use PhpParser\Node;
20 | use PhpParser\Node\Arg;
21 | use PhpParser\Node\Expr;
22 | use PhpParser\Node\Expr\FuncCall;
23 | use PhpParser\Node\Name\FullyQualified;
24 | use PhpParser\Node\Scalar\String_;
25 | use PhpParser\Node\Stmt\Const_;
26 | use PhpParser\Node\Stmt\Expression;
27 | use PhpParser\NodeVisitorAbstract;
28 | use UnexpectedValueException;
29 | use function count;
30 |
31 | /**
32 | * Replaces constants `const` declarations by `define` for exposed constants.
33 | *
34 | * ```
35 | * const DUMMY_CONST = 'foo';
36 | * ```
37 | *
38 | * =>
39 | *
40 | * ```
41 | * define('DUMMY_CONST', 'foo');
42 | * ```
43 | *
44 | * @private
45 | */
46 | final class ConstStmtReplacer extends NodeVisitorAbstract
47 | {
48 | public function __construct(
49 | private readonly IdentifierResolver $identifierResolver,
50 | private readonly EnrichedReflector $enrichedReflector,
51 | ) {
52 | }
53 |
54 | public function enterNode(Node $node): Node
55 | {
56 | if (!$node instanceof Const_) {
57 | return $node;
58 | }
59 |
60 | foreach ($node->consts as $constant) {
61 | $replacement = $this->replaceConst($node, $constant);
62 |
63 | if (null !== $replacement) {
64 | // If there is more than one constant declare in the node we
65 | // will not have a replacement (this case is not supported)
66 | // hence the return statement is safe here.
67 | return $replacement;
68 | }
69 | }
70 |
71 | return $node;
72 | }
73 |
74 | private function replaceConst(Const_ $const, Node\Const_ $constant): ?Node
75 | {
76 | $resolvedConstantName = $this->identifierResolver->resolveIdentifier(
77 | $constant->name,
78 | );
79 |
80 | if (!$this->enrichedReflector->isExposedConstant((string) $resolvedConstantName)) {
81 | // No replacement
82 | return null;
83 | }
84 |
85 | if (count($const->consts) > 1) {
86 | // TODO: add support for this case instead of bailing out.
87 | throw new UnexpectedValueException(
88 | 'Exposing a constant declared in a grouped constant statement (e.g. `const FOO = \'foo\', BAR = \'bar\'; is not supported. Consider breaking it down in multiple constant declaration statements',
89 | );
90 | }
91 |
92 | return self::createConstDefineNode(
93 | (string) $resolvedConstantName,
94 | $constant->value,
95 | );
96 | }
97 |
98 | private static function createConstDefineNode(string $name, Expr $value): Node
99 | {
100 | return new Expression(
101 | new FuncCall(
102 | new FullyQualified('define'),
103 | [
104 | new Arg(
105 | new String_($name),
106 | ),
107 | new Arg($value),
108 | ],
109 | ),
110 | );
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeVisitor/EvalPrefixer.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor;
16 |
17 | use Humbug\PhpScoper\PhpParser\NodeVisitor\AttributeAppender\ParentNodeAppender;
18 | use Humbug\PhpScoper\PhpParser\StringNodePrefixer;
19 | use PhpParser\Node;
20 | use PhpParser\Node\Expr\Eval_;
21 | use PhpParser\Node\Scalar\String_;
22 | use PhpParser\NodeVisitorAbstract;
23 |
24 | final class EvalPrefixer extends NodeVisitorAbstract
25 | {
26 | public function __construct(private readonly StringNodePrefixer $stringPrefixer)
27 | {
28 | }
29 |
30 | public function enterNode(Node $node): Node
31 | {
32 | if ($node instanceof String_
33 | && ParentNodeAppender::findParent($node) instanceof Eval_
34 | ) {
35 | $this->stringPrefixer->prefixStringValue($node);
36 | }
37 |
38 | return $node;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeVisitor/ExcludedFunctionExistsStringNodeStack.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor;
16 |
17 | use PhpParser\Node\Scalar\String_;
18 | use PhpParser\NodeVisitorAbstract;
19 |
20 | /**
21 | * Collection of String_ nodes which are contained within a `function_exists()` function call and for which the string
22 | * is an excluded function.
23 | *
24 | * Example of code for which the String_ node will be registered:
25 | *
26 | * ```
27 | * if (!function_exists('str_split')) {
28 | * // ...
29 | * }
30 | *
31 | * @internal
32 | */
33 | final class ExcludedFunctionExistsStringNodeStack extends NodeVisitorAbstract
34 | {
35 | /**
36 | * @var String_[]
37 | */
38 | private array $stack = [];
39 |
40 | // TODO: cases to handle
41 | // - what if is a variable instead?
42 | // - multiple function_exists already
43 |
44 | public function push(String_ $string): void
45 | {
46 | $this->stack[] = $string;
47 | }
48 |
49 | /**
50 | * @return String_[]
51 | */
52 | public function fetch(): array
53 | {
54 | $stack = $this->stack;
55 |
56 | $this->stack = [];
57 |
58 | return $stack;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeVisitor/MultiConstStmtReplacer.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor;
16 |
17 | use PhpParser\Node;
18 | use PhpParser\Node\Expr\ConstFetch;
19 | use PhpParser\Node\Name;
20 | use PhpParser\Node\Stmt\Const_;
21 | use PhpParser\Node\Stmt\If_;
22 | use PhpParser\NodeVisitorAbstract;
23 | use function array_map;
24 | use function count;
25 |
26 | /**
27 | * Replaces multi-constants declarations into multiple single-constant
28 | * declarations.
29 | * This is to allow ConstStmtReplacer to do its job without having to worry
30 | * about this multi-constant declaration case which it cannot handle.
31 | *
32 | * ```
33 | * const FOO = 'foo', BAR = 'bar';
34 | * ```
35 | *
36 | * =>
37 | *
38 | * ```
39 | * const FOO = 'foo';
40 | * const BAR = 'bar';
41 | * ```
42 | *
43 | * @private
44 | */
45 | final class MultiConstStmtReplacer extends NodeVisitorAbstract
46 | {
47 | public function enterNode(Node $node): Node
48 | {
49 | if (!$node instanceof Const_) {
50 | return $node;
51 | }
52 |
53 | if (count($node->consts) <= 1) {
54 | return $node;
55 | }
56 |
57 | $newStatements = array_map(
58 | static function (Node\Const_ $const) use ($node): Const_ {
59 | $newConstNode = clone $node;
60 | $newConstNode->consts = [$const];
61 |
62 | return $newConstNode;
63 | },
64 | $node->consts,
65 | );
66 |
67 | // Workaround to replace the statement.
68 | // See https://github.com/nikic/PHP-Parser/issues/507
69 | return new If_(
70 | new ConstFetch(new Name('true')),
71 | ['stmts' => $newStatements],
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeVisitor/NamespaceStmt/NamespaceManipulator.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor\NamespaceStmt;
16 |
17 | use Humbug\PhpScoper\NotInstantiable;
18 | use PhpParser\Node\Name;
19 | use PhpParser\Node\Stmt\Namespace_;
20 | use PhpParser\NodeVisitorAbstract;
21 |
22 | /**
23 | * @private
24 | */
25 | final class NamespaceManipulator extends NodeVisitorAbstract
26 | {
27 | use NotInstantiable;
28 |
29 | private const ORIGINAL_NAME_ATTRIBUTE = 'originalName';
30 |
31 | public static function hasOriginalName(Namespace_ $namespace): bool
32 | {
33 | return $namespace->hasAttribute(self::ORIGINAL_NAME_ATTRIBUTE);
34 | }
35 |
36 | public static function getOriginalName(Namespace_ $namespace): ?Name
37 | {
38 | if (!self::hasOriginalName($namespace)) {
39 | return $namespace->name;
40 | }
41 |
42 | return $namespace->getAttribute(self::ORIGINAL_NAME_ATTRIBUTE);
43 | }
44 |
45 | public static function setOriginalName(Namespace_ $namespace, ?Name $originalName): void
46 | {
47 | $namespace->setAttribute(self::ORIGINAL_NAME_ATTRIBUTE, $originalName);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeVisitor/NamespaceStmt/NamespaceStmtCollection.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor\NamespaceStmt;
16 |
17 | use ArrayIterator;
18 | use Countable;
19 | use Humbug\PhpScoper\PhpParser\NodeVisitor\AttributeAppender\ParentNodeAppender;
20 | use Humbug\PhpScoper\PhpParser\UnexpectedParsingScenario;
21 | use IteratorAggregate;
22 | use PhpParser\Node;
23 | use PhpParser\Node\Name;
24 | use PhpParser\Node\Stmt\Namespace_;
25 | use Traversable;
26 | use function count;
27 | use function end;
28 |
29 | /**
30 | * Utility class collecting all the namespaces for the scoped files allowing to easily find the namespace to which
31 | * belongs a node.
32 | *
33 | * @private
34 | *
35 | * @implements IteratorAggregate
36 | */
37 | final class NamespaceStmtCollection implements IteratorAggregate, Countable
38 | {
39 | /**
40 | * @var Namespace_[]
41 | */
42 | private array $nodes = [];
43 |
44 | /**
45 | * @var (Name|null)[] Associative array with the potentially prefixed namespace names as keys and their original name
46 | * as value.
47 | */
48 | private array $mapping = [];
49 |
50 | /**
51 | * @param Namespace_ $namespace New namespace, may have been prefixed.
52 | */
53 | public function add(Namespace_ $namespace): void
54 | {
55 | $this->nodes[] = $namespace;
56 |
57 | $this->mapping[(string) $namespace->name] = NamespaceManipulator::getOriginalName($namespace);
58 | }
59 |
60 | public function findNamespaceForNode(Node $node): ?Name
61 | {
62 | if (0 === count($this->nodes)) {
63 | return null;
64 | }
65 |
66 | // Shortcut if there is only one namespace
67 | if (1 === count($this->nodes)) {
68 | return NamespaceManipulator::getOriginalName($this->nodes[0]);
69 | }
70 |
71 | return $this->getNodeNamespaceName($node);
72 | }
73 |
74 | public function getCurrentNamespaceName(): ?Name
75 | {
76 | $lastNode = end($this->nodes);
77 |
78 | return false === $lastNode ? null : NamespaceManipulator::getOriginalName($lastNode);
79 | }
80 |
81 | public function count(): int
82 | {
83 | return count($this->nodes);
84 | }
85 |
86 | private function getNodeNamespaceName(Node $node): ?Name
87 | {
88 | if (!ParentNodeAppender::hasParent($node)) {
89 | throw UnexpectedParsingScenario::create();
90 | }
91 |
92 | $parentNode = ParentNodeAppender::getParent($node);
93 |
94 | if ($parentNode instanceof Namespace_) {
95 | return $this->mapping[(string) $parentNode->name];
96 | }
97 |
98 | return $this->getNodeNamespaceName($parentNode);
99 | }
100 |
101 | /**
102 | * @return Traversable
103 | */
104 | public function getIterator(): Traversable
105 | {
106 | return new ArrayIterator($this->nodes);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeVisitor/NamespaceStmt/NamespaceStmtPrefixer.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor\NamespaceStmt;
16 |
17 | use Humbug\PhpScoper\Symbol\EnrichedReflector;
18 | use PhpParser\Node;
19 | use PhpParser\Node\Name;
20 | use PhpParser\Node\Stmt\Namespace_;
21 | use PhpParser\NodeVisitorAbstract;
22 |
23 | /**
24 | * Prefixes the relevant namespaces.
25 | *
26 | * ```
27 | * namespace Foo;
28 | * ```
29 | *
30 | * =>
31 | *
32 | * ```
33 | * namespace Humbug\Foo;
34 | * ```
35 | *
36 | * @private
37 | */
38 | final class NamespaceStmtPrefixer extends NodeVisitorAbstract
39 | {
40 | public function __construct(
41 | private readonly string $prefix,
42 | private readonly EnrichedReflector $enrichedReflector,
43 | private readonly NamespaceStmtCollection $namespaceStatements,
44 | ) {
45 | }
46 |
47 | public function enterNode(Node $node): Node
48 | {
49 | return ($node instanceof Namespace_)
50 | ? $this->prefixNamespaceStmt($node)
51 | : $node;
52 | }
53 |
54 | private function prefixNamespaceStmt(Namespace_ $namespace): Node
55 | {
56 | if ($this->shouldPrefixStmt($namespace)) {
57 | self::prefixStmt($namespace, $this->prefix);
58 | }
59 |
60 | $this->namespaceStatements->add($namespace);
61 |
62 | return $namespace;
63 | }
64 |
65 | private function shouldPrefixStmt(Namespace_ $namespace): bool
66 | {
67 | $name = $namespace->name;
68 |
69 | if ($this->enrichedReflector->isExcludedNamespace((string) $name)) {
70 | return false;
71 | }
72 |
73 | $nameFirstPart = null === $name ? '' : $name->getFirst();
74 |
75 | return $this->prefix !== $nameFirstPart;
76 | }
77 |
78 | private static function prefixStmt(Namespace_ $namespace, string $prefix): void
79 | {
80 | $originalName = $namespace->name;
81 |
82 | $namespace->name = Name::concat($prefix, $originalName);
83 |
84 | NamespaceManipulator::setOriginalName($namespace, $originalName);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeVisitor/NewdocPrefixer.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor;
16 |
17 | use Humbug\PhpScoper\PhpParser\StringNodePrefixer;
18 | use PhpParser\Node;
19 | use PhpParser\Node\Scalar\String_;
20 | use PhpParser\NodeVisitorAbstract;
21 | use function ltrim;
22 | use function str_starts_with;
23 | use function substr;
24 |
25 | final class NewdocPrefixer extends NodeVisitorAbstract
26 | {
27 | public function __construct(private readonly StringNodePrefixer $stringPrefixer)
28 | {
29 | }
30 |
31 | public function enterNode(Node $node): Node
32 | {
33 | if ($node instanceof String_ && $this->isPhpNowdoc($node)) {
34 | $this->stringPrefixer->prefixStringValue($node);
35 | }
36 |
37 | return $node;
38 | }
39 |
40 | private function isPhpNowdoc(String_ $node): bool
41 | {
42 | if (String_::KIND_NOWDOC !== $node->getAttribute('kind')) {
43 | return false;
44 | }
45 |
46 | return str_starts_with(
47 | substr(
48 | ltrim($node->value),
49 | 0,
50 | 5,
51 | ),
52 | ',
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor\Resolver;
16 |
17 | use Humbug\PhpScoper\PhpParser\Node\NamedIdentifier;
18 | use Humbug\PhpScoper\PhpParser\NodeVisitor\AttributeAppender\ParentNodeAppender;
19 | use PhpParser\Node\Identifier;
20 | use PhpParser\Node\Name;
21 | use PhpParser\Node\Name\FullyQualified;
22 | use PhpParser\Node\Scalar\String_;
23 | use PhpParser\Node\Stmt\Function_;
24 | use PhpParser\NodeVisitor\NameResolver;
25 | use function array_filter;
26 | use function implode;
27 | use function ltrim;
28 |
29 | /**
30 | * Attempts to resolve the identifier node into a fully qualified node. Returns a valid
31 | * (non fully-qualified) name node on failure.
32 | *
33 | * @private
34 | */
35 | final readonly class IdentifierResolver
36 | {
37 | public function __construct(private NameResolver $nameResolver)
38 | {
39 | }
40 |
41 | public function resolveIdentifier(Identifier $identifier): Name
42 | {
43 | $resolvedName = $identifier->getAttribute('resolvedName');
44 |
45 | if (null !== $resolvedName) {
46 | return $resolvedName;
47 | }
48 |
49 | $parentNode = ParentNodeAppender::getParent($identifier);
50 |
51 | if ($parentNode instanceof Function_) {
52 | return $this->resolveFunctionIdentifier($identifier);
53 | }
54 |
55 | $name = NamedIdentifier::create($identifier);
56 |
57 | return $this->nameResolver
58 | ->getNameContext()
59 | ->getResolvedClassName($name);
60 | }
61 |
62 | public function resolveString(String_ $string): Name
63 | {
64 | $name = new FullyQualified(
65 | ltrim($string->value, '\\'),
66 | $string->getAttributes(),
67 | );
68 |
69 | return $this->nameResolver
70 | ->getNameContext()
71 | ->getResolvedClassName($name);
72 | }
73 |
74 | private function resolveFunctionIdentifier(Identifier $identifier): Name
75 | {
76 | $nameParts = array_filter([
77 | $this->nameResolver->getNameContext()->getNamespace(),
78 | $identifier->toString(),
79 | ]);
80 |
81 | return new FullyQualified(
82 | implode(
83 | '\\',
84 | $nameParts,
85 | ),
86 | $identifier->getAttributes(),
87 | );
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeVisitor/Resolver/OriginalNameResolver.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor\Resolver;
16 |
17 | use Humbug\PhpScoper\NotInstantiable;
18 | use PhpParser\Node\Name;
19 |
20 | final class OriginalNameResolver
21 | {
22 | use NotInstantiable;
23 |
24 | private const ORIGINAL_NAME_ATTRIBUTE = 'originalName';
25 |
26 | public static function hasOriginalName(Name $namespace): bool
27 | {
28 | return $namespace->hasAttribute(self::ORIGINAL_NAME_ATTRIBUTE);
29 | }
30 |
31 | public static function getOriginalName(Name $name): Name
32 | {
33 | if (!self::hasOriginalName($name)) {
34 | return $name;
35 | }
36 |
37 | return $name->getAttribute(self::ORIGINAL_NAME_ATTRIBUTE);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeVisitor/UseStmt/UseStmtCollector.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor\UseStmt;
16 |
17 | use Humbug\PhpScoper\PhpParser\NodeVisitor\NamespaceStmt\NamespaceStmtCollection;
18 | use PhpParser\Node;
19 | use PhpParser\Node\Stmt\Use_;
20 | use PhpParser\NodeVisitorAbstract;
21 |
22 | /**
23 | * Collects all the use statements. This allows us to resolve a class/constant/function call into a fully-qualified
24 | * call.
25 | *
26 | * @private
27 | */
28 | final class UseStmtCollector extends NodeVisitorAbstract
29 | {
30 | public function __construct(
31 | private readonly NamespaceStmtCollection $namespaceStatements,
32 | private readonly UseStmtCollection $useStatements,
33 | ) {
34 | }
35 |
36 | public function enterNode(Node $node): Node
37 | {
38 | if ($node instanceof Use_) {
39 | $namespaceName = $this->namespaceStatements->getCurrentNamespaceName();
40 |
41 | $this->useStatements->add($namespaceName, $node);
42 | }
43 |
44 | return $node;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeVisitor/UseStmt/UseStmtManipulator.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor\UseStmt;
16 |
17 | use Humbug\PhpScoper\NotInstantiable;
18 | use PhpParser\Node\Name;
19 | use PhpParser\Node\Stmt\UseUse;
20 | use PhpParser\NodeVisitorAbstract;
21 |
22 | /**
23 | * @private
24 | */
25 | final class UseStmtManipulator extends NodeVisitorAbstract
26 | {
27 | use NotInstantiable;
28 |
29 | private const ORIGINAL_NAME_ATTRIBUTE = 'originalName';
30 |
31 | public static function hasOriginalName(UseUse $use): bool
32 | {
33 | return $use->hasAttribute(self::ORIGINAL_NAME_ATTRIBUTE);
34 | }
35 |
36 | public static function getOriginalName(UseUse $use): ?Name
37 | {
38 | if (!self::hasOriginalName($use)) {
39 | return $use->name;
40 | }
41 |
42 | return $use->getAttribute(self::ORIGINAL_NAME_ATTRIBUTE);
43 | }
44 |
45 | public static function setOriginalName(UseUse $use, ?Name $originalName): void
46 | {
47 | $use->setAttribute(self::ORIGINAL_NAME_ATTRIBUTE, $originalName);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/PhpParser/NodeVisitor/UseStmt/UseStmtPrefixer.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\NodeVisitor\UseStmt;
16 |
17 | use Humbug\PhpScoper\PhpParser\NodeVisitor\AttributeAppender\ParentNodeAppender;
18 | use Humbug\PhpScoper\PhpParser\UnexpectedParsingScenario;
19 | use Humbug\PhpScoper\Symbol\EnrichedReflector;
20 | use PhpParser\Node;
21 | use PhpParser\Node\Name;
22 | use PhpParser\Node\Stmt\Use_;
23 | use PhpParser\Node\Stmt\UseUse;
24 | use PhpParser\NodeVisitorAbstract;
25 |
26 | /**
27 | * Prefixes the use statements.
28 | *
29 | * @private
30 | */
31 | final class UseStmtPrefixer extends NodeVisitorAbstract
32 | {
33 | public function __construct(
34 | private readonly string $prefix,
35 | private readonly EnrichedReflector $enrichedReflector,
36 | ) {
37 | }
38 |
39 | public function enterNode(Node $node): Node
40 | {
41 | if ($node instanceof UseUse && $this->shouldPrefixUseStmt($node)) {
42 | self::prefixStmt($node, $this->prefix);
43 | }
44 |
45 | return $node;
46 | }
47 |
48 | private function shouldPrefixUseStmt(UseUse $use): bool
49 | {
50 | $useType = self::findUseType($use);
51 | $nameString = $use->name->toString();
52 |
53 | $alreadyPrefixed = $this->prefix === $use->name->getFirst();
54 |
55 | if ($alreadyPrefixed) {
56 | return false;
57 | }
58 |
59 | if ($this->enrichedReflector->belongsToExcludedNamespace($nameString)) {
60 | return false;
61 | }
62 |
63 | if (Use_::TYPE_FUNCTION === $useType) {
64 | return !$this->enrichedReflector->isFunctionInternal($nameString);
65 | }
66 |
67 | if (Use_::TYPE_CONSTANT === $useType) {
68 | return !$this->enrichedReflector->isExposedConstant($nameString);
69 | }
70 |
71 | return Use_::TYPE_NORMAL !== $useType
72 | || !$this->enrichedReflector->isClassInternal($nameString);
73 | }
74 |
75 | private static function prefixStmt(UseUse $use, string $prefix): void
76 | {
77 | $previousName = $use->name;
78 |
79 | $prefixedName = Name::concat(
80 | $prefix,
81 | $use->name,
82 | $use->name->getAttributes(),
83 | );
84 |
85 | if (null === $prefixedName) {
86 | throw UnexpectedParsingScenario::create();
87 | }
88 |
89 | // Unlike the new (prefixed name), the previous name will not be
90 | // traversed hence we need to manually set its parent attribute
91 | ParentNodeAppender::setParent($previousName, $use);
92 | UseStmtManipulator::setOriginalName($use, $previousName);
93 |
94 | $use->name = $prefixedName;
95 | }
96 |
97 | /**
98 | * Finds the type of the use statement.
99 | *
100 | * @return int See \PhpParser\Node\Stmt\Use_ type constants.
101 | */
102 | private static function findUseType(UseUse $use): int
103 | {
104 | if (Use_::TYPE_UNKNOWN === $use->type) {
105 | /** @var Use_ $parentNode */
106 | $parentNode = ParentNodeAppender::getParent($use);
107 |
108 | return $parentNode->type;
109 | }
110 |
111 | return $use->type;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/PhpParser/Parser/ParserFactory.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\Parser;
16 |
17 | use PhpParser\Parser;
18 | use PhpParser\PhpVersion;
19 |
20 | interface ParserFactory
21 | {
22 | public function createParser(?PhpVersion $phpVersion = null): Parser;
23 | }
24 |
--------------------------------------------------------------------------------
/src/PhpParser/Parser/StandardParserFactory.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\Parser;
16 |
17 | use PhpParser\Lexer;
18 | use PhpParser\Lexer\Emulative;
19 | use PhpParser\Parser;
20 | use PhpParser\Parser\Php7;
21 | use PhpParser\Parser\Php8;
22 | use PhpParser\PhpVersion;
23 |
24 | final class StandardParserFactory implements ParserFactory
25 | {
26 | public function createParser(?PhpVersion $phpVersion = null): Parser
27 | {
28 | $version = $phpVersion ?? PhpVersion::getHostVersion();
29 |
30 | $lexer = $version->isHostVersion()
31 | ? new Lexer()
32 | : new Emulative($version);
33 |
34 | return $version->id >= 80_000
35 | ? new Php8($lexer, $version)
36 | : new Php7($lexer, $version);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/PhpParser/Printer/Printer.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\Printer;
16 |
17 | use PhpParser\Node;
18 | use PhpParser\Token;
19 |
20 | interface Printer
21 | {
22 | /**
23 | * @param Node[] $newStmts
24 | * @param Node[] $oldStmts
25 | * @param Token[] $oldTokens
26 | */
27 | public function print(array $newStmts, array $oldStmts, array $oldTokens): string;
28 | }
29 |
--------------------------------------------------------------------------------
/src/PhpParser/Printer/PrinterFactory.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\Printer;
16 |
17 | use PhpParser\PhpVersion;
18 |
19 | interface PrinterFactory
20 | {
21 | public function createPrinter(?PhpVersion $phpVersion = null): Printer;
22 | }
23 |
--------------------------------------------------------------------------------
/src/PhpParser/Printer/StandardPrinter.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\Printer;
16 |
17 | use PhpParser\PrettyPrinter;
18 |
19 | final readonly class StandardPrinter implements Printer
20 | {
21 | public function __construct(private PrettyPrinter $decoratedPrinter)
22 | {
23 | }
24 |
25 | public function print(array $newStmts, array $oldStmts, array $oldTokens): string
26 | {
27 | $printedStatements = $this->decoratedPrinter->prettyPrintFile($newStmts);
28 |
29 | return $printedStatements."\n";
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/PhpParser/Printer/StandardPrinterFactory.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser\Printer;
16 |
17 | use PhpParser\PhpVersion;
18 | use PhpParser\PrettyPrinter\Standard;
19 |
20 | final class StandardPrinterFactory implements PrinterFactory
21 | {
22 | public function createPrinter(?PhpVersion $phpVersion = null): Printer
23 | {
24 | return new StandardPrinter(
25 | new Standard([
26 | 'phpVersion' => $phpVersion ?? PhpVersion::fromComponents(7, 2),
27 | ]),
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/PhpParser/StringNodePrefixer.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser;
16 |
17 | use Humbug\PhpScoper\Scoper\PhpScoper;
18 | use PhpParser\Error as PhpParserError;
19 | use PhpParser\Node\Scalar\String_;
20 | use function substr;
21 |
22 | /**
23 | * @private
24 | */
25 | final readonly class StringNodePrefixer
26 | {
27 | public function __construct(private PhpScoper $scoper)
28 | {
29 | }
30 |
31 | public function prefixStringValue(String_ $node): void
32 | {
33 | try {
34 | $lastChar = substr($node->value, -1);
35 |
36 | $newValue = $this->scoper->scopePhp($node->value);
37 |
38 | if ("\n" !== $lastChar) {
39 | $newValue = substr($newValue, 0, -1);
40 | }
41 |
42 | $node->value = $newValue;
43 | } catch (PhpParserError) {
44 | // Continue without scoping the heredoc which for some reasons contains invalid PHP code
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/PhpParser/TraverserFactory.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser;
16 |
17 | use Humbug\PhpScoper\PhpParser\NodeVisitor\ExcludedFunctionExistsEnricher;
18 | use Humbug\PhpScoper\PhpParser\NodeVisitor\ExcludedFunctionExistsStringNodeStack;
19 | use Humbug\PhpScoper\PhpParser\NodeVisitor\NamespaceStmt\NamespaceStmtCollection;
20 | use Humbug\PhpScoper\PhpParser\NodeVisitor\Resolver\IdentifierResolver;
21 | use Humbug\PhpScoper\PhpParser\NodeVisitor\UseStmt\UseStmtCollection;
22 | use Humbug\PhpScoper\Scoper\PhpScoper;
23 | use Humbug\PhpScoper\Symbol\EnrichedReflector;
24 | use Humbug\PhpScoper\Symbol\SymbolsRegistry;
25 | use PhpParser\NodeTraverser as PhpParserNodeTraverser;
26 | use PhpParser\NodeTraverserInterface;
27 | use PhpParser\NodeVisitor as PhpParserNodeVisitor;
28 | use PhpParser\NodeVisitor\NameResolver;
29 |
30 | /**
31 | * @private
32 | */
33 | class TraverserFactory
34 | {
35 | public function __construct(
36 | private readonly EnrichedReflector $reflector,
37 | private readonly string $prefix,
38 | private readonly SymbolsRegistry $symbolsRegistry,
39 | ) {
40 | }
41 |
42 | public function create(PhpScoper $scoper): NodeTraverserInterface
43 | {
44 | return self::createTraverser(
45 | self::createNodeVisitors(
46 | $this->prefix,
47 | $this->reflector,
48 | $scoper,
49 | $this->symbolsRegistry,
50 | ),
51 | );
52 | }
53 |
54 | /**
55 | * @param PhpParserNodeVisitor[] $nodeVisitors
56 | */
57 | private static function createTraverser(array $nodeVisitors): NodeTraverserInterface
58 | {
59 | $traverser = new NodeTraverser(
60 | new PhpParserNodeTraverser(),
61 | );
62 |
63 | foreach ($nodeVisitors as $nodeVisitor) {
64 | $traverser->addVisitor($nodeVisitor);
65 | }
66 |
67 | return $traverser;
68 | }
69 |
70 | /**
71 | * @return PhpParserNodeVisitor[]
72 | */
73 | private static function createNodeVisitors(
74 | string $prefix,
75 | EnrichedReflector $reflector,
76 | PhpScoper $scoper,
77 | SymbolsRegistry $symbolsRegistry
78 | ): array {
79 | $namespaceStatements = new NamespaceStmtCollection();
80 | $useStatements = new UseStmtCollection();
81 |
82 | $nameResolver = new NameResolver(
83 | null,
84 | ['preserveOriginalNames' => true],
85 | );
86 | $identifierResolver = new IdentifierResolver($nameResolver);
87 | $stringNodePrefixer = new StringNodePrefixer($scoper);
88 |
89 | $excludedFunctionExistsStringNodeStack = new ExcludedFunctionExistsStringNodeStack();
90 |
91 | return [
92 | $nameResolver,
93 | new NodeVisitor\AttributeAppender\ParentNodeAppender(),
94 | new NodeVisitor\AttributeAppender\IdentifierNameAppender($identifierResolver),
95 |
96 | new NodeVisitor\NamespaceStmt\NamespaceStmtPrefixer(
97 | $prefix,
98 | $reflector,
99 | $namespaceStatements,
100 | ),
101 |
102 | new NodeVisitor\UseStmt\UseStmtCollector(
103 | $namespaceStatements,
104 | $useStatements,
105 | ),
106 | new NodeVisitor\UseStmt\UseStmtPrefixer(
107 | $prefix,
108 | $reflector,
109 | ),
110 |
111 | new NodeVisitor\FunctionIdentifierRecorder(
112 | $prefix,
113 | $identifierResolver,
114 | $symbolsRegistry,
115 | $reflector,
116 | ),
117 | new NodeVisitor\ClassIdentifierRecorder(
118 | $prefix,
119 | $identifierResolver,
120 | $symbolsRegistry,
121 | $reflector,
122 | ),
123 | new NodeVisitor\NameStmtPrefixer(
124 | $prefix,
125 | $namespaceStatements,
126 | $useStatements,
127 | $reflector,
128 | ),
129 | new NodeVisitor\StringScalarPrefixer(
130 | $prefix,
131 | $reflector,
132 | $excludedFunctionExistsStringNodeStack,
133 | ),
134 | new NodeVisitor\NewdocPrefixer($stringNodePrefixer),
135 | new NodeVisitor\EvalPrefixer($stringNodePrefixer),
136 |
137 | new NodeVisitor\ClassAliasStmtAppender(
138 | $identifierResolver,
139 | $symbolsRegistry,
140 | ),
141 | new ExcludedFunctionExistsEnricher(
142 | $prefix,
143 | $excludedFunctionExistsStringNodeStack,
144 | ),
145 | new NodeVisitor\MultiConstStmtReplacer(),
146 | new NodeVisitor\ConstStmtReplacer(
147 | $identifierResolver,
148 | $reflector,
149 | ),
150 | ];
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/PhpParser/UnexpectedParsingScenario.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser;
16 |
17 | use UnexpectedValueException;
18 |
19 | final class UnexpectedParsingScenario extends UnexpectedValueException
20 | {
21 | public static function create(): self
22 | {
23 | return new self('Unexpected case. Please report it.');
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/PhpParser/UseStmtName.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\PhpParser;
16 |
17 | use Humbug\PhpScoper\PhpParser\NodeVisitor\AttributeAppender\ParentNodeAppender;
18 | use PhpParser\Node\Name;
19 | use PhpParser\Node\Stmt\Use_;
20 | use PhpParser\Node\Stmt\UseUse;
21 | use function count;
22 | use function sprintf;
23 |
24 | final readonly class UseStmtName
25 | {
26 | public function __construct(private Name $name)
27 | {
28 | }
29 |
30 | public function contains(Name $resolvedName): bool
31 | {
32 | return self::arrayStartsWith(
33 | $resolvedName->getParts(),
34 | $this->name->getParts(),
35 | );
36 | }
37 |
38 | /**
39 | * @param string[] $array
40 | * @param string[] $start
41 | */
42 | private static function arrayStartsWith(array $array, array $start): bool
43 | {
44 | $prefixLength = count($start);
45 |
46 | for ($index = 0; $index < $prefixLength; ++$index) {
47 | if (!isset($array[$index]) || $array[$index] !== $start[$index]) {
48 | return false;
49 | }
50 | }
51 |
52 | return true;
53 | }
54 |
55 | /**
56 | * @return array{string|null, Use_::TYPE_*}
57 | */
58 | public function getUseStmtAliasAndType(): array
59 | {
60 | $use = self::getUseNode($this->name);
61 | $useParent = self::getUseParentNode($use);
62 |
63 | $alias = $use->alias;
64 |
65 | if (null !== $alias) {
66 | $alias = (string) $alias;
67 | }
68 |
69 | return [
70 | $alias,
71 | $useParent->type,
72 | ];
73 | }
74 |
75 | private static function getUseNode(Name $name): UseUse
76 | {
77 | $use = ParentNodeAppender::getParent($name);
78 |
79 | if ($use instanceof UseUse) {
80 | return $use;
81 | }
82 |
83 | // @codeCoverageIgnoreStart
84 | throw new UnexpectedParsingScenario(
85 | sprintf(
86 | 'Unexpected use statement name parent "%s"',
87 | $use::class,
88 | ),
89 | );
90 | // @codeCoverageIgnoreEnd
91 | }
92 |
93 | private static function getUseParentNode(UseUse $use): Use_
94 | {
95 | $useParent = ParentNodeAppender::getParent($use);
96 |
97 | if ($useParent instanceof Use_) {
98 | return $useParent;
99 | }
100 |
101 | // @codeCoverageIgnoreStart
102 | throw new UnexpectedParsingScenario(
103 | sprintf(
104 | 'Unexpected UseUse parent "%s"',
105 | $useParent::class,
106 | ),
107 | );
108 | // @codeCoverageIgnoreEnd
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/Scoper/Composer/InstalledPackagesScoper.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Scoper\Composer;
16 |
17 | use Humbug\PhpScoper\Scoper\Scoper;
18 | use InvalidArgumentException;
19 | use stdClass;
20 | use function array_map;
21 | use function gettype;
22 | use function is_array;
23 | use function preg_match as native_preg_match;
24 | use function Safe\json_decode;
25 | use function Safe\json_encode;
26 | use function sprintf;
27 | use const JSON_PRETTY_PRINT;
28 | use const JSON_THROW_ON_ERROR;
29 |
30 | final readonly class InstalledPackagesScoper implements Scoper
31 | {
32 | private const COMPOSER_INSTALLED_FILE_PATTERN = '/composer(\/|\\\\)installed\.json$/';
33 |
34 | public function __construct(
35 | private Scoper $decoratedScoper,
36 | private AutoloadPrefixer $autoloadPrefixer,
37 | ) {
38 | }
39 |
40 | /**
41 | * Scopes PHP and JSON files related to Composer.
42 | */
43 | public function scope(string $filePath, string $contents): string
44 | {
45 | if (1 !== native_preg_match(self::COMPOSER_INSTALLED_FILE_PATTERN, $filePath)) {
46 | return $this->decoratedScoper->scope($filePath, $contents);
47 | }
48 |
49 | $decodedJson = self::decodeContents($contents);
50 |
51 | if (!isset($decodedJson->packages) || !is_array($decodedJson->packages)) {
52 | throw new InvalidArgumentException('Expected the decoded JSON to contain the list of installed packages');
53 | }
54 |
55 | /** @phpstan-ignore argument.type */
56 | $decodedJson->packages = $this->prefixLockPackages($decodedJson->packages);
57 |
58 | return json_encode(
59 | $decodedJson,
60 | JSON_PRETTY_PRINT,
61 | );
62 | }
63 |
64 | private static function decodeContents(string $contents): stdClass
65 | {
66 | $decodedJson = json_decode($contents, false, 512, JSON_THROW_ON_ERROR);
67 |
68 | if ($decodedJson instanceof stdClass) {
69 | return $decodedJson;
70 | }
71 |
72 | throw new InvalidArgumentException(
73 | sprintf(
74 | 'Expected the decoded JSON to be an stdClass instance, got "%s" instead',
75 | gettype($decodedJson),
76 | ),
77 | );
78 | }
79 |
80 | /**
81 | * @param array $packages
82 | *
83 | * @return array
84 | */
85 | private function prefixLockPackages(array $packages): array
86 | {
87 | return array_map(
88 | fn (stdClass $package) => $this->autoloadPrefixer->prefixPackageAutoloadStatements($package),
89 | $packages,
90 | );
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Scoper/Composer/JsonFileScoper.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Scoper\Composer;
16 |
17 | use Humbug\PhpScoper\Scoper\Scoper;
18 | use InvalidArgumentException;
19 | use stdClass;
20 | use function gettype;
21 | use function preg_match as native_preg_match;
22 | use function Safe\json_decode;
23 | use function Safe\json_encode;
24 | use function sprintf;
25 | use const JSON_PRETTY_PRINT;
26 | use const JSON_THROW_ON_ERROR;
27 |
28 | final readonly class JsonFileScoper implements Scoper
29 | {
30 | public function __construct(
31 | private Scoper $decoratedScoper,
32 | private AutoloadPrefixer $autoloadPrefixer,
33 | ) {
34 | }
35 |
36 | /**
37 | * Scopes PHP and JSON files related to Composer.
38 | */
39 | public function scope(string $filePath, string $contents): string
40 | {
41 | if (1 !== native_preg_match('/composer\.json$/', $filePath)) {
42 | return $this->decoratedScoper->scope($filePath, $contents);
43 | }
44 |
45 | $decodedJson = self::decodeContents($contents);
46 |
47 | $decodedJson = $this->autoloadPrefixer->prefixPackageAutoloadStatements($decodedJson);
48 |
49 | return json_encode(
50 | $decodedJson,
51 | JSON_PRETTY_PRINT,
52 | );
53 | }
54 |
55 | private static function decodeContents(string $contents): stdClass
56 | {
57 | $decodedJson = json_decode($contents, false, 512, JSON_THROW_ON_ERROR);
58 |
59 | if ($decodedJson instanceof stdClass) {
60 | return $decodedJson;
61 | }
62 |
63 | throw new InvalidArgumentException(
64 | sprintf(
65 | 'Expected the decoded JSON to be an stdClass instance, got "%s" instead',
66 | gettype($decodedJson),
67 | ),
68 | );
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Scoper/Factory/ScoperFactory.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Scoper\Factory;
16 |
17 | use Humbug\PhpScoper\Configuration\Configuration;
18 | use Humbug\PhpScoper\Scoper\Scoper;
19 | use Humbug\PhpScoper\Symbol\SymbolsRegistry;
20 | use PhpParser\PhpVersion;
21 |
22 | interface ScoperFactory
23 | {
24 | public function createScoper(
25 | Configuration $configuration,
26 | SymbolsRegistry $symbolsRegistry,
27 | ?PhpVersion $phpVersion = null,
28 | ): Scoper;
29 | }
30 |
--------------------------------------------------------------------------------
/src/Scoper/Factory/StandardScoperFactory.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Scoper\Factory;
16 |
17 | use Humbug\PhpScoper\Configuration\Configuration;
18 | use Humbug\PhpScoper\PhpParser\Parser\ParserFactory;
19 | use Humbug\PhpScoper\PhpParser\Printer\PrinterFactory;
20 | use Humbug\PhpScoper\PhpParser\TraverserFactory;
21 | use Humbug\PhpScoper\Scoper\Composer\AutoloadPrefixer;
22 | use Humbug\PhpScoper\Scoper\Composer\InstalledPackagesScoper;
23 | use Humbug\PhpScoper\Scoper\Composer\JsonFileScoper;
24 | use Humbug\PhpScoper\Scoper\NullScoper;
25 | use Humbug\PhpScoper\Scoper\PatchScoper;
26 | use Humbug\PhpScoper\Scoper\PhpScoper;
27 | use Humbug\PhpScoper\Scoper\Scoper;
28 | use Humbug\PhpScoper\Scoper\SymfonyScoper;
29 | use Humbug\PhpScoper\Symbol\EnrichedReflectorFactory;
30 | use Humbug\PhpScoper\Symbol\SymbolsRegistry;
31 | use PhpParser\PhpVersion;
32 |
33 | final readonly class StandardScoperFactory implements ScoperFactory
34 | {
35 | public function __construct(
36 | private EnrichedReflectorFactory $enrichedReflectorFactory,
37 | private ParserFactory $parserFactory,
38 | private PrinterFactory $printerFactory,
39 | ) {
40 | }
41 |
42 | public function createScoper(
43 | Configuration $configuration,
44 | SymbolsRegistry $symbolsRegistry,
45 | ?PhpVersion $phpVersion = null,
46 | ): Scoper {
47 | $prefix = $configuration->getPrefix();
48 | $symbolsConfiguration = $configuration->getSymbolsConfiguration();
49 | $enrichedReflector = $this->enrichedReflectorFactory->create($symbolsConfiguration);
50 |
51 | $parser = $this->parserFactory->createParser($phpVersion);
52 | $printer = $this->printerFactory->createPrinter($phpVersion);
53 |
54 | $autoloadPrefixer = new AutoloadPrefixer(
55 | $prefix,
56 | $enrichedReflector,
57 | );
58 |
59 | return new PatchScoper(
60 | new PhpScoper(
61 | $parser,
62 | new JsonFileScoper(
63 | new InstalledPackagesScoper(
64 | new SymfonyScoper(
65 | new NullScoper(),
66 | $prefix,
67 | $enrichedReflector,
68 | $symbolsRegistry,
69 | ),
70 | $autoloadPrefixer,
71 | ),
72 | $autoloadPrefixer,
73 | ),
74 | new TraverserFactory(
75 | $enrichedReflector,
76 | $prefix,
77 | $symbolsRegistry,
78 | ),
79 | $printer,
80 | ),
81 | $prefix,
82 | $configuration->getPatcher(),
83 | );
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/Scoper/NullScoper.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Scoper;
16 |
17 | final class NullScoper implements Scoper
18 | {
19 | public function scope(string $filePath, string $contents): string
20 | {
21 | return $contents;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Scoper/PatchScoper.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Scoper;
16 |
17 | use Humbug\PhpScoper\Patcher\Patcher;
18 | use function func_get_args;
19 |
20 | final readonly class PatchScoper implements Scoper
21 | {
22 | public function __construct(
23 | private Scoper $decoratedScoper,
24 | private string $prefix,
25 | private Patcher $patcher,
26 | ) {
27 | }
28 |
29 | public function scope(string $filePath, string $contents): string
30 | {
31 | return ($this->patcher)(
32 | $filePath,
33 | $this->prefix,
34 | $this->decoratedScoper->scope(...func_get_args()),
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Scoper/PhpScoper.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Scoper;
16 |
17 | use Humbug\PhpScoper\PhpParser\Printer\Printer;
18 | use Humbug\PhpScoper\PhpParser\TraverserFactory;
19 | use Humbug\PhpScoper\Throwable\Exception\ParsingException;
20 | use PhpParser\Error as PhpParserError;
21 | use PhpParser\Parser;
22 | use function basename;
23 | use function func_get_args;
24 | use function ltrim;
25 | use function preg_match as native_preg_match;
26 |
27 | final readonly class PhpScoper implements Scoper
28 | {
29 | private const FILE_PATH_PATTERN = '/\.php$/';
30 | private const NOT_FILE_BINARY = '/\..+?$/';
31 | private const PHP_TAG = '/^<\?php/';
32 | private const PHP_BINARY = '/^#!.+?php.*\n{1,}<\?php/';
33 |
34 | public function __construct(
35 | private Parser $parser,
36 | private Scoper $decoratedScoper,
37 | private TraverserFactory $traverserFactory,
38 | private Printer $printer,
39 | ) {
40 | }
41 |
42 | /**
43 | * Scopes PHP files.
44 | */
45 | public function scope(string $filePath, string $contents): string
46 | {
47 | if (!self::isPhpFile($filePath, $contents)) {
48 | return $this->decoratedScoper->scope(...func_get_args());
49 | }
50 |
51 | try {
52 | return $this->scopePhp($contents);
53 | } catch (PhpParserError $parsingException) {
54 | throw ParsingException::forFile($filePath, $parsingException);
55 | }
56 | }
57 |
58 | public function scopePhp(string $php): string
59 | {
60 | $statements = $this->parser->parse($php);
61 | $oldTokens = $this->parser->getTokens();
62 |
63 | $scopedStatements = $this->traverserFactory
64 | ->create($this)
65 | ->traverse($statements);
66 |
67 | return $this->printer->print(
68 | $scopedStatements,
69 | $scopedStatements,
70 | $oldTokens,
71 | );
72 | }
73 |
74 | private static function isPhpFile(string $filePath, string $contents): bool
75 | {
76 | if (1 === native_preg_match(self::FILE_PATH_PATTERN, $filePath)) {
77 | return true;
78 | }
79 |
80 | if (1 === native_preg_match(self::NOT_FILE_BINARY, basename($filePath))) {
81 | return false;
82 | }
83 |
84 | if (1 === native_preg_match(self::PHP_TAG, ltrim($contents))) {
85 | return true;
86 | }
87 |
88 | return 1 === native_preg_match(self::PHP_BINARY, $contents);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/Scoper/Scoper.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Scoper;
16 |
17 | use Humbug\PhpScoper\Throwable\Exception\ParsingException;
18 |
19 | interface Scoper
20 | {
21 | /**
22 | * Scope AKA. apply the given prefix to the file in the appropriate way.
23 | *
24 | * @param string $filePath File to scope
25 | * @param string $contents File contents
26 | *
27 | * @throws ParsingException
28 | *
29 | * @return string Contents of the file with the prefix applied
30 | */
31 | public function scope(string $filePath, string $contents): string;
32 | }
33 |
--------------------------------------------------------------------------------
/src/Scoper/ScoperFactory.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Scoper;
16 |
17 | use Humbug\PhpScoper\Configuration\Configuration;
18 | use Humbug\PhpScoper\PhpParser\Printer\Printer;
19 | use Humbug\PhpScoper\PhpParser\TraverserFactory;
20 | use Humbug\PhpScoper\Scoper\Composer\AutoloadPrefixer;
21 | use Humbug\PhpScoper\Scoper\Composer\InstalledPackagesScoper;
22 | use Humbug\PhpScoper\Scoper\Composer\JsonFileScoper;
23 | use Humbug\PhpScoper\Symbol\EnrichedReflectorFactory;
24 | use Humbug\PhpScoper\Symbol\SymbolsRegistry;
25 | use PhpParser\Parser;
26 |
27 | /**
28 | * @final
29 | *
30 | * @deprecated Deprecated in favour of \Humbug\PhpScoper\Scoper\Factory\ScoperFactory
31 | */
32 | class ScoperFactory
33 | {
34 | public function __construct(
35 | private readonly Parser $parser,
36 | private readonly EnrichedReflectorFactory $enrichedReflectorFactory,
37 | private readonly Printer $printer,
38 | ) {
39 | }
40 |
41 | public function createScoper(
42 | Configuration $configuration,
43 | SymbolsRegistry $symbolsRegistry
44 | ): Scoper {
45 | $prefix = $configuration->getPrefix();
46 | $symbolsConfiguration = $configuration->getSymbolsConfiguration();
47 | $enrichedReflector = $this->enrichedReflectorFactory->create($symbolsConfiguration);
48 |
49 | $autoloadPrefixer = new AutoloadPrefixer(
50 | $prefix,
51 | $enrichedReflector,
52 | );
53 |
54 | return new PatchScoper(
55 | new PhpScoper(
56 | $this->parser,
57 | new JsonFileScoper(
58 | new InstalledPackagesScoper(
59 | new SymfonyScoper(
60 | new NullScoper(),
61 | $prefix,
62 | $enrichedReflector,
63 | $symbolsRegistry,
64 | ),
65 | $autoloadPrefixer,
66 | ),
67 | $autoloadPrefixer,
68 | ),
69 | new TraverserFactory(
70 | $enrichedReflector,
71 | $prefix,
72 | $symbolsRegistry,
73 | ),
74 | $this->printer,
75 | ),
76 | $prefix,
77 | $configuration->getPatcher(),
78 | );
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Scoper/Symfony/YamlScoper.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Scoper\Symfony;
16 |
17 | use Humbug\PhpScoper\Scoper\Scoper;
18 | use Humbug\PhpScoper\Symbol\EnrichedReflector;
19 | use Humbug\PhpScoper\Symbol\SymbolsRegistry;
20 | use PhpParser\Node\Name\FullyQualified;
21 | use function array_filter;
22 | use function func_get_args;
23 | use function preg_match as native_preg_match;
24 | use function preg_match_all as native_preg_match_all;
25 | use function str_contains;
26 | use function str_replace;
27 | use function strlen;
28 | use function strpos;
29 | use function substr;
30 |
31 | /**
32 | * Scopes the Symfony YAML configuration files.
33 | */
34 | final readonly class YamlScoper implements Scoper
35 | {
36 | private const YAML_EXTENSION_REGEX = '/\.ya?ml$/i';
37 | private const CLASS_PATTERN = '/(?:(?(?:[\p{L}_][\p{L}_\d]*(?\\\\(?:\\\\)?))):)|(?(?:[\p{L}_][\p{L}_\d]*(?\\\\(?:\\\\)?)+)+[\p{L}_\d]+)/u';
38 |
39 | public function __construct(
40 | private Scoper $decoratedScoper,
41 | private string $prefix,
42 | private EnrichedReflector $enrichedReflector,
43 | private SymbolsRegistry $symbolsRegistry,
44 | ) {
45 | }
46 |
47 | public function scope(string $filePath, string $contents): string
48 | {
49 | if (1 !== native_preg_match(self::YAML_EXTENSION_REGEX, $filePath)) {
50 | return $this->decoratedScoper->scope(...func_get_args());
51 | }
52 |
53 | if (1 > native_preg_match_all(self::CLASS_PATTERN, $contents, $matches)) {
54 | return $contents;
55 | }
56 |
57 | $contents = self::replaceClasses(
58 | array_filter($matches['singleClass']),
59 | array_filter($matches['singleSeparator']),
60 | $this->prefix,
61 | $contents,
62 | $this->enrichedReflector,
63 | $this->symbolsRegistry,
64 | );
65 |
66 | return self::replaceClasses(
67 | array_filter($matches['class']),
68 | array_filter($matches['separator']),
69 | $this->prefix,
70 | $contents,
71 | $this->enrichedReflector,
72 | $this->symbolsRegistry,
73 | );
74 | }
75 |
76 | /**
77 | * @param string[] $classes
78 | * @param string[] $separators
79 | */
80 | private static function replaceClasses(
81 | array $classes,
82 | array $separators,
83 | string $prefix,
84 | string $contents,
85 | EnrichedReflector $enrichedReflector,
86 | SymbolsRegistry $symbolsRegistry
87 | ): string {
88 | if ([] === $classes) {
89 | return $contents;
90 | }
91 |
92 | $scopedContents = '';
93 |
94 | foreach ($classes as $index => $class) {
95 | $separator = $separators[$index];
96 |
97 | $psr4Service = $class.$separator.':';
98 |
99 | if (str_contains($contents, $psr4Service)) {
100 | $offset = strpos($contents, $psr4Service) + strlen($psr4Service);
101 |
102 | $stringToScope = substr($contents, 0, $offset);
103 | $contents = substr($contents, $offset);
104 |
105 | $prefixedClass = $prefix.$separator.$class;
106 |
107 | $scopedContents .= $enrichedReflector->belongsToExcludedNamespace($class.$separator.'__UnknownService__')
108 | ? $stringToScope
109 | : str_replace($class, $prefixedClass, $stringToScope);
110 |
111 | continue;
112 | }
113 |
114 | $offset = strpos($contents, $class) + strlen($class);
115 |
116 | $stringToScope = substr($contents, 0, $offset);
117 | $contents = substr($contents, $offset);
118 |
119 | $prefixedClass = $prefix.$separator.$class;
120 |
121 | $scopedContents .= $enrichedReflector->belongsToExcludedNamespace($class)
122 | ? $stringToScope
123 | : str_replace($class, $prefixedClass, $stringToScope);
124 |
125 | if ($enrichedReflector->isExposedClass($class)) {
126 | $symbolsRegistry->recordClass(
127 | new FullyQualified($class),
128 | new FullyQualified($prefixedClass),
129 | );
130 | }
131 | }
132 |
133 | $scopedContents .= $contents;
134 |
135 | return $scopedContents;
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/Scoper/SymfonyScoper.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Scoper;
16 |
17 | use Humbug\PhpScoper\Scoper\Symfony\XmlScoper as SymfonyXmlScoper;
18 | use Humbug\PhpScoper\Scoper\Symfony\YamlScoper as SymfonyYamlScoper;
19 | use Humbug\PhpScoper\Symbol\EnrichedReflector;
20 | use Humbug\PhpScoper\Symbol\SymbolsRegistry;
21 | use function func_get_args;
22 |
23 | /**
24 | * Scopes the Symfony configuration related files.
25 | */
26 | final class SymfonyScoper implements Scoper
27 | {
28 | private readonly SymfonyXmlScoper $decoratedScoper;
29 |
30 | public function __construct(
31 | Scoper $decoratedScoper,
32 | string $prefix,
33 | EnrichedReflector $enrichedReflector,
34 | SymbolsRegistry $symbolsRegistry
35 | ) {
36 | $this->decoratedScoper = new SymfonyXmlScoper(
37 | new SymfonyYamlScoper(
38 | $decoratedScoper,
39 | $prefix,
40 | $enrichedReflector,
41 | $symbolsRegistry,
42 | ),
43 | $prefix,
44 | $enrichedReflector,
45 | $symbolsRegistry,
46 | );
47 | }
48 |
49 | public function scope(string $filePath, string $contents): string
50 | {
51 | return $this->decoratedScoper->scope(...func_get_args());
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Symbol/EnrichedReflector.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Symbol;
16 |
17 | use Humbug\PhpScoper\Configuration\SymbolsConfiguration;
18 | use function ltrim;
19 | use function str_contains;
20 |
21 | /**
22 | * Combines the API or the "traditional" reflector which is about to tell
23 | * if a symbol is internal or not with the more PHP-Scoper specific exposed
24 | * API.
25 | *
26 | * To recap, the configurations to take into account:
27 | * - excluded & exposed namespaces (exclusion takes priority)
28 | * - internal symbols
29 | * - exposed symbols
30 | * - whether symbols from the global namespace should be exposed or not
31 | */
32 | final readonly class EnrichedReflector
33 | {
34 | public function __construct(
35 | private Reflector $reflector,
36 | private SymbolsConfiguration $symbolsConfiguration,
37 | ) {
38 | }
39 |
40 | public function belongsToExcludedNamespace(string $name): bool
41 | {
42 | return $this->symbolsConfiguration
43 | ->getExcludedNamespaces()
44 | ->belongsToRegisteredNamespace($name);
45 | }
46 |
47 | private function belongsToExposedNamespace(string $name): bool
48 | {
49 | return $this->symbolsConfiguration
50 | ->getExposedNamespaces()
51 | ->belongsToRegisteredNamespace($name);
52 | }
53 |
54 | public function isFunctionInternal(string $name): bool
55 | {
56 | return $this->reflector->isFunctionInternal($name);
57 | }
58 |
59 | public function isFunctionExcluded(string $name): bool
60 | {
61 | return $this->reflector->isFunctionInternal($name)
62 | || $this->belongsToExcludedNamespace($name);
63 | }
64 |
65 | public function isClassInternal(string $name): bool
66 | {
67 | return $this->reflector->isClassInternal($name);
68 | }
69 |
70 | public function isClassExcluded(string $name): bool
71 | {
72 | return $this->reflector->isClassInternal($name)
73 | || $this->belongsToExcludedNamespace($name);
74 | }
75 |
76 | public function isConstantInternal(string $name): bool
77 | {
78 | return $this->reflector->isConstantInternal($name);
79 | }
80 |
81 | public function isExposedFunction(string $resolvedName): bool
82 | {
83 | return !$this->isFunctionExcluded($resolvedName)
84 | && (
85 | $this->isExposedFunctionFromGlobalNamespaceWithoutExclusionCheck($resolvedName)
86 | || $this->symbolsConfiguration
87 | ->getExposedFunctions()
88 | ->matches($resolvedName)
89 | || $this->belongsToExposedNamespace($resolvedName)
90 | );
91 | }
92 |
93 | public function isExposedFunctionFromGlobalNamespace(string $resolvedName): bool
94 | {
95 | return !$this->isFunctionExcluded($resolvedName)
96 | && $this->isExposedFunctionFromGlobalNamespaceWithoutExclusionCheck($resolvedName);
97 | }
98 |
99 | public function isExposedClass(string $resolvedName): bool
100 | {
101 | return !$this->isClassExcluded($resolvedName)
102 | && (
103 | $this->isExposedClassFromGlobalNamespaceWithoutExclusionCheck($resolvedName)
104 | || $this->symbolsConfiguration
105 | ->getExposedClasses()
106 | ->matches($resolvedName)
107 | || $this->belongsToExposedNamespace($resolvedName)
108 | );
109 | }
110 |
111 | public function isExposedClassFromGlobalNamespace(string $resolvedName): bool
112 | {
113 | return !$this->isClassExcluded($resolvedName)
114 | && $this->isExposedClassFromGlobalNamespaceWithoutExclusionCheck($resolvedName);
115 | }
116 |
117 | public function isExposedConstant(string $name): bool
118 | {
119 | // Special case: internal constants must be treated as exposed symbols.
120 | //
121 | // Example: when declaring a new internal constant for compatibility
122 | // reasons, it must remain un-prefixed.
123 | return !$this->belongsToExcludedNamespace($name)
124 | && (
125 | $this->reflector->isConstantInternal($name)
126 | || $this->isExposedConstantFromGlobalNamespace($name)
127 | || $this->symbolsConfiguration
128 | ->getExposedConstants()
129 | ->matches($name)
130 | || $this->belongsToExposedNamespace($name)
131 | );
132 | }
133 |
134 | public function isExposedConstantFromGlobalNamespace(string $constantName): bool
135 | {
136 | return $this->symbolsConfiguration->shouldExposeGlobalConstants()
137 | && $this->belongsToGlobalNamespace($constantName);
138 | }
139 |
140 | public function isExcludedNamespace(string $name): bool
141 | {
142 | return $this->symbolsConfiguration
143 | ->getExcludedNamespaces()
144 | ->isRegisteredNamespace($name);
145 | }
146 |
147 | private function isExposedFunctionFromGlobalNamespaceWithoutExclusionCheck(string $functionName): bool
148 | {
149 | return $this->symbolsConfiguration->shouldExposeGlobalFunctions()
150 | && $this->belongsToGlobalNamespace($functionName);
151 | }
152 |
153 | private function isExposedClassFromGlobalNamespaceWithoutExclusionCheck(string $className): bool
154 | {
155 | return $this->symbolsConfiguration->shouldExposeGlobalClasses()
156 | && $this->belongsToGlobalNamespace($className);
157 | }
158 |
159 | public function belongsToGlobalNamespace(string $symbolName): bool
160 | {
161 | return !str_contains(
162 | ltrim($symbolName, '\\'),
163 | '\\',
164 | );
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/src/Symbol/EnrichedReflectorFactory.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Symbol;
16 |
17 | use Humbug\PhpScoper\Configuration\SymbolsConfiguration;
18 |
19 | final readonly class EnrichedReflectorFactory
20 | {
21 | public function __construct(private Reflector $reflector)
22 | {
23 | }
24 |
25 | public function create(SymbolsConfiguration $symbolsConfiguration): EnrichedReflector
26 | {
27 | $configuredReflector = $this->reflector->withAdditionalSymbols(
28 | $symbolsConfiguration->getExcludedClasses(),
29 | $symbolsConfiguration->getExcludedFunctions(),
30 | $symbolsConfiguration->getExcludedConstants(),
31 | );
32 |
33 | return new EnrichedReflector(
34 | $configuredReflector,
35 | $symbolsConfiguration,
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Symbol/NamespaceRegistry.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Symbol;
16 |
17 | use function array_filter;
18 | use function array_map;
19 | use function array_pop;
20 | use function array_unique;
21 | use function array_values;
22 | use function count;
23 | use function explode;
24 | use function implode;
25 | use function ltrim;
26 | use function Safe\preg_match;
27 | use function str_contains;
28 | use function strtolower;
29 | use function trim;
30 | use const SORT_STRING;
31 |
32 | final readonly class NamespaceRegistry
33 | {
34 | private bool $containsGlobalNamespace;
35 |
36 | /**
37 | * @param string[] $namespaceNames
38 | * @param string[] $namespaceRegexes
39 | */
40 | public static function create(
41 | array $namespaceNames = [],
42 | array $namespaceRegexes = []
43 | ): self {
44 | return new self(
45 | array_values(
46 | array_unique(
47 | array_map(
48 | static fn (string $namespaceName) => strtolower(trim($namespaceName, '\\')),
49 | $namespaceNames,
50 | ),
51 | SORT_STRING,
52 | ),
53 | ),
54 | array_values(
55 | array_unique($namespaceRegexes, SORT_STRING),
56 | ),
57 | );
58 | }
59 |
60 | /**
61 | * @param list $names
62 | * @param list $regexes
63 | */
64 | private function __construct(
65 | private array $names,
66 | private array $regexes
67 | ) {
68 | $this->containsGlobalNamespace = count(
69 | array_filter(
70 | $names,
71 | static fn (string $name) => '' === $name,
72 | ),
73 | ) !== 0;
74 | }
75 |
76 | public function belongsToRegisteredNamespace(string $symbolName): bool
77 | {
78 | return $this->isRegisteredNamespace(
79 | self::extractNameNamespace($symbolName),
80 | );
81 | }
82 |
83 | /**
84 | * Checks if the given namespace matches one of the registered namespace
85 | * names, is a sub-namespace of a registered namespace name or matches any
86 | * regex provided.
87 | */
88 | public function isRegisteredNamespace(string $namespaceName): bool
89 | {
90 | if ($this->containsGlobalNamespace) {
91 | return true;
92 | }
93 |
94 | $originalNamespaceName = ltrim($namespaceName, '\\');
95 | $normalizedNamespaceName = strtolower($originalNamespaceName);
96 |
97 | foreach ($this->names as $excludedNamespaceName) {
98 | if ('' === $excludedNamespaceName
99 | || str_contains($normalizedNamespaceName, $excludedNamespaceName)
100 | ) {
101 | return true;
102 | }
103 | }
104 |
105 | foreach ($this->regexes as $excludedNamespace) {
106 | if (preg_match($excludedNamespace, $originalNamespaceName)) {
107 | return true;
108 | }
109 | }
110 |
111 | return false;
112 | }
113 |
114 | /**
115 | * @internal
116 | *
117 | * @return list
118 | */
119 | public function getNames(): array
120 | {
121 | return $this->names;
122 | }
123 |
124 | /**
125 | * @internal
126 | *
127 | * @return list
128 | */
129 | public function getRegexes(): array
130 | {
131 | return $this->regexes;
132 | }
133 |
134 | private static function extractNameNamespace(string $name): string
135 | {
136 | $nameParts = explode('\\', $name);
137 |
138 | array_pop($nameParts);
139 |
140 | return [] === $nameParts ? '' : implode('\\', $nameParts);
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/Symbol/SymbolRegistry.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Symbol;
16 |
17 | use InvalidArgumentException;
18 | use function array_flip;
19 | use function array_key_exists;
20 | use function array_keys;
21 | use function array_map;
22 | use function array_pop;
23 | use function array_unique;
24 | use function array_values;
25 | use function explode;
26 | use function implode;
27 | use function ltrim;
28 | use function Safe\preg_match;
29 | use function strtolower;
30 | use function trim;
31 |
32 | final class SymbolRegistry
33 | {
34 | /**
35 | * @var array
36 | */
37 | private readonly array $names;
38 |
39 | /**
40 | * @param string[] $names
41 | * @param string[] $regexes
42 | */
43 | public static function create(
44 | array $names = [],
45 | array $regexes = []
46 | ): self {
47 | return new self(
48 | array_values(self::normalizeNames($names)),
49 | array_values(array_unique($regexes)),
50 | false,
51 | );
52 | }
53 |
54 | /**
55 | * Unlike classes & functions, constants are not case-insensitive (although
56 | * the namespace part _is_). I.e. \Acme\FOO = \ACME\FOO but Acme\FOO ≠ Acme\Foo.
57 | *
58 | * @param string[] $names
59 | * @param string[] $regexes
60 | */
61 | public static function createForConstants(
62 | array $names = [],
63 | array $regexes = []
64 | ): self {
65 | return new self(
66 | array_values(self::normalizeConstantNames($names)),
67 | array_values(array_unique($regexes)),
68 | true,
69 | );
70 | }
71 |
72 | /**
73 | * @param list $names
74 | * @param list $regexes
75 | */
76 | private function __construct(
77 | array $names,
78 | private readonly array $regexes,
79 | private readonly bool $constants
80 | ) {
81 | $this->names = array_flip($names);
82 |
83 | if (array_key_exists('', $this->names)) {
84 | throw new InvalidArgumentException('Cannot register "" as a symbol name.');
85 | }
86 |
87 | if (array_key_exists('', array_flip($regexes))) {
88 | throw new InvalidArgumentException('Cannot register "" as a symbol regex.');
89 | }
90 | }
91 |
92 | public function matches(string $symbol): bool
93 | {
94 | $originalSymbol = ltrim($symbol, '\\');
95 | $symbol = $this->constants
96 | ? self::lowerCaseConstantName($originalSymbol)
97 | : strtolower($originalSymbol);
98 |
99 | if (array_key_exists($symbol, $this->names)) {
100 | return true;
101 | }
102 |
103 | foreach ($this->regexes as $regex) {
104 | if (preg_match($regex, $originalSymbol)) {
105 | return true;
106 | }
107 | }
108 |
109 | return false;
110 | }
111 |
112 | public function merge(self $registry): self
113 | {
114 | if ($this->constants !== $registry->constants) {
115 | throw new InvalidArgumentException('Cannot merge registries of different symbol types');
116 | }
117 |
118 | $args = [
119 | [
120 | ...$this->getNames(),
121 | ...$registry->getNames(),
122 | ],
123 | [
124 | ...$this->getRegexes(),
125 | ...$registry->getRegexes(),
126 | ],
127 | ];
128 |
129 | return $this->constants
130 | ? self::createForConstants(...$args)
131 | : self::create(...$args);
132 | }
133 |
134 | /**
135 | * @internal
136 | *
137 | * @return list
138 | */
139 | public function getNames(): array
140 | {
141 | return array_keys($this->names);
142 | }
143 |
144 | /**
145 | * @internal
146 | *
147 | * @return list
148 | */
149 | public function getRegexes(): array
150 | {
151 | return $this->regexes;
152 | }
153 |
154 | /**
155 | * @param string[] $names
156 | *
157 | * @return string[]
158 | */
159 | private static function normalizeNames(array $names): array
160 | {
161 | return array_map(
162 | static fn (string $name) => strtolower(
163 | self::normalizeName($name),
164 | ),
165 | $names,
166 | );
167 | }
168 |
169 | /**
170 | * @param string[] $names
171 | *
172 | * @return string[]
173 | */
174 | private static function normalizeConstantNames(array $names): array
175 | {
176 | return array_map(
177 | static fn (string $name) => self::lowerCaseConstantName(
178 | self::normalizeName($name),
179 | ),
180 | $names,
181 | );
182 | }
183 |
184 | private static function normalizeName(string $name): string
185 | {
186 | return trim($name, '\\ ');
187 | }
188 |
189 | /**
190 | * Transforms the constant FQ name "Acme\Foo\X" to "acme\foo\X" since the
191 | * namespace remains case-insensitive for constants regardless of whether
192 | * constants actually are case-insensitive.
193 | */
194 | private static function lowerCaseConstantName(string $name): string
195 | {
196 | $parts = explode('\\', $name);
197 |
198 | $lastPart = array_pop($parts);
199 |
200 | $parts = array_map('strtolower', $parts);
201 |
202 | $parts[] = $lastPart;
203 |
204 | return implode('\\', $parts);
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/src/Symbol/SymbolsRegistry.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Symbol;
16 |
17 | use Countable;
18 | use PhpParser\Node\Name\FullyQualified;
19 | use function array_values;
20 | use function count;
21 | use function serialize;
22 | use function unserialize;
23 |
24 | final class SymbolsRegistry implements Countable
25 | {
26 | /**
27 | * @var array
28 | */
29 | private array $recordedFunctions = [];
30 |
31 | /**
32 | * @var array
33 | */
34 | private array $recordedClasses = [];
35 |
36 | /**
37 | * @param array $functions
38 | * @param array $classes
39 | */
40 | public static function create(
41 | array $functions = [],
42 | array $classes = [],
43 | ): self {
44 | $registry = new self();
45 |
46 | foreach ($functions as [$original, $alias]) {
47 | $registry->recordFunction(
48 | $original instanceof FullyQualified ? $original : new FullyQualified($original),
49 | $alias instanceof FullyQualified ? $alias : new FullyQualified($alias),
50 | );
51 | }
52 |
53 | foreach ($classes as [$original, $alias]) {
54 | $registry->recordClass(
55 | $original instanceof FullyQualified ? $original : new FullyQualified($original),
56 | $alias instanceof FullyQualified ? $alias : new FullyQualified($alias),
57 | );
58 | }
59 |
60 | return $registry;
61 | }
62 |
63 | /**
64 | * @param self[] $symbolsRegistries
65 | */
66 | public static function createFromRegistries(array $symbolsRegistries): self
67 | {
68 | $symbolsRegistry = new self();
69 |
70 | foreach ($symbolsRegistries as $symbolsRegistryToMerge) {
71 | $symbolsRegistry->merge($symbolsRegistryToMerge);
72 | }
73 |
74 | return $symbolsRegistry;
75 | }
76 |
77 | public static function unserialize(string $serialized): self
78 | {
79 | return unserialize(
80 | $serialized,
81 | ['allowed_classes' => [self::class]],
82 | );
83 | }
84 |
85 | public function serialize(): string
86 | {
87 | return serialize($this);
88 | }
89 |
90 | public function merge(self $symbolsRegistry): void
91 | {
92 | foreach ($symbolsRegistry->getRecordedFunctions() as [$original, $alias]) {
93 | $this->recordedFunctions[$original] = [$original, $alias];
94 | }
95 |
96 | foreach ($symbolsRegistry->getRecordedClasses() as [$original, $alias]) {
97 | $this->recordedClasses[$original] = [$original, $alias];
98 | }
99 | }
100 |
101 | public function recordFunction(FullyQualified $original, FullyQualified $alias): void
102 | {
103 | $this->recordedFunctions[(string) $original] = [(string) $original, (string) $alias];
104 | }
105 |
106 | /**
107 | * @return list
108 | */
109 | public function getRecordedFunctions(): array
110 | {
111 | return array_values($this->recordedFunctions);
112 | }
113 |
114 | public function recordClass(FullyQualified $original, FullyQualified $alias): void
115 | {
116 | $this->recordedClasses[(string) $original] = [(string) $original, (string) $alias];
117 | }
118 |
119 | /**
120 | * @return array{string, string}|null
121 | */
122 | public function getRecordedClass(string $original): ?array
123 | {
124 | return $this->recordedClasses[$original] ?? null;
125 | }
126 |
127 | /**
128 | * @return list
129 | */
130 | public function getRecordedClasses(): array
131 | {
132 | return array_values($this->recordedClasses);
133 | }
134 |
135 | public function count(): int
136 | {
137 | return count($this->recordedFunctions) + count($this->recordedClasses);
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/Throwable/Exception/ParsingException.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Throwable\Exception;
16 |
17 | use Throwable;
18 |
19 | final class ParsingException extends RuntimeException
20 | {
21 | public static function forFile(string $filePath, Throwable $previous): self
22 | {
23 | return new self(
24 | sprintf(
25 | 'Could not parse the file "%s".',
26 | $filePath,
27 | ),
28 | previous: $previous,
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Throwable/Exception/RuntimeException.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper\Throwable\Exception;
16 |
17 | use RuntimeException as RootRuntimeException;
18 |
19 | class RuntimeException extends RootRuntimeException
20 | {
21 | }
22 |
--------------------------------------------------------------------------------
/src/functions.php:
--------------------------------------------------------------------------------
1 | ,
9 | * Pádraic Brady
10 | *
11 | * For the full copyright and license information, please view the LICENSE
12 | * file that was distributed with this source code.
13 | */
14 |
15 | namespace Humbug\PhpScoper;
16 |
17 | use Composer\InstalledVersions;
18 | use Iterator;
19 | use function str_starts_with;
20 | use function substr;
21 |
22 | function get_php_scoper_version(): string
23 | {
24 | // Since PHP-Scoper relies on COMPOSER_ROOT_VERSION the version parsed by PackageVersions, we rely on Box
25 | // placeholders in order to get the right version for the PHAR.
26 | if (str_starts_with(__FILE__, 'phar:')) {
27 | return '@git_version_placeholder@';
28 | }
29 |
30 | $prettyVersion = InstalledVersions::getPrettyVersion('humbug/php-scoper');
31 | $commitHash = InstalledVersions::getReference('humbug/php-scoper');
32 | $shortCommitHash = null === $commitHash ? 'local' : substr($commitHash, 0, 7);
33 |
34 | return $prettyVersion.'@'.$shortCommitHash;
35 | }
36 |
37 | /**
38 | * @template T
39 | *
40 | * @param iterable ...$iterables
41 | * @return Iterator
42 | */
43 | function chain(iterable ...$iterables): Iterator
44 | {
45 | foreach ($iterables as $iterable) {
46 | yield from $iterable;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/vendor-hotfix/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/humbug/php-scoper/f52d5909d1aa12968d51cf525cc3bbb595508dad/vendor-hotfix/.gitkeep
--------------------------------------------------------------------------------