diff --git a/.gitattributes b/.gitattributes index 22512cef10..d30fb22a3b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,4 @@ /src/Symfony/Component/Mailer/Bridge export-ignore /src/Symfony/Component/Messenger/Bridge export-ignore /src/Symfony/Component/Notifier/Bridge export-ignore +/src/Symfony/Component/Runtime export-ignore diff --git a/.github/patch-types.php b/.github/patch-types.php index 04335da560..4122b94ccc 100644 --- a/.github/patch-types.php +++ b/.github/patch-types.php @@ -30,6 +30,7 @@ foreach ($loader->getClassMap() as $class => $file) { case false !== strpos($file, '/src/Symfony/Component/ErrorHandler/Tests/Fixtures/'): case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php'): case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php'): + case false !== strpos($file, '/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php'): case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php'): case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/NotLoadableClass.php'): continue 2; diff --git a/composer.json b/composer.json index e8aa4df1e3..f25818b173 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,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", @@ -193,6 +194,10 @@ "symfony/contracts": "2.4.x-dev" } } + }, + { + "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 444127374c..1b59446c6f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php @@ -38,6 +38,7 @@ use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; 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; @@ -78,6 +79,7 @@ return static function (ContainerConfigurator $container) { 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 0000000000..84c7add058 --- /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 0000000000..c49a5d8df5 --- /dev/null +++ b/src/Symfony/Component/Runtime/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Runtime/CHANGELOG.md b/src/Symfony/Component/Runtime/CHANGELOG.md new file mode 100644 index 0000000000..a2badea2db --- /dev/null +++ b/src/Symfony/Component/Runtime/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.3.0 +----- + + * Add the component diff --git a/src/Symfony/Component/Runtime/GenericRuntime.php b/src/Symfony/Component/Runtime/GenericRuntime.php new file mode 100644 index 0000000000..88f33edf27 --- /dev/null +++ b/src/Symfony/Component/Runtime/GenericRuntime.php @@ -0,0 +1,170 @@ + + * + * 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\Resolver\ClosureResolver; +use Symfony\Component\Runtime\Resolver\DebugClosureResolver; +use Symfony\Component\Runtime\Runner\ClosureRunner; + +// Help opcache.preload discover always-needed symbols +class_exists(ClosureResolver::class); + +/** + * A runtime to do bare-metal PHP without using superglobals. + * + * One option named "debug" is supported; it toggles displaying errors + * and defaults to the "APP_ENV" environment variable. + * + * The app-callable 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", "body", "files" and + * "session", which map to $_GET, $_POST, $FILES and &$_SESSION respectively. + * + * It should return a Closure():int|string|null or an instance of RunnerInterface. + * + * In debug mode, the runtime registers a strict error handler + * that throws exceptions when a PHP warning/notice is raised. + * + * @author Nicolas Grekas
+ * + * @experimental in 5.3 + */ +class GenericRuntime implements RuntimeInterface +{ + private $debug; + + /** + * @param array { + * debug?: ?bool, + * } $options + */ + public function __construct(array $options = []) + { + $this->debug = $options['debug'] ?? $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? true; + + if (!\is_bool($this->debug)) { + $this->debug = filter_var($this->debug, \FILTER_VALIDATE_BOOLEAN); + } + + if ($this->debug) { + $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '1'; + $errorHandler = new BasicErrorHandler($this->debug); + set_error_handler($errorHandler); + } else { + $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'; + } + } + + /** + * {@inheritdoc} + */ + public function getResolver(callable $callable): ResolverInterface + { + if (!$callable instanceof \Closure) { + $callable = \Closure::fromCallable($callable); + } + + $function = new \ReflectionFunction($callable); + $parameters = $function->getParameters(); + + $arguments = function () use ($parameters) { + $arguments = []; + + try { + foreach ($parameters as $parameter) { + $type = $parameter->getType(); + $arguments[] = $this->getArgument($parameter, $type instanceof \ReflectionNamedType ? $type->getName() : null); + } + } catch (\InvalidArgumentException $e) { + if (!$parameter->isOptional()) { + throw $e; + } + } + + return $arguments; + }; + + if ($this->debug) { + return new DebugClosureResolver($callable, $arguments); + } + + return new ClosureResolver($callable, $arguments); + } + + /** + * {@inheritdoc} + */ + public function getRunner(?object $application): RunnerInterface + { + if (null === $application) { + $application = static function () { return 0; }; + } + + if ($application instanceof RunnerInterface) { + return $application; + } + + if (!\is_callable($application)) { + throw new \LogicException(sprintf('"%s" doesn\'t know how to handle apps of type "%s".', get_debug_type($this), get_debug_type($application))); + } + + if (!$application instanceof \Closure) { + $application = \Closure::fromCallable($application); + } + + if ($this->debug && ($r = new \ReflectionFunction($application)) && $r->getNumberOfRequiredParameters()) { + throw new \ArgumentCountError(sprintf('Zero argument should be required by the runner callable, but at least one is in "%s" on line "%d.', $r->getFileName(), $r->getStartLine())); + } + + return new ClosureRunner($application); + } + + /** + * @return mixed + */ + protected function getArgument(\ReflectionParameter $parameter, ?string $type) + { + if ('array' === $type) { + 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, + 'body' => $_POST, + 'files' => $_FILES, + 'session' => &$_SESSION, + ]; + } + } + + if (RuntimeInterface::class === $type) { + return $this; + } + + $r = $parameter->getDeclaringFunction(); + + throw new \InvalidArgumentException(sprintf('Cannot resolve argument "%s $%s" in "%s" on line "%d": "%s" supports only arguments "array $context", "array $argv" and "array $request".', $type, $parameter->name, $r->getFileName(), $r->getStartLine(), get_debug_type($this))); + } +} diff --git a/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php b/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php new file mode 100644 index 0000000000..87ffec9ff0 --- /dev/null +++ b/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.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\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 $message, 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($message, 0, $type, $file, $line); + } + + return false; + } +} diff --git a/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php b/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php new file mode 100644 index 0000000000..b140d0b230 --- /dev/null +++ b/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php @@ -0,0 +1,128 @@ + + * + * 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; +use Symfony\Component\Runtime\RuntimeInterface; +use Symfony\Component\Runtime\SymfonyRuntime; + +/** + * @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): void
+ {
+ self::$activated = true;
+ $this->composer = $composer;
+ $this->io = $io;
+ }
+
+ public function deactivate(Composer $composer, IOInterface $io): void
+ {
+ self::$activated = false;
+ }
+
+ public function uninstall(Composer $composer, IOInterface $io): void
+ {
+ @unlink($composer->getConfig()->get('vendor-dir').'/autoload_runtime.php');
+ }
+
+ public function updateAutoloadFile(): void
+ {
+ $vendorDir = $this->composer->getConfig()->get('vendor-dir');
+
+ if (!is_file($autoloadFile = $vendorDir.'/autoload.php')
+ || false === $extra = $this->composer->getPackage()->getExtra()['runtime'] ?? []
+ ) {
+ return;
+ }
+
+ $fs = new Filesystem();
+ $projectDir = \dirname(realpath(Factory::getComposerFile()));
+
+ if (null === $autoloadTemplate = $extra['autoload_template'] ?? null) {
+ $autoloadTemplate = __DIR__.'/autoload_runtime.template';
+ } else {
+ if (!$fs->isAbsolutePath($autoloadTemplate)) {
+ $autoloadTemplate = $projectDir.'/'.$autoloadTemplate;
+ }
+
+ if (!is_file($autoloadTemplate)) {
+ throw new \InvalidArgumentException(sprintf('File "%s" defined under "extra.runtime.autoload_template" in your composer.json file not found.', $this->composer->getPackage()->getExtra()['runtime']['autoload_template']));
+ }
+ }
+
+ $projectDir = $fs->makePathRelative($projectDir, $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) : '');
+ }
+
+ $runtimeClass = $extra['class'] ?? SymfonyRuntime::class;
+
+ if (SymfonyRuntime::class !== $runtimeClass && !is_subclass_of($runtimeClass, RuntimeInterface::class)) {
+ throw new \InvalidArgumentException(sprintf('Class "%s" listed under "extra.runtime.class" in your composer.json file '.(class_exists($runtimeClass) ? 'should implement "%s".' : 'not found.'), $runtimeClass, RuntimeInterface::class));
+ }
+
+ if (!\is_array($runtimeOptions = $extra['options'] ?? [])) {
+ throw new \InvalidArgumentException('The "extra.runtime.options" entry in your composer.json file must be an array.');
+ }
+
+ $code = strtr(file_get_contents($autoloadTemplate), [
+ '%project_dir%' => $projectDir,
+ '%runtime_class%' => var_export($runtimeClass, true),
+ '%runtime_options%' => '['.substr(var_export($runtimeOptions, true), 7, -1)." 'project_dir' => {$projectDir},\n]",
+ ]);
+
+ file_put_contents(substr_replace($autoloadFile, '_runtime', -4, 0), $code);
+ }
+
+ 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 0000000000..896865653e
--- /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/autoload_runtime.template b/src/Symfony/Component/Runtime/Internal/autoload_runtime.template
new file mode 100644
index 0000000000..e6acb1eed6
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Internal/autoload_runtime.template
@@ -0,0 +1,26 @@
+getResolver($app)
+ ->resolve();
+
+$app = $app(...$args);
+
+exit(
+ $runtime
+ ->getRunner($app)
+ ->run()
+);
diff --git a/src/Symfony/Component/Runtime/LICENSE b/src/Symfony/Component/Runtime/LICENSE
new file mode 100644
index 0000000000..efb17f98e7
--- /dev/null
+++ b/src/Symfony/Component/Runtime/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2021 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 0000000000..f9b23b4a02
--- /dev/null
+++ b/src/Symfony/Component/Runtime/README.md
@@ -0,0 +1,112 @@
+Runtime Component
+=================
+
+Symfony Runtime enables decoupling applications from global state.
+
+**This Component is experimental**.
+[Experimental features](https://symfony.com/doc/current/contributing/code/experimental.html)
+are not covered by Symfony's
+[Backward Compatibility Promise](https://symfony.com/doc/current/contributing/code/bc.html).
+
+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. the main entry point returns a callable that wraps the application;
+ 2. this callable is passed to `RuntimeInterface::getResolver()`, which returns a
+ `ResolverInterface`; this resolver returns an array with the (potentially
+ decorated) callable at index 0, and all its resolved arguments at index 1;
+ 3. the callable is invoked with its arguments; it returns an object that
+ represents the application;
+ 4. that object is passed to `RuntimeInterface::getRunner()`, which returns a
+ `RunnerInterface`: an instance that knows how to "run" the object;
+ 5. that instance is `run()` 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. This file shall be required instead of the
+usual `vendor/autoload.php` in front-controllers that leverage this component
+and return a callable.
+
+Before requiring the `vendor/autoload_runtime.php` file, set the
+`$_SERVER['APP_RUNTIME']` variable to a class that implements `RuntimeInterface`
+and that should be used to run the returned callable.
+
+Alternatively, the class of the runtime can be defined in the `extra.runtime.class`
+entry of the `composer.json` file.
+
+A `SymfonyRuntime` is used by default. It knows the conventions to run
+Symfony and native PHP applications.
+
+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 $command->setCode(function (InputInterface $input, OutputInterface $output) {
+ $name = $input->getArgument('name');
+ $output->writeln(sprintf('Hello
+ *
+ * @experimental in 5.3
+ */
+class ClosureResolver implements ResolverInterface
+{
+ private $closure;
+ private $arguments;
+
+ public function __construct(\Closure $closure, \Closure $arguments)
+ {
+ $this->closure = $closure;
+ $this->arguments = $arguments;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resolve(): array
+ {
+ return [$this->closure, ($this->arguments)()];
+ }
+}
diff --git a/src/Symfony/Component/Runtime/Resolver/DebugClosureResolver.php b/src/Symfony/Component/Runtime/Resolver/DebugClosureResolver.php
new file mode 100644
index 0000000000..082ea88965
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Resolver/DebugClosureResolver.php
@@ -0,0 +1,41 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Runtime\Resolver;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @experimental in 5.3
+ */
+class DebugClosureResolver extends ClosureResolver
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function resolve(): array
+ {
+ [$closure, $arguments] = parent::resolve();
+
+ return [
+ static function (...$arguments) use ($closure) {
+ if (\is_object($app = $closure(...$arguments)) || null === $app) {
+ return $app;
+ }
+
+ $r = new \ReflectionFunction($closure);
+
+ throw new \TypeError(sprintf('Unexpected value of type "%s" returned, "object" expected from "%s" on line "%d".', get_debug_type($app), $r->getFileName(), $r->getStartLine()));
+ },
+ $arguments,
+ ];
+ }
+}
diff --git a/src/Symfony/Component/Runtime/ResolverInterface.php b/src/Symfony/Component/Runtime/ResolverInterface.php
new file mode 100644
index 0000000000..4486dbd005
--- /dev/null
+++ b/src/Symfony/Component/Runtime/ResolverInterface.php
@@ -0,0 +1,25 @@
+
+ *
+ * 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
+ *
+ * @experimental in 5.3
+ */
+interface ResolverInterface
+{
+ /**
+ * @return array{0: callable, 1: mixed[]}
+ */
+ public function resolve(): array;
+}
diff --git a/src/Symfony/Component/Runtime/Runner/ClosureRunner.php b/src/Symfony/Component/Runtime/Runner/ClosureRunner.php
new file mode 100644
index 0000000000..470ad082f4
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Runner/ClosureRunner.php
@@ -0,0 +1,48 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Runtime\Runner;
+
+use Symfony\Component\Runtime\RunnerInterface;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @experimental in 5.3
+ */
+class ClosureRunner implements RunnerInterface
+{
+ private $closure;
+
+ public function __construct(\Closure $closure)
+ {
+ $this->closure = $closure;
+ }
+
+ public function run(): int
+ {
+ $exitStatus = ($this->closure)();
+
+ if (\is_string($exitStatus)) {
+ echo $exitStatus;
+
+ return 0;
+ }
+
+ if (null !== $exitStatus && !\is_int($exitStatus)) {
+ $r = new \ReflectionFunction($this->closure);
+
+ throw new \TypeError(sprintf('Unexpected value of type "%s" returned, "string|int|null" expected from "%s" on line "%d".', get_debug_type($exitStatus), $r->getFileName(), $r->getStartLine()));
+ }
+
+ return $exitStatus ?? 0;
+ }
+}
diff --git a/src/Symfony/Component/Runtime/Runner/Symfony/ConsoleApplicationRunner.php b/src/Symfony/Component/Runtime/Runner/Symfony/ConsoleApplicationRunner.php
new file mode 100644
index 0000000000..44a72c5b91
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Runner/Symfony/ConsoleApplicationRunner.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\Runner\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\RunnerInterface;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @experimental in 5.3
+ */
+class ConsoleApplicationRunner implements RunnerInterface
+{
+ 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 run(): 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, $this->output);
+ }
+}
diff --git a/src/Symfony/Component/Runtime/Runner/Symfony/HttpKernelRunner.php b/src/Symfony/Component/Runtime/Runner/Symfony/HttpKernelRunner.php
new file mode 100644
index 0000000000..06a2a7277c
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Runner/Symfony/HttpKernelRunner.php
@@ -0,0 +1,46 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Runtime\Runner\Symfony;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\HttpKernel\TerminableInterface;
+use Symfony\Component\Runtime\RunnerInterface;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @experimental in 5.3
+ */
+class HttpKernelRunner implements RunnerInterface
+{
+ private $kernel;
+ private $request;
+
+ public function __construct(HttpKernelInterface $kernel, Request $request)
+ {
+ $this->kernel = $kernel;
+ $this->request = $request;
+ }
+
+ public function run(): int
+ {
+ $response = $this->kernel->handle($this->request);
+ $response->send();
+
+ if ($this->kernel instanceof TerminableInterface) {
+ $this->kernel->terminate($this->request, $response);
+ }
+
+ return 0;
+ }
+}
diff --git a/src/Symfony/Component/Runtime/Runner/Symfony/ResponseRunner.php b/src/Symfony/Component/Runtime/Runner/Symfony/ResponseRunner.php
new file mode 100644
index 0000000000..1cabcd270c
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Runner/Symfony/ResponseRunner.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Runtime\Runner\Symfony;
+
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Runtime\RunnerInterface;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @experimental in 5.3
+ */
+class ResponseRunner implements RunnerInterface
+{
+ private $response;
+
+ public function __construct(Response $response)
+ {
+ $this->response = $response;
+ }
+
+ public function run(): int
+ {
+ $this->response->send();
+
+ return 0;
+ }
+}
diff --git a/src/Symfony/Component/Runtime/RunnerInterface.php b/src/Symfony/Component/Runtime/RunnerInterface.php
new file mode 100644
index 0000000000..15d242fe74
--- /dev/null
+++ b/src/Symfony/Component/Runtime/RunnerInterface.php
@@ -0,0 +1,22 @@
+
+ *
+ * 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
+ *
+ * @experimental in 5.3
+ */
+interface RunnerInterface
+{
+ public function run(): int;
+}
diff --git a/src/Symfony/Component/Runtime/RuntimeInterface.php b/src/Symfony/Component/Runtime/RuntimeInterface.php
new file mode 100644
index 0000000000..bd88202e41
--- /dev/null
+++ b/src/Symfony/Component/Runtime/RuntimeInterface.php
@@ -0,0 +1,36 @@
+
+ *
+ * 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 applications from global state.
+ *
+ * @author Nicolas Grekas
+ *
+ * @experimental in 5.3
+ */
+interface RuntimeInterface
+{
+ /**
+ * Returns a resolver that should compute the arguments of a callable.
+ *
+ * The callable itself should return an object that represents the application to pass to the getRunner() method.
+ */
+ public function getResolver(callable $callable): ResolverInterface;
+
+ /**
+ * Returns a callable that knows how to run the passed object and that returns its exit status as int.
+ *
+ * The passed object is typically created by calling ResolverInterface::resolve().
+ */
+ public function getRunner(?object $application): RunnerInterface;
+}
diff --git a/src/Symfony/Component/Runtime/SymfonyRuntime.php b/src/Symfony/Component/Runtime/SymfonyRuntime.php
new file mode 100644
index 0000000000..39ae2e91a5
--- /dev/null
+++ b/src/Symfony/Component/Runtime/SymfonyRuntime.php
@@ -0,0 +1,205 @@
+
+ *
+ * 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\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\Runtime\Internal\MissingDotenv;
+use Symfony\Component\Runtime\Runner\Symfony\ConsoleApplicationRunner;
+use Symfony\Component\Runtime\Runner\Symfony\HttpKernelRunner;
+use Symfony\Component\Runtime\Runner\Symfony\ResponseRunner;
+
+// Help opcache.preload discover always-needed symbols
+class_exists(ResponseRunner::class);
+class_exists(HttpKernelRunner::class);
+class_exists(MissingDotenv::class, false) || class_exists(Dotenv::class) || class_exists(MissingDotenv::class);
+
+/**
+ * Knows the basic conventions to run Symfony apps.
+ *
+ * Accepts the following options:
+ * - "debug" to toggle debugging features
+ * - "env" to define the name of the environment the app runs in
+ * - "disable_dotenv" to disable looking for .env files
+ * - "dotenv_path" to define the path of dot-env files - defaults to ".env"
+ * - "prod_envs" to define the names of the production envs - defaults to ["prod"]
+ * - "test_envs" to define the names of the test envs - defaults to ["test"]
+ *
+ * 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 registred in debug mode.
+ *
+ * On top of the base arguments provided by GenericRuntime,
+ * this runtime can feed the app-callable 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-callables that return instances of either:
+ * - HttpKernelInterface,
+ * - Response,
+ * - Application,
+ * - Command,
+ * - int|string|null as handled by GenericRuntime.
+ *
+ * @author Nicolas Grekas
+ *
+ * @experimental in 5.3
+ */
+class SymfonyRuntime extends GenericRuntime
+{
+ private $input;
+ private $output;
+ private $console;
+ private $command;
+ private $env;
+
+ /**
+ * @param array {
+ * debug?: ?bool,
+ * env?: ?string,
+ * disable_dotenv?: ?bool,
+ * project_dir?: ?string,
+ * prod_envs?: ?string[],
+ * dotenv_path?: ?string,
+ * test_envs?: ?string[],
+ * } $options
+ */
+ public function __construct(array $options = [])
+ {
+ $this->env = $options['env'] ?? null;
+ $_SERVER['APP_ENV'] = $options['env'] ?? $_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null;
+
+ if (isset($_SERVER['argv']) && null === $this->env && class_exists(ArgvInput::class)) {
+ $this->getInput();
+ }
+
+ if (!($options['disable_dotenv'] ?? false) && isset($options['project_dir']) && !class_exists(MissingDotenv::class, false)) {
+ (new Dotenv())
+ ->setProdEnvs((array) ($options['prod_envs'] ?? ['prod']))
+ ->bootEnv($options['project_dir'].'/'.($options['dotenv_path'] ?? '.env'), 'dev', (array) ($options['test_envs'] ?? ['test']));
+ $options['debug'] ?? $options['debug'] = '1' === $_SERVER['APP_DEBUG'];
+ }
+
+ parent::__construct($options);
+
+ if ($_SERVER['APP_DEBUG'] && class_exists(Debug::class)) {
+ restore_error_handler();
+ umask(0000);
+ Debug::enable();
+ }
+ }
+
+ public function getRunner(?object $application): RunnerInterface
+ {
+ if ($application instanceof HttpKernelInterface) {
+ return new HttpKernelRunner($application, Request::createFromGlobals());
+ }
+
+ if ($application instanceof Response) {
+ return new ResponseRunner($application);
+ }
+
+ if ($application instanceof Command) {
+ $console = $this->console ?? $this->console = new Application();
+ $console->setName($application->getName() ?: $console->getName());
+
+ if (!$application->getName() || !$console->has($application->getName())) {
+ $application->setName($_SERVER['argv'][0]);
+ $console->add($application);
+ }
+
+ $console->setDefaultCommand($application->getName(), true);
+
+ return $this->getRunner($console);
+ }
+
+ if ($application 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;
+ $output = $this->output ?? $this->output = new ConsoleOutput();
+
+ return new ConsoleApplicationRunner($application, $defaultEnv, $this->getInput(), $output);
+ }
+
+ if ($this->command) {
+ $this->getInput()->bind($this->command->getDefinition());
+ }
+
+ return parent::getRunner($application);
+ }
+
+ /**
+ * @return mixed
+ */
+ protected function getArgument(\ReflectionParameter $parameter, ?string $type)
+ {
+ switch ($type) {
+ case Request::class:
+ return Request::createFromGlobals();
+
+ case InputInterface::class:
+ return $this->getInput();
+
+ case OutputInterface::class:
+ return $this->output ?? $this->output = new ConsoleOutput();
+
+ case Application::class:
+ return $this->console ?? $this->console = 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 0000000000..1853ef1741
--- /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 0000000000..cbe96ed421
--- /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 0000000000..e8e685f0e2
--- /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 0000000000..e415bc1de8
--- /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, $args] = $runtime->getResolver($app)->resolve();
+ exit($runtime->getRunner($app(...$args))->run());
+}
+
+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 0000000000..42d66c1c98
--- /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 0000000000..8d1beeef3f
--- /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/command2.php b/src/Symfony/Component/Runtime/Tests/phpt/command2.php
new file mode 100644
index 0000000000..3c6c08d5d0
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Tests/phpt/command2.php
@@ -0,0 +1,16 @@
+addArgument('name', null, 'Who should I greet?', 'World');
+
+ return static function () use ($input, $output, $context) {
+ $output->writeln(sprintf('Hello %s', $input->getArgument('name')));
+ $output->write('OK Command '.$context['SOME_VAR']);
+ };
+};
diff --git a/src/Symfony/Component/Runtime/Tests/phpt/command2.phpt b/src/Symfony/Component/Runtime/Tests/phpt/command2.phpt
new file mode 100644
index 0000000000..10e234db8f
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Tests/phpt/command2.phpt
@@ -0,0 +1,13 @@
+--TEST--
+Test Command
+--INI--
+display_errors=1
+--FILE--
+
+--EXPECTF--
+Hello World
+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 0000000000..f264b60b29
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Tests/phpt/command_list.php
@@ -0,0 +1,20 @@
+setVersion('1.2.3');
+ $app->setName('Hello console');
+
+ $command->setDescription('Hello description ');
+ $command->setName('my_command');
+
+ [$cmd, $args] = $runtime->getResolver(require __DIR__.'/command.php')->resolve();
+ $app->add($cmd(...$args));
+
+ 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 0000000000..0383b35871
--- /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 %s
+ -q, --quiet Do not output any message
+ -V, --version Display this application version
+ --ansi%A
+ -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 Display%S help for a command
+ list List%S commands
+ my_command Hello description
diff --git a/src/Symfony/Component/Runtime/Tests/phpt/hello.php b/src/Symfony/Component/Runtime/Tests/phpt/hello.php
new file mode 100644
index 0000000000..113e61834c
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Tests/phpt/hello.php
@@ -0,0 +1,10 @@
+
+--EXPECTF--
+Hello World foo_bar
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 0000000000..a213a9f4bf
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Tests/phpt/kernel-loop.php
@@ -0,0 +1,25 @@
+ __DIR__]) extends SymfonyRuntime {
+ public function getRunner(?object $kernel): RunnerInterface
+ {
+ return new ClosureRunner(static function () use ($kernel): int {
+ $kernel->handle(new Request())->send();
+ echo "\n";
+ $kernel->handle(new Request())->send();
+ echo "\n";
+
+ return 0;
+ });
+ }
+};
+
+[$app, $args] = $runtime->getResolver(require __DIR__.'/kernel.php')->resolve();
+echo $runtime->getRunner($app(...$args))->run();
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 0000000000..966007c0d9
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Tests/phpt/kernel-loop.phpt
@@ -0,0 +1,16 @@
+--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 0000000000..e4ca8366e8
--- /dev/null
+++ b/src/Symfony/Component/Runtime/Tests/phpt/kernel.php
@@ -0,0 +1,26 @@
+var = $var;
+ }
+
+ public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
+ {
+ return new Response('OK Kernel '.$this->var);
+ }
+}
+
+return function (array $context) {
+ return new TestKernel($context['SOME_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 0000000000..e739eb0924
--- /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 0000000000..6bc25ef408
--- /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 0000000000..126758b952
--- /dev/null
+++ b/src/Symfony/Component/Runtime/composer.json
@@ -0,0 +1,42 @@
+{
+ "name": "symfony/runtime",
+ "type": "composer-plugin",
+ "description": "Enables decoupling PHP applications 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": {
+ "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 0000000000..7b2c19ae05
--- /dev/null
+++ b/src/Symfony/Component/Runtime/phpunit.xml.dist
@@ -0,0 +1,30 @@
+
+
+