├── .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 --------------------------------------------------------------------------------