From 61b32ab2a30000f53a1ca97ad115069543deae34 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 applications from global state --- .gitattributes | 1 + .github/patch-types.php | 1 + composer.json | 7 +- .../Resources/config/services.php | 2 + src/Symfony/Component/Runtime/.gitattributes | 4 + src/Symfony/Component/Runtime/.gitignore | 3 + src/Symfony/Component/Runtime/CHANGELOG.md | 7 + .../Component/Runtime/GenericRuntime.php | 170 +++++++++++++++ .../Runtime/Internal/BasicErrorHandler.php | 53 +++++ .../Runtime/Internal/ComposerPlugin.php | 128 +++++++++++ .../Runtime/Internal/MissingDotenv.php | 19 ++ .../Internal/autoload_runtime.template | 26 +++ src/Symfony/Component/Runtime/LICENSE | 19 ++ src/Symfony/Component/Runtime/README.md | 112 ++++++++++ .../Runtime/Resolver/ClosureResolver.php | 39 ++++ .../Runtime/Resolver/DebugClosureResolver.php | 41 ++++ .../Component/Runtime/ResolverInterface.php | 25 +++ .../Runtime/Runner/ClosureRunner.php | 48 ++++ .../Symfony/ConsoleApplicationRunner.php | 58 +++++ .../Runner/Symfony/HttpKernelRunner.php | 46 ++++ .../Runtime/Runner/Symfony/ResponseRunner.php | 37 ++++ .../Component/Runtime/RunnerInterface.php | 22 ++ .../Component/Runtime/RuntimeInterface.php | 36 +++ .../Component/Runtime/SymfonyRuntime.php | 205 ++++++++++++++++++ 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 + .../Component/Runtime/Tests/phpt/command2.php | 16 ++ .../Runtime/Tests/phpt/command2.phpt | 13 ++ .../Runtime/Tests/phpt/command_list.php | 20 ++ .../Runtime/Tests/phpt/command_list.phpt | 38 ++++ .../Component/Runtime/Tests/phpt/hello.php | 10 + .../Component/Runtime/Tests/phpt/hello.phpt | 12 + .../Runtime/Tests/phpt/kernel-loop.php | 25 +++ .../Runtime/Tests/phpt/kernel-loop.phpt | 16 ++ .../Component/Runtime/Tests/phpt/kernel.php | 26 +++ .../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 | 42 ++++ .../Component/Runtime/phpunit.xml.dist | 30 +++ 44 files changed, 1475 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/CHANGELOG.md create mode 100644 src/Symfony/Component/Runtime/GenericRuntime.php 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/autoload_runtime.template create mode 100644 src/Symfony/Component/Runtime/LICENSE create mode 100644 src/Symfony/Component/Runtime/README.md create mode 100644 src/Symfony/Component/Runtime/Resolver/ClosureResolver.php create mode 100644 src/Symfony/Component/Runtime/Resolver/DebugClosureResolver.php create mode 100644 src/Symfony/Component/Runtime/ResolverInterface.php create mode 100644 src/Symfony/Component/Runtime/Runner/ClosureRunner.php create mode 100644 src/Symfony/Component/Runtime/Runner/Symfony/ConsoleApplicationRunner.php create mode 100644 src/Symfony/Component/Runtime/Runner/Symfony/HttpKernelRunner.php create mode 100644 src/Symfony/Component/Runtime/Runner/Symfony/ResponseRunner.php create mode 100644 src/Symfony/Component/Runtime/RunnerInterface.php create mode 100644 src/Symfony/Component/Runtime/RuntimeInterface.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/command2.php create mode 100644 src/Symfony/Component/Runtime/Tests/phpt/command2.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/hello.php create mode 100644 src/Symfony/Component/Runtime/Tests/phpt/hello.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/.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 %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/Resolver/ClosureResolver.php b/src/Symfony/Component/Runtime/Resolver/ClosureResolver.php new file mode 100644 index 0000000000..119387d7f2 --- /dev/null +++ b/src/Symfony/Component/Runtime/Resolver/ClosureResolver.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime\Resolver; + +use Symfony\Component\Runtime\ResolverInterface; + +/** + * @author Nicolas Grekas + * + * @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 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + +