diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php index 1b59446c6f..c3ecb8b6f6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php @@ -38,6 +38,8 @@ use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\HttpKernel\UriSigner; +use Symfony\Component\Runtime\Runner\Symfony\HttpKernelRunner; +use Symfony\Component\Runtime\Runner\Symfony\ResponseRunner; use Symfony\Component\Runtime\SymfonyRuntime; use Symfony\Component\String\LazyString; use Symfony\Component\String\Slugger\AsciiSlugger; @@ -79,6 +81,8 @@ return static function (ContainerConfigurator $container) { service('argument_resolver'), ]) ->tag('container.hot_path') + ->tag('container.preload', ['class' => HttpKernelRunner::class]) + ->tag('container.preload', ['class' => ResponseRunner::class]) ->tag('container.preload', ['class' => SymfonyRuntime::class]) ->alias(HttpKernelInterface::class, 'http_kernel') diff --git a/src/Symfony/Component/Runtime/GenericRuntime.php b/src/Symfony/Component/Runtime/GenericRuntime.php index 88f33edf27..e9484f755e 100644 --- a/src/Symfony/Component/Runtime/GenericRuntime.php +++ b/src/Symfony/Component/Runtime/GenericRuntime.php @@ -22,8 +22,12 @@ 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. + * It supports the following options: + * - "debug" toggles displaying errors and defaults + * to the "APP_DEBUG" environment variable; + * - "runtimes" maps types to a GenericRuntime implementation + * that knows how to deal with each of them; + * - "error_handler" defines the class to use to handle PHP errors. * * The app-callable can declare arguments among either: * - "array $context" to get a local array similar to $_SERVER; @@ -42,42 +46,48 @@ class_exists(ClosureResolver::class); */ class GenericRuntime implements RuntimeInterface { - private $debug; + protected $options; /** * @param array { * debug?: ?bool, + * runtimes?: ?array, + * error_handler?: string|false, * } $options */ public function __construct(array $options = []) { - $this->debug = $options['debug'] ?? $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? true; + $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 (!\is_bool($debug)) { + $debug = filter_var($debug, \FILTER_VALIDATE_BOOLEAN); } - if ($this->debug) { + if ($debug) { + umask(0000); $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '1'; - $errorHandler = new BasicErrorHandler($this->debug); - set_error_handler($errorHandler); + + if (false !== $errorHandler = ($options['error_handler'] ?? BasicErrorHandler::class)) { + $errorHandler::register($debug); + $options['error_handler'] = false; + } } else { $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'; } + + $this->options = $options; } /** * {@inheritdoc} */ - public function getResolver(callable $callable): ResolverInterface + public function getResolver(callable $callable, \ReflectionFunction $reflector = null): ResolverInterface { if (!$callable instanceof \Closure) { $callable = \Closure::fromCallable($callable); } - $function = new \ReflectionFunction($callable); - $parameters = $function->getParameters(); - + $parameters = ($reflector ?? new \ReflectionFunction($callable))->getParameters(); $arguments = function () use ($parameters) { $arguments = []; @@ -95,7 +105,7 @@ class GenericRuntime implements RuntimeInterface return $arguments; }; - if ($this->debug) { + if ($_SERVER['APP_DEBUG']) { return new DebugClosureResolver($callable, $arguments); } @@ -115,15 +125,19 @@ class GenericRuntime implements RuntimeInterface 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) { + if ($runtime = $this->resolveRuntime(\get_class($application))) { + return $runtime->getRunner($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))); + } + $application = \Closure::fromCallable($application); } - if ($this->debug && ($r = new \ReflectionFunction($application)) && $r->getNumberOfRequiredParameters()) { + if ($_SERVER['APP_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())); } @@ -163,8 +177,56 @@ class GenericRuntime implements RuntimeInterface return $this; } - $r = $parameter->getDeclaringFunction(); + if (!$runtime = $this->getRuntime($type)) { + $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))); + 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", or a runtime named "Symfony\Runtime\%1$sRuntime".', $type, $parameter->name, $r->getFileName(), $r->getStartLine(), get_debug_type($this))); + } + + return $runtime->getArgument($parameter, $type); + } + + protected static function register(self $runtime): self + { + return $runtime; + } + + private function getRuntime(string $type): ?self + { + if (null === $runtime = ($this->options['runtimes'][$type] ?? null)) { + $runtime = 'Symfony\Runtime\\'.$type.'Runtime'; + $runtime = class_exists($runtime) ? $runtime : $this->options['runtimes'][$type] = false; + } + + if (\is_string($runtime)) { + $runtime = $runtime::register($this); + } + + if ($this === $runtime) { + return null; + } + + return $runtime ?: null; + } + + private function resolveRuntime(string $class): ?self + { + if ($runtime = $this->getRuntime($class)) { + return $runtime; + } + + foreach (class_parents($class) as $type) { + if ($runtime = $this->getRuntime($type)) { + return $runtime; + } + } + + foreach (class_implements($class) as $type) { + if ($runtime = $this->getRuntime($type)) { + return $runtime; + } + } + + return null; } } diff --git a/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php b/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php index 87ffec9ff0..a3f0da7ff2 100644 --- a/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php +++ b/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php @@ -18,7 +18,7 @@ namespace Symfony\Component\Runtime\Internal; */ class BasicErrorHandler { - public function __construct(bool $debug) + public static function register(bool $debug): void { error_reporting(-1); @@ -32,10 +32,11 @@ class BasicErrorHandler 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); } + + set_error_handler(new self()); } public function __invoke(int $type, string $message, string $file, int $line): bool diff --git a/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php b/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php index b140d0b230..fc6e5a0c99 100644 --- a/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php +++ b/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php @@ -102,14 +102,12 @@ class ComposerPlugin implements PluginInterface, EventSubscriberInterface 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.'); - } + unset($extra['class'], $extra['autoload_template']); $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]", + '%runtime_options%' => '['.substr(var_export($extra, true), 7, -1)." 'project_dir' => {$projectDir},\n]", ]); file_put_contents(substr_replace($autoloadFile, '_runtime', -4, 0), $code); diff --git a/src/Symfony/Component/Runtime/Internal/Console/ApplicationRuntime.php b/src/Symfony/Component/Runtime/Internal/Console/ApplicationRuntime.php new file mode 100644 index 0000000000..de6b1d9748 --- /dev/null +++ b/src/Symfony/Component/Runtime/Internal/Console/ApplicationRuntime.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Runtime\Symfony\Component\Console; + +use Symfony\Component\Runtime\SymfonyRuntime; + +/** + * @internal + */ +class ApplicationRuntime extends SymfonyRuntime +{ +} diff --git a/src/Symfony/Component/Runtime/Internal/Console/Command/CommandRuntime.php b/src/Symfony/Component/Runtime/Internal/Console/Command/CommandRuntime.php new file mode 100644 index 0000000000..9cc198ea1b --- /dev/null +++ b/src/Symfony/Component/Runtime/Internal/Console/Command/CommandRuntime.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Runtime\Symfony\Component\Console\Command; + +use Symfony\Component\Runtime\SymfonyRuntime; + +/** + * @internal + */ +class CommandRuntime extends SymfonyRuntime +{ +} diff --git a/src/Symfony/Component/Runtime/Internal/Console/Input/InputInterfaceRuntime.php b/src/Symfony/Component/Runtime/Internal/Console/Input/InputInterfaceRuntime.php new file mode 100644 index 0000000000..44360bf508 --- /dev/null +++ b/src/Symfony/Component/Runtime/Internal/Console/Input/InputInterfaceRuntime.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Runtime\Symfony\Component\Console\Input; + +use Symfony\Component\Runtime\SymfonyRuntime; + +/** + * @internal + */ +class InputInterfaceRuntime extends SymfonyRuntime +{ +} diff --git a/src/Symfony/Component/Runtime/Internal/Console/Output/OutputInterfaceRuntime.php b/src/Symfony/Component/Runtime/Internal/Console/Output/OutputInterfaceRuntime.php new file mode 100644 index 0000000000..7c59186adc --- /dev/null +++ b/src/Symfony/Component/Runtime/Internal/Console/Output/OutputInterfaceRuntime.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Runtime\Symfony\Component\Console\Output; + +use Symfony\Component\Runtime\SymfonyRuntime; + +/** + * @internal + */ +class OutputInterfaceRuntime extends SymfonyRuntime +{ +} diff --git a/src/Symfony/Component/Runtime/Internal/HttpFoundation/RequestRuntime.php b/src/Symfony/Component/Runtime/Internal/HttpFoundation/RequestRuntime.php new file mode 100644 index 0000000000..2a3a0bb827 --- /dev/null +++ b/src/Symfony/Component/Runtime/Internal/HttpFoundation/RequestRuntime.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Runtime\Symfony\Component\HttpFoundation; + +use Symfony\Component\Runtime\SymfonyRuntime; + +/** + * @internal + */ +class RequestRuntime extends SymfonyRuntime +{ +} diff --git a/src/Symfony/Component/Runtime/Internal/HttpFoundation/ResponseRuntime.php b/src/Symfony/Component/Runtime/Internal/HttpFoundation/ResponseRuntime.php new file mode 100644 index 0000000000..c70fbfff59 --- /dev/null +++ b/src/Symfony/Component/Runtime/Internal/HttpFoundation/ResponseRuntime.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Runtime\Symfony\Component\HttpFoundation; + +use Symfony\Component\Runtime\SymfonyRuntime; + +/** + * @internal + */ +class ResponseRuntime extends SymfonyRuntime +{ +} diff --git a/src/Symfony/Component/Runtime/Internal/HttpKernel/HttpKernelInterfaceRuntime.php b/src/Symfony/Component/Runtime/Internal/HttpKernel/HttpKernelInterfaceRuntime.php new file mode 100644 index 0000000000..5ce0a3b4ab --- /dev/null +++ b/src/Symfony/Component/Runtime/Internal/HttpKernel/HttpKernelInterfaceRuntime.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Runtime\Symfony\Component\HttKernel; + +use Symfony\Component\Runtime\SymfonyRuntime; + +/** + * @internal + */ +class HttpKernelInterfaceRuntime extends SymfonyRuntime +{ +} diff --git a/src/Symfony/Component/Runtime/Internal/SymfonyErrorHandler.php b/src/Symfony/Component/Runtime/Internal/SymfonyErrorHandler.php new file mode 100644 index 0000000000..bf6e1cfe33 --- /dev/null +++ b/src/Symfony/Component/Runtime/Internal/SymfonyErrorHandler.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Runtime\Internal; + +use Symfony\Component\ErrorHandler\BufferingLogger; +use Symfony\Component\ErrorHandler\DebugClassLoader; +use Symfony\Component\ErrorHandler\ErrorHandler; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class SymfonyErrorHandler +{ + public static function register(bool $debug): void + { + BasicErrorHandler::register($debug); + + if (class_exists(ErrorHandler::class)) { + DebugClassLoader::enable(); + restore_error_handler(); + ErrorHandler::register(new ErrorHandler(new BufferingLogger(), true)); + } + } +} diff --git a/src/Symfony/Component/Runtime/RuntimeInterface.php b/src/Symfony/Component/Runtime/RuntimeInterface.php index bd88202e41..d1ac8790f0 100644 --- a/src/Symfony/Component/Runtime/RuntimeInterface.php +++ b/src/Symfony/Component/Runtime/RuntimeInterface.php @@ -25,7 +25,7 @@ interface RuntimeInterface * * The callable itself should return an object that represents the application to pass to the getRunner() method. */ - public function getResolver(callable $callable): ResolverInterface; + public function getResolver(callable $callable, \ReflectionFunction $reflector = null): ResolverInterface; /** * Returns a callable that knows how to run the passed object and that returns its exit status as int. diff --git a/src/Symfony/Component/Runtime/SymfonyRuntime.php b/src/Symfony/Component/Runtime/SymfonyRuntime.php index 39ae2e91a5..a2cc6cb13c 100644 --- a/src/Symfony/Component/Runtime/SymfonyRuntime.php +++ b/src/Symfony/Component/Runtime/SymfonyRuntime.php @@ -18,38 +18,35 @@ 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\Internal\SymfonyErrorHandler; 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"] + * In addition to the options managed by GenericRuntime, it accepts the following options: + * - "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"]; + * - "use_putenv" to tell Dotenv to set env vars using putenv() (NOT RECOMMENDED.) * - * 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 "debug" / "env" options are not defined, they will fallback to the + * "APP_DEBUG" / "APP_ENV" environment variables, and to the "--env|-e" / "--no-debug" + * command line arguments if "symfony/console" 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. + * When "symfony/error-handler" is installed, it is registered in debug mode. * * On top of the base arguments provided by GenericRuntime, * this runtime can feed the app-callable with arguments of type: @@ -74,7 +71,6 @@ class SymfonyRuntime extends GenericRuntime private $output; private $console; private $command; - private $env; /** * @param array { @@ -85,31 +81,32 @@ class SymfonyRuntime extends GenericRuntime * prod_envs?: ?string[], * dotenv_path?: ?string, * test_envs?: ?string[], + * use_putenv?: ?bool, + * runtimes?: ?array, + * error_handler?: string|false, * } $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)) { + if (isset($_SERVER['argv']) && !isset($options['env']) && class_exists(ArgvInput::class)) { + $this->options = $options; $this->getInput(); } + $_SERVER['APP_ENV'] = $options['env'] ?? $_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? 'dev'; + if (!($options['disable_dotenv'] ?? false) && isset($options['project_dir']) && !class_exists(MissingDotenv::class, false)) { (new Dotenv()) ->setProdEnvs((array) ($options['prod_envs'] ?? ['prod'])) + ->usePutenv($options['use_putenv'] ?? false) ->bootEnv($options['project_dir'].'/'.($options['dotenv_path'] ?? '.env'), 'dev', (array) ($options['test_envs'] ?? ['test'])); $options['debug'] ?? $options['debug'] = '1' === $_SERVER['APP_DEBUG']; + $options['disable_dotenv'] = true; } + $options['error_handler'] ?? $options['error_handler'] = SymfonyErrorHandler::class; + parent::__construct($options); - - if ($_SERVER['APP_DEBUG'] && class_exists(Debug::class)) { - restore_error_handler(); - umask(0000); - Debug::enable(); - } } public function getRunner(?object $application): RunnerInterface @@ -142,7 +139,7 @@ class SymfonyRuntime extends GenericRuntime } set_time_limit(0); - $defaultEnv = null === $this->env ? ($_SERVER['APP_ENV'] ?? 'dev') : null; + $defaultEnv = !isset($this->options['env']) ? ($_SERVER['APP_ENV'] ?? 'dev') : null; $output = $this->output ?? $this->output = new ConsoleOutput(); return new ConsoleApplicationRunner($application, $defaultEnv, $this->getInput(), $output); @@ -180,6 +177,23 @@ class SymfonyRuntime extends GenericRuntime return parent::getArgument($parameter, $type); } + protected static function register(parent $runtime): parent + { + $self = new self($runtime->options + ['runtimes' => []]); + $self->options['runtimes'] += [ + HttpKernelInterface::class => $self, + Request::class => $self, + Response::class => $self, + Application::class => $self, + Command::class => $self, + InputInterface::class => $self, + OutputInterface::class => $self, + ]; + $runtime->options = $self->options; + + return $self; + } + private function getInput(): ArgvInput { if (null !== $this->input) { @@ -188,7 +202,7 @@ class SymfonyRuntime extends GenericRuntime $input = new ArgvInput(); - if (null !== $this->env) { + if (isset($this->options['env'])) { return $this->input = $input; } diff --git a/src/Symfony/Component/Runtime/Tests/phpt/autoload.php b/src/Symfony/Component/Runtime/Tests/phpt/autoload.php index e415bc1de8..78036ad2c4 100644 --- a/src/Symfony/Component/Runtime/Tests/phpt/autoload.php +++ b/src/Symfony/Component/Runtime/Tests/phpt/autoload.php @@ -12,7 +12,8 @@ if (file_exists(dirname(__DIR__, 2).'/vendor/autoload.php')) { } $app = require $_SERVER['SCRIPT_FILENAME']; - $runtime = new SymfonyRuntime($_SERVER['APP_RUNTIME_OPTIONS']); + $runtime = $_SERVER['APP_RUNTIME'] ?? SymfonyRuntime::class; + $runtime = new $runtime($_SERVER['APP_RUNTIME_OPTIONS']); [$app, $args] = $runtime->getResolver($app)->resolve(); exit($runtime->getRunner($app(...$args))->run()); } diff --git a/src/Symfony/Component/Runtime/Tests/phpt/generic-request.php b/src/Symfony/Component/Runtime/Tests/phpt/generic-request.php new file mode 100644 index 0000000000..512b1d5b42 --- /dev/null +++ b/src/Symfony/Component/Runtime/Tests/phpt/generic-request.php @@ -0,0 +1,19 @@ + +--EXPECTF-- +OK request runtime +OK Request foo_bar +OK response runtime diff --git a/src/Symfony/Component/Runtime/Tests/phpt/kernel-loop.php b/src/Symfony/Component/Runtime/Tests/phpt/kernel-loop.php index a213a9f4bf..36167611e5 100644 --- a/src/Symfony/Component/Runtime/Tests/phpt/kernel-loop.php +++ b/src/Symfony/Component/Runtime/Tests/phpt/kernel-loop.php @@ -1,9 +1,9 @@