From 925cf0ec9293368b8c4f0689596620f7372dd808 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 1 May 2020 12:47:45 +0200 Subject: [PATCH] [Runtime] a new component to decouple apps from global state --- composer.json | 7 +- .../Resources/config/services.php | 2 + src/Symfony/Component/Runtime/.gitattributes | 4 + src/Symfony/Component/Runtime/.gitignore | 3 + src/Symfony/Component/Runtime/BaseRuntime.php | 139 ++++++++++++ src/Symfony/Component/Runtime/CHANGELOG.md | 7 + .../Runtime/Internal/BasicErrorHandler.php | 58 +++++ .../Runtime/Internal/ComposerPlugin.php | 118 +++++++++++ .../Runtime/Internal/MissingDotenv.php | 19 ++ .../Runtime/Internal/MissingErrorHandler.php | 19 ++ src/Symfony/Component/Runtime/LICENSE | 19 ++ src/Symfony/Component/Runtime/README.md | 104 +++++++++ .../Runtime/ResolvedApp/ClosureResolved.php | 42 ++++ .../Runtime/ResolvedApp/ScalarResolved.php | 32 +++ .../ResolvedApp/Symfony/CommandResolved.php | 53 +++++ .../Runtime/ResolvedAppInterface.php | 20 ++ .../Component/Runtime/RuntimeInterface.php | 34 +++ .../Runtime/StartedApp/ClosureStarted.php | 32 +++ .../StartedApp/Symfony/ApplicationStarted.php | 56 +++++ .../StartedApp/Symfony/HttpKernelStarted.php | 47 ++++ .../StartedApp/Symfony/ResponseStarted.php | 35 +++ .../Component/Runtime/StartedAppInterface.php | 20 ++ .../Component/Runtime/SymfonyRuntime.php | 200 ++++++++++++++++++ src/Symfony/Component/Runtime/Tests/phpt/.env | 1 + .../Runtime/Tests/phpt/application.php | 21 ++ .../Runtime/Tests/phpt/application.phpt | 12 ++ .../Component/Runtime/Tests/phpt/autoload.php | 24 +++ .../Component/Runtime/Tests/phpt/command.php | 15 ++ .../Component/Runtime/Tests/phpt/command.phpt | 12 ++ .../Runtime/Tests/phpt/command_list.php | 18 ++ .../Runtime/Tests/phpt/command_list.phpt | 38 ++++ .../Runtime/Tests/phpt/kernel-loop.php | 25 +++ .../Runtime/Tests/phpt/kernel-loop.phpt | 14 ++ .../Component/Runtime/Tests/phpt/kernel.php | 23 ++ .../Component/Runtime/Tests/phpt/kernel.phpt | 12 ++ .../Component/Runtime/Tests/phpt/request.php | 10 + .../Component/Runtime/Tests/phpt/request.phpt | 12 ++ src/Symfony/Component/Runtime/composer.json | 45 ++++ .../Component/Runtime/phpunit.xml.dist | 30 +++ 39 files changed, 1381 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Runtime/.gitattributes create mode 100644 src/Symfony/Component/Runtime/.gitignore create mode 100644 src/Symfony/Component/Runtime/BaseRuntime.php create mode 100644 src/Symfony/Component/Runtime/CHANGELOG.md create mode 100644 src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php create mode 100644 src/Symfony/Component/Runtime/Internal/ComposerPlugin.php create mode 100644 src/Symfony/Component/Runtime/Internal/MissingDotenv.php create mode 100644 src/Symfony/Component/Runtime/Internal/MissingErrorHandler.php create mode 100644 src/Symfony/Component/Runtime/LICENSE create mode 100644 src/Symfony/Component/Runtime/README.md create mode 100644 src/Symfony/Component/Runtime/ResolvedApp/ClosureResolved.php create mode 100644 src/Symfony/Component/Runtime/ResolvedApp/ScalarResolved.php create mode 100644 src/Symfony/Component/Runtime/ResolvedApp/Symfony/CommandResolved.php create mode 100644 src/Symfony/Component/Runtime/ResolvedAppInterface.php create mode 100644 src/Symfony/Component/Runtime/RuntimeInterface.php create mode 100644 src/Symfony/Component/Runtime/StartedApp/ClosureStarted.php create mode 100644 src/Symfony/Component/Runtime/StartedApp/Symfony/ApplicationStarted.php create mode 100644 src/Symfony/Component/Runtime/StartedApp/Symfony/HttpKernelStarted.php create mode 100644 src/Symfony/Component/Runtime/StartedApp/Symfony/ResponseStarted.php create mode 100644 src/Symfony/Component/Runtime/StartedAppInterface.php create mode 100644 src/Symfony/Component/Runtime/SymfonyRuntime.php create mode 100644 src/Symfony/Component/Runtime/Tests/phpt/.env create mode 100644 src/Symfony/Component/Runtime/Tests/phpt/application.php create mode 100644 src/Symfony/Component/Runtime/Tests/phpt/application.phpt create mode 100644 src/Symfony/Component/Runtime/Tests/phpt/autoload.php create mode 100644 src/Symfony/Component/Runtime/Tests/phpt/command.php create mode 100644 src/Symfony/Component/Runtime/Tests/phpt/command.phpt create mode 100644 src/Symfony/Component/Runtime/Tests/phpt/command_list.php create mode 100644 src/Symfony/Component/Runtime/Tests/phpt/command_list.phpt create mode 100644 src/Symfony/Component/Runtime/Tests/phpt/kernel-loop.php create mode 100644 src/Symfony/Component/Runtime/Tests/phpt/kernel-loop.phpt create mode 100644 src/Symfony/Component/Runtime/Tests/phpt/kernel.php create mode 100644 src/Symfony/Component/Runtime/Tests/phpt/kernel.phpt create mode 100644 src/Symfony/Component/Runtime/Tests/phpt/request.php create mode 100644 src/Symfony/Component/Runtime/Tests/phpt/request.phpt create mode 100644 src/Symfony/Component/Runtime/composer.json create mode 100644 src/Symfony/Component/Runtime/phpunit.xml.dist diff --git a/composer.json b/composer.json index b1d530e233186..23b9d4dc94fdb 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,8 @@ "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php73": "^1.11", "symfony/polyfill-php80": "^1.15", - "symfony/polyfill-uuid": "^1.15" + "symfony/polyfill-uuid": "^1.15", + "symfony/runtime": "self.version" }, "replace": { "symfony/asset": "self.version", @@ -168,6 +169,10 @@ { "type": "path", "url": "src/Symfony/Contracts" + }, + { + "type": "path", + "url": "src/Symfony/Component/Runtime" } ], "minimum-stability": "dev", diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php index 84d14a86ec27c..96f8fb19a8e27 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php @@ -50,6 +50,7 @@ use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\HttpKernel\UriSigner; +use Symfony\Component\Runtime\SymfonyRuntime; use Symfony\Component\String\LazyString; use Symfony\Component\String\Slugger\AsciiSlugger; use Symfony\Component\String\Slugger\SluggerInterface; @@ -114,6 +115,7 @@ service('argument_resolver'), ]) ->tag('container.hot_path') + ->tag('container.preload', ['class' => SymfonyRuntime::class]) ->alias(HttpKernelInterface::class, 'http_kernel') ->set('request_stack', RequestStack::class) diff --git a/src/Symfony/Component/Runtime/.gitattributes b/src/Symfony/Component/Runtime/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Runtime/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Runtime/.gitignore b/src/Symfony/Component/Runtime/.gitignore new file mode 100644 index 0000000000000..207f97e0236ed --- /dev/null +++ b/src/Symfony/Component/Runtime/.gitignore @@ -0,0 +1,3 @@ +/vendor/ +/composer.lock +/phpunit.xml diff --git a/src/Symfony/Component/Runtime/BaseRuntime.php b/src/Symfony/Component/Runtime/BaseRuntime.php new file mode 100644 index 0000000000000..4434dc52bc67a --- /dev/null +++ b/src/Symfony/Component/Runtime/BaseRuntime.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime; + +use Symfony\Component\Runtime\Internal\BasicErrorHandler; +use Symfony\Component\Runtime\ResolvedApp\ClosureResolved; +use Symfony\Component\Runtime\ResolvedApp\ScalarResolved; +use Symfony\Component\Runtime\StartedApp\ClosureStarted; + +// Help opcache.preload discover always-needed symbols +class_exists(ClosureResolved::class); +class_exists(BasicErrorHandler::class); + +/** + * A runtime to do bare-metal PHP without using superglobals. + * + * One option named "debug" is supported; it toggles displaying errors. + * + * The app-closure returned by the entry script must return either: + * - "string" to echo the response content, or + * - "int" to set the exit status code. + * + * The app-closure can declare arguments among either: + * - "array $context" to get a local array similar to $_SERVER; + * - "array $argv" to get the command line arguments when running on the CLI; + * - "array $request" to get a local array with keys "query", "data", "files" and + * "session", which map to $_GET, $_POST, $FILES and &$_SESSION respectively. + * + * The runtime sets up a strict error handler that throws + * exceptions when a PHP warning/notice is raised. + * + * @author Nicolas Grekas + */ +class BaseRuntime implements RuntimeInterface +{ + private $debug; + + public function __construct(array $options = []) + { + $this->debug = $options['debug'] ?? true; + $errorHandler = new BasicErrorHandler($this->debug); + set_error_handler($errorHandler); + set_exception_handler([$errorHandler, 'handleException']); + } + + public function resolve(\Closure $app): ResolvedAppInterface + { + $arguments = []; + $function = new \ReflectionFunction($app); + + try { + foreach ($function->getParameters() as $parameter) { + $arguments[] = $this->getArgument($parameter, $parameter->getType()); + } + } catch (\InvalidArgumentException $e) { + if (!$parameter->isOptional()) { + throw $e; + } + } + + $returnType = $function->getReturnType(); + + switch ($returnType instanceof \ReflectionNamedType ? $returnType->getName() : '') { + case 'string': + return new ScalarResolved(static function () use ($app, $arguments): int { + echo $app(...$arguments); + + return 0; + }); + + case 'int': + case 'void': + return new ScalarResolved(static function () use ($app, $arguments): int { + return $app(...$arguments) ?? 0; + }); + } + + return new ClosureResolved($app, $arguments); + } + + public function start(object $app): StartedAppInterface + { + if (!$app instanceof \Closure) { + throw new \LogicException(sprintf('"%s" doesn\'t know how to handle apps of type "%s".', get_debug_type($this), get_debug_type($app))); + } + + if ($this->debug && (new \ReflectionFunction($app))->getNumberOfRequiredParameters()) { + throw new \ArgumentCountError('Zero argument should be required by the closure returned by the app, but at least one is.'); + } + + return new ClosureStarted($app); + } + + protected function getArgument(\ReflectionParameter $parameter, ?\ReflectionType $type) + { + $type = $type instanceof \ReflectionNamedType ? $type->getName() : ''; + + if (RuntimeInterface::class === $type) { + return $this; + } + + if ('array' !== $type) { + throw new \InvalidArgumentException(sprintf('Cannot resolve argument "%s $%s".', $type, $parameter->name)); + } + + switch ($parameter->name) { + case 'context': + $context = $_SERVER; + + if ($_ENV && !isset($_SERVER['PATH']) && !isset($_SERVER['Path'])) { + $context += $_ENV; + } + + return $context; + + case 'argv': + return $_SERVER['argv'] ?? []; + + case 'request': + return [ + 'query' => $_GET, + 'data' => $_POST, + 'files' => $_FILES, + 'session' => &$_SESSION, + ]; + } + + throw new \InvalidArgumentException(sprintf('Cannot resolve array argument "$%s", did you mean "$context" or "$request"?', $parameter->name)); + } +} diff --git a/src/Symfony/Component/Runtime/CHANGELOG.md b/src/Symfony/Component/Runtime/CHANGELOG.md new file mode 100644 index 0000000000000..1e70f9a64318a --- /dev/null +++ b/src/Symfony/Component/Runtime/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.2.0 +----- + + * added the component diff --git a/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php b/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php new file mode 100644 index 0000000000000..4a439928d5bad --- /dev/null +++ b/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime\Internal; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class BasicErrorHandler +{ + public function __construct(bool $debug) + { + error_reporting(-1); + + if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { + ini_set('display_errors', $debug); + } elseif (!filter_var(ini_get('log_errors'), FILTER_VALIDATE_BOOLEAN) || ini_get('error_log')) { + // CLI - display errors only if they're not already logged to STDERR + ini_set('display_errors', 1); + } + + if (0 <= ini_get('zend.assertions')) { + ini_set('zend.assertions', 1); + ini_set('assert.active', $debug); + ini_set('assert.bail', 0); + ini_set('assert.warning', 0); + ini_set('assert.exception', 1); + } + } + + public function __invoke(int $type, string $msg, string $file, int $line): bool + { + if ((E_DEPRECATED | E_USER_DEPRECATED) & $type) { + return true; + } + + if ((error_reporting() | E_ERROR | E_RECOVERABLE_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR) & $type) { + throw new \ErrorException($msg, 0, $type, $file, $line); + } + + return false; + } + + public function handleException(\Throwable $e): void + { + echo "
\n$e\n
\n"; + } +} diff --git a/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php b/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php new file mode 100644 index 0000000000000..3d470baf4a0b7 --- /dev/null +++ b/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime\Internal; + +use Composer\Composer; +use Composer\EventDispatcher\EventSubscriberInterface; +use Composer\Factory; +use Composer\IO\IOInterface; +use Composer\Plugin\PluginInterface; +use Composer\Script\ScriptEvents; +use Symfony\Component\Filesystem\Filesystem; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class ComposerPlugin implements PluginInterface, EventSubscriberInterface +{ + /** + * @var Composer + */ + private $composer; + + /** + * @var IOInterface + */ + private $io; + + private static $activated = false; + + public function activate(Composer $composer, IOInterface $io) + { + self::$activated = true; + $this->composer = $composer; + $this->io = $io; + } + + public function deactivate(Composer $composer, IOInterface $io) + { + self::$activated = false; + } + + public function uninstall(Composer $composer, IOInterface $io) + { + @unlink($composer->getConfig()->get('vendor-dir').'/autoload_runtime.php'); + } + + public function updateAutoloadFile() + { + $vendorDir = $this->composer->getConfig()->get('vendor-dir'); + $autoloadFile = $vendorDir.'/autoload.php'; + + if (!file_exists($autoloadFile)) { + return; + } + + $projectDir = (new Filesystem())->makePathRelative(\dirname(realpath(Factory::getComposerFile())), $vendorDir); + $nestingLevel = 0; + + while (0 === strpos($projectDir, '../')) { + ++$nestingLevel; + $projectDir = substr($projectDir, 3); + } + + if (!$nestingLevel) { + $projectDir = '__DIR__.'.var_export('/'.$projectDir, true); + } else { + $projectDir = "dirname(__DIR__, $nestingLevel)".('' !== $projectDir ? var_export('/'.$projectDir, true) : ''); + } + + $code = <<<'EOPHP' + %project_dir%]); +$app = $runtime->resolve($app)(); +exit($runtime->start($app)()); + +EOPHP; + + file_put_contents(substr_replace($autoloadFile, '_runtime', -4, 0), strtr($code, [ + '%project_dir%' => $projectDir, + ])); + } + + public static function getSubscribedEvents(): array + { + if (!self::$activated) { + return []; + } + + return [ + ScriptEvents::POST_AUTOLOAD_DUMP => 'updateAutoloadFile', + ]; + } +} diff --git a/src/Symfony/Component/Runtime/Internal/MissingDotenv.php b/src/Symfony/Component/Runtime/Internal/MissingDotenv.php new file mode 100644 index 0000000000000..896865653eee8 --- /dev/null +++ b/src/Symfony/Component/Runtime/Internal/MissingDotenv.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime\Internal; + +/** + * @internal class that should be loaded only when symfony/dotenv is not installed + */ +class MissingDotenv +{ +} diff --git a/src/Symfony/Component/Runtime/Internal/MissingErrorHandler.php b/src/Symfony/Component/Runtime/Internal/MissingErrorHandler.php new file mode 100644 index 0000000000000..b2c196504c4da --- /dev/null +++ b/src/Symfony/Component/Runtime/Internal/MissingErrorHandler.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime\Internal; + +/** + * @internal class that should be loaded only when symfony/error-handler is not installed + */ +class MissingErrorHandler +{ +} diff --git a/src/Symfony/Component/Runtime/LICENSE b/src/Symfony/Component/Runtime/LICENSE new file mode 100644 index 0000000000000..5593b1d84f74a --- /dev/null +++ b/src/Symfony/Component/Runtime/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Runtime/README.md b/src/Symfony/Component/Runtime/README.md new file mode 100644 index 0000000000000..eaca0c16ce518 --- /dev/null +++ b/src/Symfony/Component/Runtime/README.md @@ -0,0 +1,104 @@ +Runtime Component +================= + +Symfony Runtime enables decoupling apps from global state. + +Getting Started +--------------- + +``` +$ composer require symfony/runtime +``` + +RuntimeInterface +---------------- + +The core of this component is the `RuntimeInterface` which describes a high-order +runtime logic. + +It is designed to be totally generic and able to run any application outside of +the global state in 6 steps: + + 1. your front-controller returns a closure that wraps your app; + 2. the arguments of this closure are resolved by `RuntimeInterface::resolve()` + which returns a `ResolvedAppInterface`. This is an invokable with zero + arguments that returns whatever object of yours represents your app + (e.g a Symfony kernel or response, a console application or command); + 3. this invokable is called and returns this object that represents your app; + 4. your app object is passed to `RuntimeInterface::start()`, which returns a + `StartedAppInterface`: an invokable that knows how to "run" your app; + 5. that invokable is called and returns the exit status code as int; + 6. the PHP engine is exited with this status code. + +This process is extremely flexible as it allows implementations of +`RuntimeInterface` to hook into any critical steps. + +Autoloading +----------- + +This package registers itself as a Composer plugin to generate a +`vendor/autoload_runtime.php` file. You need to require it instead of the usual +`vendor/autoload.php` in front-controllers that leverage this component and +return a closure. + +Before requiring the `vendor/autoload_runtime.php` file, you can set the +`$_SERVER['APP_RUNTIME']` variable to a class that implements `RuntimeInterface` +and that should be used to run the app. + +A `SymfonyRuntime` is used by default. It knows the conventions to run +Symfony and native PHP apps. + +Examples +-------- + +This `public/index.php` is a "Hello World" that handles a "name" query parameter: +```php +addArgument('name', null, 'Who should I greet?', 'World'); + + return function (InputInterface $input, OutputInterface $output) { + $name = $input->getArgument('name'); + $output->writeln(sprintf('Hello %s', $name)); + }; +}; +``` + +The `SymfonyRuntime` can resolve and handle many types related to the +`symfony/http-foundation` and `symfony/console` components. +Check its source code for more information. + +Resources +--------- + + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/Runtime/ResolvedApp/ClosureResolved.php b/src/Symfony/Component/Runtime/ResolvedApp/ClosureResolved.php new file mode 100644 index 0000000000000..52019bba21d8a --- /dev/null +++ b/src/Symfony/Component/Runtime/ResolvedApp/ClosureResolved.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime\ResolvedApp; + +use Symfony\Component\Runtime\ResolvedAppInterface; + +/** + * @author Nicolas Grekas + */ +class ClosureResolved implements ResolvedAppInterface +{ + private $closure; + private $arguments; + + public function __construct(\Closure $closure, array $arguments) + { + $this->closure = $closure; + $this->arguments = $arguments; + } + + public function __invoke(): object + { + if (\is_object($app = ($this->closure)(...$this->arguments))) { + return $app; + } + + if (null === $app || \is_string($app) || \is_int($app)) { + throw new \TypeError(sprintf('The app returned a value of type "%s" but no explicit return-type was found, did you forget to declare one?', get_debug_type($app))); + } + + throw new \TypeError(sprintf('The app returned a value of type "%s" while an object was expected.', get_debug_type($app))); + } +} diff --git a/src/Symfony/Component/Runtime/ResolvedApp/ScalarResolved.php b/src/Symfony/Component/Runtime/ResolvedApp/ScalarResolved.php new file mode 100644 index 0000000000000..6a8353d315ef3 --- /dev/null +++ b/src/Symfony/Component/Runtime/ResolvedApp/ScalarResolved.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime\ResolvedApp; + +use Symfony\Component\Runtime\ResolvedAppInterface; + +/** + * @author Nicolas Grekas + */ +class ScalarResolved implements ResolvedAppInterface +{ + private $closure; + + public function __construct(\Closure $closure) + { + $this->closure = $closure; + } + + public function __invoke(): object + { + return $this->closure; + } +} diff --git a/src/Symfony/Component/Runtime/ResolvedApp/Symfony/CommandResolved.php b/src/Symfony/Component/Runtime/ResolvedApp/Symfony/CommandResolved.php new file mode 100644 index 0000000000000..1d6ada5e178ef --- /dev/null +++ b/src/Symfony/Component/Runtime/ResolvedApp/Symfony/CommandResolved.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime\ResolvedApp\Symfony; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Runtime\ResolvedAppInterface; + +/** + * @author Nicolas Grekas + */ +class CommandResolved implements ResolvedAppInterface +{ + private $command; + private $resolvedApp; + + public function __construct(Command $command, ResolvedAppInterface $resolvedApp) + { + $this->command = $command; + $this->resolvedApp = $resolvedApp; + } + + public function __invoke(): object + { + if (!($app = ($this->resolvedApp)()) instanceof \Closure) { + return $app; + } + + $parameters = (new \ReflectionFunction($app))->getParameters(); + $types = []; + + foreach ($parameters as $parameter) { + $type = $parameter->getType(); + $types[] = $type instanceof \ReflectionNamedType ? $type->getName() : null; + } + + if ([InputInterface::class, OutputInterface::class] === $types) { + return $this->command->setCode($app); + } + + return $app; + } +} diff --git a/src/Symfony/Component/Runtime/ResolvedAppInterface.php b/src/Symfony/Component/Runtime/ResolvedAppInterface.php new file mode 100644 index 0000000000000..801f70201b4a7 --- /dev/null +++ b/src/Symfony/Component/Runtime/ResolvedAppInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime; + +/** + * @author Nicolas Grekas + */ +interface ResolvedAppInterface +{ + public function __invoke(): object; +} diff --git a/src/Symfony/Component/Runtime/RuntimeInterface.php b/src/Symfony/Component/Runtime/RuntimeInterface.php new file mode 100644 index 0000000000000..5ee1dd03097b5 --- /dev/null +++ b/src/Symfony/Component/Runtime/RuntimeInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime; + +/** + * Enables decoupling apps from global state. + * + * @author Nicolas Grekas + */ +interface RuntimeInterface +{ + /** + * Returns a resolver that should return the object representing your app. + * + * This object representing your app should then be passed to the start() method. + */ + public function resolve(\Closure $app): ResolvedAppInterface; + + /** + * Returns a starter that runs the app and returns its exit status. + * + * The passed object should be created by calling the resolver returned by the resolve() method. + */ + public function start(object $app): StartedAppInterface; +} diff --git a/src/Symfony/Component/Runtime/StartedApp/ClosureStarted.php b/src/Symfony/Component/Runtime/StartedApp/ClosureStarted.php new file mode 100644 index 0000000000000..fbd4d25c75903 --- /dev/null +++ b/src/Symfony/Component/Runtime/StartedApp/ClosureStarted.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime\StartedApp; + +use Symfony\Component\Runtime\StartedAppInterface; + +/** + * @author Nicolas Grekas + */ +class ClosureStarted implements StartedAppInterface +{ + private $app; + + public function __construct(\Closure $app) + { + $this->app = $app; + } + + public function __invoke(): int + { + return ($this->app)() ?? 0; + } +} diff --git a/src/Symfony/Component/Runtime/StartedApp/Symfony/ApplicationStarted.php b/src/Symfony/Component/Runtime/StartedApp/Symfony/ApplicationStarted.php new file mode 100644 index 0000000000000..b675ab2f47905 --- /dev/null +++ b/src/Symfony/Component/Runtime/StartedApp/Symfony/ApplicationStarted.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime\StartedApp\Symfony; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Runtime\StartedAppInterface; + +/** + * @author Nicolas Grekas + */ +class ApplicationStarted implements StartedAppInterface +{ + private $application; + private $defaultEnv; + private $input; + private $output; + + public function __construct(Application $application, ?string $defaultEnv, InputInterface $input, OutputInterface $output = null) + { + $this->application = $application; + $this->defaultEnv = $defaultEnv; + $this->input = $input; + $this->output = $output; + } + + public function __invoke(): int + { + if (null === $this->defaultEnv) { + return $this->application->run($this->input, $this->output); + } + + $definition = $this->application->getDefinition(); + + if (!$definition->hasOption('env') && !$definition->hasOption('e') && !$definition->hasShortcut('e')) { + $definition->addOption(new InputOption('--env', '-e', InputOption::VALUE_REQUIRED, 'The Environment name.', $this->defaultEnv)); + } + + if (!$definition->hasOption('no-debug')) { + $definition->addOption(new InputOption('--no-debug', null, InputOption::VALUE_NONE, 'Switches off debug mode.')); + } + + return $this->application->run($this->input); + } +} diff --git a/src/Symfony/Component/Runtime/StartedApp/Symfony/HttpKernelStarted.php b/src/Symfony/Component/Runtime/StartedApp/Symfony/HttpKernelStarted.php new file mode 100644 index 0000000000000..631cdd1c45e29 --- /dev/null +++ b/src/Symfony/Component/Runtime/StartedApp/Symfony/HttpKernelStarted.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime\StartedApp\Symfony; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\TerminableInterface; +use Symfony\Component\Runtime\RuntimeInterface; +use Symfony\Component\Runtime\StartedAppInterface; + +/** + * @author Nicolas Grekas + */ +class HttpKernelStarted implements StartedAppInterface +{ + private $kernel; + private $request; + private $runtime; + + public function __construct(HttpKernelInterface $kernel, Request $request, RuntimeInterface $runtime) + { + $this->kernel = $kernel; + $this->request = $request; + $this->runtime = $runtime; + } + + public function __invoke(): int + { + $response = $this->kernel->handle($this->request); + $status = $this->runtime->start($response)(); + + if ($this->kernel instanceof TerminableInterface) { + $this->kernel->terminate($this->request, $response); + } + + return $status; + } +} diff --git a/src/Symfony/Component/Runtime/StartedApp/Symfony/ResponseStarted.php b/src/Symfony/Component/Runtime/StartedApp/Symfony/ResponseStarted.php new file mode 100644 index 0000000000000..c2072319419df --- /dev/null +++ b/src/Symfony/Component/Runtime/StartedApp/Symfony/ResponseStarted.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime\StartedApp\Symfony; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Runtime\StartedAppInterface; + +/** + * @author Nicolas Grekas + */ +class ResponseStarted implements StartedAppInterface +{ + private $response; + + public function __construct(Response $response) + { + $this->response = $response; + } + + public function __invoke(): int + { + $this->response->send(); + + return 0; + } +} diff --git a/src/Symfony/Component/Runtime/StartedAppInterface.php b/src/Symfony/Component/Runtime/StartedAppInterface.php new file mode 100644 index 0000000000000..105936850b531 --- /dev/null +++ b/src/Symfony/Component/Runtime/StartedAppInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime; + +/** + * @author Nicolas Grekas + */ +interface StartedAppInterface +{ + public function __invoke(): int; +} diff --git a/src/Symfony/Component/Runtime/SymfonyRuntime.php b/src/Symfony/Component/Runtime/SymfonyRuntime.php new file mode 100644 index 0000000000000..d99aba411ca74 --- /dev/null +++ b/src/Symfony/Component/Runtime/SymfonyRuntime.php @@ -0,0 +1,200 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Dotenv\Dotenv; +use Symfony\Component\ErrorHandler\Debug; +use Symfony\Component\ErrorHandler\ErrorHandler; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Runtime\Internal\MissingDotenv; +use Symfony\Component\Runtime\Internal\MissingErrorHandler; +use Symfony\Component\Runtime\ResolvedApp\Symfony\CommandResolved; +use Symfony\Component\Runtime\StartedApp\Symfony\ApplicationStarted; +use Symfony\Component\Runtime\StartedApp\Symfony\HttpKernelStarted; +use Symfony\Component\Runtime\StartedApp\Symfony\ResponseStarted; + +// Help opcache.preload discover always-needed symbols +class_exists(ResponseStarted::class); +class_exists(HttpKernelStarted::class); +class_exists(MissingDotenv::class, false) || class_exists(Dotenv::class) || class_exists(MissingDotenv::class); +class_exists(MissingErrorHandler::class, false) || class_exists(ErrorHandler::class) || class_exists(MissingErrorHandler::class); + +/** + * Knows the basic conventions to run Symfony apps. + * + * Accepts two options: + * - "debug" to toggle debugging features; + * - "env" to define the name of the environment the app runs in. + * + * When these options are not defined, they will fallback: + * - to reading the "APP_DEBUG" and "APP_ENV" environment variables; + * - to parsing the "--env|-e" and "--no-debug" command line arguments + * if the "symfony/console" component is installed. + * + * When the "symfony/dotenv" component is installed, .env files are loaded. + * When "symfony/error-handler" is installed, it is used to improve error handling. + * + * On top of the base arguments provided by the parent runtime, + * this runtime can feed the app-closure with arguments of type: + * - Request from "symfony/http-foundation" if the component is installed; + * - Application, Command, InputInterface and/or OutputInterface + * from "symfony/console" if the component is installed. + * + * This runtime can handle app-closures that return instances of either: + * - HttpKernelInterface, + * - Response, + * - Application, + * - Command, + * - or Closure(): int + * + * @author Nicolas Grekas + */ +class SymfonyRuntime extends BaseRuntime +{ + private $request; + private $input; + private $output; + private $application; + private $command; + private $env; + + public function __construct(array $options = []) + { + $this->env = $options['env'] ?? null; + $_SERVER['APP_ENV'] = $options['env'] ?? $_SERVER['APP_ENV'] ?? null; + + parent::__construct($options); + + if (isset($_SERVER['argv']) && null === $this->env && class_exists(ArgvInput::class)) { + $this->getInput(); + } + + if (class_exists(MissingDotenv::class, false) || !isset($options['project_dir'])) { + $_SERVER['APP_DEBUG'] = filter_var($options['debug'] ?? $_SERVER['APP_DEBUG'] ?? true, FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; + } else { + (new Dotenv())->bootEnv($options['project_dir'].'/.env'); + } + + if (class_exists(MissingErrorHandler::class, false)) { + // no-op + } elseif ($_SERVER['APP_DEBUG']) { + umask(0000); + Debug::enable(); + } else { + ErrorHandler::register(); + } + } + + public function resolve(\Closure $app): ResolvedAppInterface + { + $app = parent::resolve($app); + + return $this->command ? new CommandResolved($this->command, $app) : $app; + } + + public function start(object $app): StartedAppInterface + { + if ($app instanceof HttpKernelInterface) { + $request = $this->request ?? $this->request = Request::createFromGlobals(); + + return new HttpKernelStarted($app, $request, $this); + } + + if ($app instanceof Response) { + return new ResponseStarted($app); + } + + if ($app instanceof Command) { + $application = $this->application ?? $this->application = new Application(); + $application->setName($app->getName() ?: $application->getName()); + + if (!$app->getName() || !$application->has($app->getName())) { + $app->setName($_SERVER['argv'][0]); + $application->add($app); + } + + $application->setDefaultCommand($app->getName(), true); + + return $this->start($application); + } + + if ($app instanceof Application) { + if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) { + echo 'Warning: The console should be invoked via the CLI version of PHP, not the '.\PHP_SAPI.' SAPI'.PHP_EOL; + } + + set_time_limit(0); + $defaultEnv = null === $this->env ? ($_SERVER['APP_ENV'] ?? 'dev') : null; + + return new ApplicationStarted($app, $defaultEnv, $this->getInput(), $this->output); + } + + return parent::start($app); + } + + protected function getArgument(\ReflectionParameter $parameter, ?\ReflectionType $type) + { + if (!$type instanceof \ReflectionNamedType) { + return parent::getArgument($parameter, $type); + } + + switch ($type->getName()) { + case Request::class: + return $this->request ?? $this->request = Request::createFromGlobals(); + + case InputInterface::class: + return $this->getInput(); + + case OutputInterface::class: + return $this->output ?? $this->output = new ConsoleOutput(); + + case Application::class: + return $this->application ?? $this->application = new Application(); + + case Command::class: + return $this->command ?? $this->command = new Command(); + } + + return parent::getArgument($parameter, $type); + } + + private function getInput(): ArgvInput + { + if (null !== $this->input) { + return $this->input; + } + + $input = new ArgvInput(); + + if (null !== $this->env) { + return $this->input = $input; + } + + if (null !== $env = $input->getParameterOption(['--env', '-e'], null, true)) { + putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); + } + + if ($input->hasParameterOption('--no-debug', true)) { + putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); + } + + return $this->input = $input; + } +} diff --git a/src/Symfony/Component/Runtime/Tests/phpt/.env b/src/Symfony/Component/Runtime/Tests/phpt/.env new file mode 100644 index 0000000000000..1853ef1741a1c --- /dev/null +++ b/src/Symfony/Component/Runtime/Tests/phpt/.env @@ -0,0 +1 @@ +SOME_VAR=foo_bar diff --git a/src/Symfony/Component/Runtime/Tests/phpt/application.php b/src/Symfony/Component/Runtime/Tests/phpt/application.php new file mode 100644 index 0000000000000..cbe96ed421dfc --- /dev/null +++ b/src/Symfony/Component/Runtime/Tests/phpt/application.php @@ -0,0 +1,21 @@ +setCode(function (InputInterface $input, OutputInterface $output) use ($context) { + $output->write('OK Application '.$context['SOME_VAR']); + }); + + $app = new Application(); + $app->add($command); + $app->setDefaultCommand('go', true); + + return $app; +}; diff --git a/src/Symfony/Component/Runtime/Tests/phpt/application.phpt b/src/Symfony/Component/Runtime/Tests/phpt/application.phpt new file mode 100644 index 0000000000000..e8e685f0e2f48 --- /dev/null +++ b/src/Symfony/Component/Runtime/Tests/phpt/application.phpt @@ -0,0 +1,12 @@ +--TEST-- +Test Application +--INI-- +display_errors=1 +--FILE-- + +--EXPECTF-- +OK Application foo_bar diff --git a/src/Symfony/Component/Runtime/Tests/phpt/autoload.php b/src/Symfony/Component/Runtime/Tests/phpt/autoload.php new file mode 100644 index 0000000000000..d4141e182fe9a --- /dev/null +++ b/src/Symfony/Component/Runtime/Tests/phpt/autoload.php @@ -0,0 +1,24 @@ + __DIR__, +]; + +if (file_exists(dirname(__DIR__, 2).'/vendor/autoload.php')) { + if (true === (require_once dirname(__DIR__, 2).'/vendor/autoload.php') || empty($_SERVER['SCRIPT_FILENAME'])) { + return; + } + + $app = require $_SERVER['SCRIPT_FILENAME']; + $runtime = new SymfonyRuntime($_SERVER['APP_RUNTIME_OPTIONS']); + $app = $runtime->resolve($app)(); + exit($runtime->start($app)()); +} + +if (!file_exists(dirname(__DIR__, 6).'/vendor/autoload_runtime.php')) { + throw new LogicException('Autoloader not found.'); +} + +require dirname(__DIR__, 6).'/vendor/autoload_runtime.php'; diff --git a/src/Symfony/Component/Runtime/Tests/phpt/command.php b/src/Symfony/Component/Runtime/Tests/phpt/command.php new file mode 100644 index 0000000000000..42d66c1c98462 --- /dev/null +++ b/src/Symfony/Component/Runtime/Tests/phpt/command.php @@ -0,0 +1,15 @@ +setCode(function () use ($output, $context) { + $output->write('OK Command '.$context['SOME_VAR']); + }); + + return $command; +}; diff --git a/src/Symfony/Component/Runtime/Tests/phpt/command.phpt b/src/Symfony/Component/Runtime/Tests/phpt/command.phpt new file mode 100644 index 0000000000000..8d1beeef3f34d --- /dev/null +++ b/src/Symfony/Component/Runtime/Tests/phpt/command.phpt @@ -0,0 +1,12 @@ +--TEST-- +Test Command +--INI-- +display_errors=1 +--FILE-- + +--EXPECTF-- +OK Command foo_bar diff --git a/src/Symfony/Component/Runtime/Tests/phpt/command_list.php b/src/Symfony/Component/Runtime/Tests/phpt/command_list.php new file mode 100644 index 0000000000000..0f99e3d066c41 --- /dev/null +++ b/src/Symfony/Component/Runtime/Tests/phpt/command_list.php @@ -0,0 +1,18 @@ +setVersion('1.2.3'); + $app->setName('Hello console'); + $command->setDescription('Hello description '); + $command->setName('my_command'); + + $app->add($runtime->resolve(require __DIR__.'/command.php')()); + + return $app; +}; diff --git a/src/Symfony/Component/Runtime/Tests/phpt/command_list.phpt b/src/Symfony/Component/Runtime/Tests/phpt/command_list.phpt new file mode 100644 index 0000000000000..dce7079951361 --- /dev/null +++ b/src/Symfony/Component/Runtime/Tests/phpt/command_list.phpt @@ -0,0 +1,38 @@ +--TEST-- +Test "list" Command +--INI-- +display_errors=1 +--FILE-- + +--EXPECTF-- +Hello console 1.2.3 + +Usage: + command [options] [arguments] + +Options: + -h, --help Display this help message + -q, --quiet Do not output any message + -V, --version Display this application version + --ansi Force ANSI output + --no-ansi Disable ANSI output + -n, --no-interaction Do not ask any interactive question + -e, --env=ENV The Environment name. [default: "prod"] + --no-debug Switches off debug mode. + -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + +Available commands: + help Displays help for a command + list Lists commands + my_command Hello description diff --git a/src/Symfony/Component/Runtime/Tests/phpt/kernel-loop.php b/src/Symfony/Component/Runtime/Tests/phpt/kernel-loop.php new file mode 100644 index 0000000000000..ce47d93315913 --- /dev/null +++ b/src/Symfony/Component/Runtime/Tests/phpt/kernel-loop.php @@ -0,0 +1,25 @@ + __DIR__]) extends SymfonyRuntime { + public function start(object $kernel): StartedAppInterface + { + return new ClosureStarted(static function () use ($kernel): int { + $kernel->handle(new Request())->send(); + echo "\n"; + $kernel->handle(new Request())->send(); + echo "\n"; + + return 0; + }); + } +}; + +$kernel = $runtime->resolve(require __DIR__.'/kernel.php')(); +echo $runtime->start($kernel)(); diff --git a/src/Symfony/Component/Runtime/Tests/phpt/kernel-loop.phpt b/src/Symfony/Component/Runtime/Tests/phpt/kernel-loop.phpt new file mode 100644 index 0000000000000..550645fb7be29 --- /dev/null +++ b/src/Symfony/Component/Runtime/Tests/phpt/kernel-loop.phpt @@ -0,0 +1,14 @@ +--TEST-- +Test HttpKernelInterface +--INI-- +display_errors=1 +--FILE-- + +--EXPECTF-- +OK Kernel foo_bar +OK Kernel foo_bar +0 diff --git a/src/Symfony/Component/Runtime/Tests/phpt/kernel.php b/src/Symfony/Component/Runtime/Tests/phpt/kernel.php new file mode 100644 index 0000000000000..ed5a2feb3a7f3 --- /dev/null +++ b/src/Symfony/Component/Runtime/Tests/phpt/kernel.php @@ -0,0 +1,23 @@ +var = $var; + } + + public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true) + { + return new Response('OK Kernel '.$this->var); + } + }; +}; diff --git a/src/Symfony/Component/Runtime/Tests/phpt/kernel.phpt b/src/Symfony/Component/Runtime/Tests/phpt/kernel.phpt new file mode 100644 index 0000000000000..e739eb092477e --- /dev/null +++ b/src/Symfony/Component/Runtime/Tests/phpt/kernel.phpt @@ -0,0 +1,12 @@ +--TEST-- +Test HttpKernelInterface +--INI-- +display_errors=1 +--FILE-- + +--EXPECTF-- +OK Kernel foo_bar diff --git a/src/Symfony/Component/Runtime/Tests/phpt/request.php b/src/Symfony/Component/Runtime/Tests/phpt/request.php new file mode 100644 index 0000000000000..6bc25ef408a06 --- /dev/null +++ b/src/Symfony/Component/Runtime/Tests/phpt/request.php @@ -0,0 +1,10 @@ + +--EXPECTF-- +OK Request foo_bar diff --git a/src/Symfony/Component/Runtime/composer.json b/src/Symfony/Component/Runtime/composer.json new file mode 100644 index 0000000000000..1018077acfb60 --- /dev/null +++ b/src/Symfony/Component/Runtime/composer.json @@ -0,0 +1,45 @@ +{ + "name": "symfony/runtime", + "type": "composer-plugin", + "description": "Enables decoupling PHP apps from global state", + "homepage": "https://symfony.com", + "license" : "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "composer-plugin-api": "^1.0|^2.0", + "symfony/polyfill-php80": "^1.15" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "symfony/console": "^4.4|^5", + "symfony/dotenv": "^5.1", + "symfony/http-foundation": "^4.4|^5", + "symfony/http-kernel": "^4.4|^5" + }, + "conflict": { + "symfony/dotenv": "<5.1" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Runtime\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "5.2-dev" + }, + "class": "Symfony\\Component\\Runtime\\Internal\\ComposerPlugin" + } +} diff --git a/src/Symfony/Component/Runtime/phpunit.xml.dist b/src/Symfony/Component/Runtime/phpunit.xml.dist new file mode 100644 index 0000000000000..7b2c19ae05cec --- /dev/null +++ b/src/Symfony/Component/Runtime/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + +