diff --git a/.gitattributes b/.gitattributes index 22512cef105ae..d30fb22a3bdbb 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 04335da560504..4122b94ccc492 100644 --- a/.github/patch-types.php +++ b/.github/patch-types.php @@ -30,6 +30,7 @@ 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 e8aa4df1e3af3..f25818b173a41 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 444127374c393..1b59446c6f140 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\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 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : [] 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..c49a5d8df5c65 --- /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 0000000000000..a2badea2db675 --- /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 0000000000000..88f33edf27649 --- /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 0000000000000..87ffec9ff033e --- /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 0000000000000..b140d0b230f7e --- /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 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/autoload_runtime.template b/src/Symfony/Component/Runtime/Internal/autoload_runtime.template
new file mode 100644
index 0000000000000..e6acb1eed6a16
--- /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 0000000000000..efb17f98e7dd3
--- /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 0000000000000..f9b23b4a0223f
--- /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 0000000000000..082ea889651fa
--- /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 0000000000000..4486dbd005875
--- /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 0000000000000..470ad082f4996
--- /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 0000000000000..44a72c5b910cc
--- /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 0000000000000..06a2a7277cdad
--- /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 0000000000000..1cabcd270c684
--- /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 0000000000000..15d242fe74c60
--- /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 0000000000000..bd88202e41196
--- /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 0000000000000..39ae2e91a55ad
--- /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 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..e415bc1de8227
--- /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 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/command2.php b/src/Symfony/Component/Runtime/Tests/phpt/command2.php
new file mode 100644
index 0000000000000..3c6c08d5d0465
--- /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 0000000000000..10e234db8f6f2
--- /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 0000000000000..f264b60b29879
--- /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 0000000000000..0383b35871660
--- /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 0000000000000..113e61834c6c8
--- /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 0000000000000..a213a9f4bf3af
--- /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 0000000000000..966007c0d9fb7
--- /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 0000000000000..e4ca8366e8abe
--- /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 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..126758b9524f4
--- /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 0000000000000..7b2c19ae05cec
--- /dev/null
+++ b/src/Symfony/Component/Runtime/phpunit.xml.dist
@@ -0,0 +1,30 @@
+
+
+ Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages. Alternative Proxies:Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.