Files
archived-runtime/SymfonyRuntime.php

282 lines
12 KiB
PHP

<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Input\RawInputInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Dotenv\Dotenv;
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\FrankenPhpWorkerRunner;
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(MissingDotenv::class, false) || class_exists(Dotenv::class) || class_exists(MissingDotenv::class);
/**
* Knows the basic conventions to run Symfony apps.
*
* 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.)
* - "dotenv_overload" to tell Dotenv to override existing vars
* - "dotenv_extra_paths" to define a list of additional dot-env files
* - "worker_loop_max" to define the number of requests after which the worker must restart to prevent memory leaks
*
* 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 registered 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 <p@tchwork.com>
*/
class SymfonyRuntime extends GenericRuntime
{
private readonly ArgvInput $input;
private readonly ConsoleOutput $output;
private readonly Application $console;
private readonly Command $command;
private readonly Request $request;
/**
* @param array{
* debug?: ?bool,
* env?: ?string,
* disable_dotenv?: ?bool,
* project_dir?: ?string,
* prod_envs?: ?string[],
* dotenv_path?: ?string,
* test_envs?: ?string[],
* use_putenv?: ?bool,
* runtimes?: ?array,
* error_handler?: string|false,
* env_var_name?: string,
* debug_var_name?: string,
* project_dir_var?: string|false,
* dotenv_overload?: ?bool,
* dotenv_extra_paths?: ?string[],
* worker_loop_max?: int, // Use 0 or a negative integer to never restart the worker. Default: 500
* } $options
*/
public function __construct(array $options = [])
{
$envKey = $options['env_var_name'] ??= 'APP_ENV';
$debugKey = $options['debug_var_name'] ??= 'APP_DEBUG';
if (isset($_SERVER['argv']) && !empty($_GET)) {
// register_argc_argv=On is too risky in web servers
$_SERVER['argv'] = [];
$_SERVER['argc'] = 0;
}
if (isset($options['env'])) {
$_SERVER[$envKey] = $options['env'];
} elseif (empty($_GET) && isset($_SERVER['argv']) && class_exists(ArgvInput::class)) {
$this->options = $options;
$this->getInput();
}
if (isset($options['project_dir']) && $projectDirVar = $options['project_dir_var'] ?? 'APP_PROJECT_DIR') {
$_SERVER[$projectDirVar] = $_ENV[$projectDirVar] = $options['project_dir'];
if ($options['use_putenv'] ?? false) {
putenv($projectDirVar.'='.$options['project_dir']);
}
}
if (!($options['disable_dotenv'] ?? false) && isset($options['project_dir']) && !class_exists(MissingDotenv::class, false)) {
$overrideExistingVars = $options['dotenv_overload'] ?? false;
$dotenv = (new Dotenv($envKey, $debugKey))
->setProdEnvs((array) ($options['prod_envs'] ?? ['prod']))
->usePutenv($options['use_putenv'] ?? false);
$dotenv->bootEnv($options['project_dir'].'/'.($options['dotenv_path'] ?? '.env'), 'dev', (array) ($options['test_envs'] ?? ['test']), $overrideExistingVars);
if (\is_array($options['dotenv_extra_paths'] ?? null) && $options['dotenv_extra_paths']) {
$options['dotenv_extra_paths'] = array_map(static fn (string $path) => $options['project_dir'].'/'.$path, $options['dotenv_extra_paths']);
$overrideExistingVars
? $dotenv->overload(...$options['dotenv_extra_paths'])
: $dotenv->load(...$options['dotenv_extra_paths']);
}
if (isset($this->input) && $overrideExistingVars) {
if ($this->input->getParameterOption(['--env', '-e'], $_SERVER[$envKey], true) !== $_SERVER[$envKey]) {
throw new \LogicException(\sprintf('Cannot use "--env" or "-e" when the "%s" file defines "%s" and the "dotenv_overload" runtime option is true.', $options['dotenv_path'] ?? '.env', $envKey));
}
if ($_SERVER[$debugKey] && $this->input->hasParameterOption('--no-debug', true)) {
putenv($debugKey.'='.$_SERVER[$debugKey] = $_ENV[$debugKey] = '0');
}
}
$options['debug'] ??= '1' === $_SERVER[$debugKey];
$options['disable_dotenv'] = true;
} else {
$_SERVER[$envKey] ??= $_ENV[$envKey] ?? 'dev';
$_SERVER[$debugKey] ??= $_ENV[$debugKey] ?? !\in_array($_SERVER[$envKey], (array) ($options['prod_envs'] ?? ['prod']), true);
}
$options['error_handler'] ??= SymfonyErrorHandler::class;
$workerLoopMax = $options['worker_loop_max'] ?? $_SERVER['FRANKENPHP_LOOP_MAX'] ?? $_ENV['FRANKENPHP_LOOP_MAX'] ?? null;
if (null !== $workerLoopMax && null === filter_var($workerLoopMax, \FILTER_VALIDATE_INT, \FILTER_NULL_ON_FAILURE)) {
throw new \LogicException(\sprintf('The "worker_loop_max" runtime option must be an integer, "%s" given.', get_debug_type($workerLoopMax)));
}
$options['worker_loop_max'] = (int) ($workerLoopMax ?? 500);
parent::__construct($options);
}
public function getRunner(?object $application): RunnerInterface
{
if ($application instanceof HttpKernelInterface) {
if ($_SERVER['FRANKENPHP_WORKER'] ?? false) {
return new FrankenPhpWorkerRunner($application, $this->options['worker_loop_max']);
}
return new HttpKernelRunner($application, $this->resolveType(Request::class), $this->options['debug'] ?? false);
}
if ($application instanceof Response) {
if ($_SERVER['FRANKENPHP_WORKER'] ?? false) {
return new FrankenPhpWorkerRunner($application, $this->options['worker_loop_max']);
}
return new ResponseRunner($application);
}
if ($application instanceof Command) {
$console = $this->resolveType(Application::class);
$console->setName($application->getName() ?: $console->getName());
if (!$application->getName() || !$console->has($application->getName())) {
$application->setName($_SERVER['argv'][0]);
if (!method_exists($console, 'addCommand') || method_exists($console, 'add') && (new \ReflectionMethod($console, 'add'))->class !== (new \ReflectionMethod($console, 'addCommand'))->class) {
$console->add($application);
} else {
$console->addCommand($application);
}
}
$console->setDefaultCommand($application->getName(), true);
$console->getDefinition()->addOptions($application->getDefinition()->getOptions());
return $this->getRunner($console);
}
if ($application instanceof Application) {
set_time_limit(0);
$defaultEnv = !isset($this->options['env']) ? ($_SERVER[$this->options['env_var_name']] ?? 'dev') : null;
$output = $this->resolveType(OutputInterface::class);
return new ConsoleApplicationRunner($application, $defaultEnv, $this->resolveType(InputInterface::class), $output);
}
if (isset($this->command)) {
$this->resolveType(InputInterface::class)->bind($this->command->getDefinition());
}
return parent::getRunner($application);
}
protected function getArgument(\ReflectionParameter $parameter, ?string $type): mixed
{
return $this->resolveType($type) ?? parent::getArgument($parameter, $type);
}
protected function resolveType(string $type): mixed
{
return match ($type) {
Request::class => $this->request ??= Request::createFromGlobals(),
InputInterface::class => $this->getInput(),
RawInputInterface::class => $this->getInput(),
OutputInterface::class => $this->output ??= new ConsoleOutput(),
Application::class => $this->console ??= new Application(),
Command::class => $this->command ??= new Command(),
default => null,
};
}
protected static function register(GenericRuntime $runtime): GenericRuntime
{
$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,
RawInputInterface::class => $self,
OutputInterface::class => $self,
];
$runtime->options = $self->options;
return $self;
}
private function getInput(): ArgvInput
{
if (isset($this->input)) {
return $this->input;
}
$input = new ArgvInput();
if (isset($this->options['env'])) {
return $this->input = $input;
}
if (null !== $env = $input->getParameterOption(['--env', '-e'], null, true)) {
putenv($this->options['env_var_name'].'='.$_SERVER[$this->options['env_var_name']] = $_ENV[$this->options['env_var_name']] = $env);
}
if ($input->hasParameterOption('--no-debug', true)) {
putenv($this->options['debug_var_name'].'='.$_SERVER[$this->options['debug_var_name']] = $_ENV[$this->options['debug_var_name']] = '0');
}
return $this->input = $input;
}
}