diff --git a/GenericRuntime.php b/GenericRuntime.php index b96468d..effabe2 100644 --- a/GenericRuntime.php +++ b/GenericRuntime.php @@ -46,7 +46,7 @@ class_exists(ClosureResolver::class); */ class GenericRuntime implements RuntimeInterface { - protected $options; + protected array $options; /** * @param array { @@ -125,14 +125,14 @@ public function getRunner(?object $application): RunnerInterface } 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))); + throw new \LogicException(\sprintf('"%s" doesn\'t know how to handle apps of type "%s".', get_debug_type($this), get_debug_type($application))); } $application = $application(...); } if ($_SERVER[$this->options['debug_var_name']] && ($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())); + 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); @@ -171,7 +171,7 @@ protected function getArgument(\ReflectionParameter $parameter, ?string $type): 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", or a runtime named "Symfony\Runtime\%1$sRuntime".', $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); diff --git a/Internal/ComposerPlugin.php b/Internal/ComposerPlugin.php index 4f49e2b..471aada 100644 --- a/Internal/ComposerPlugin.php +++ b/Internal/ComposerPlugin.php @@ -70,7 +70,7 @@ public function updateAutoloadFile(): void } 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'])); + 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'])); } } @@ -82,6 +82,7 @@ public function updateAutoloadFile(): void $projectDir = substr($projectDir, 3); } + // the hack about __DIR__ is required because composer pre-processes plugins if (!$nestingLevel) { $projectDir = '__'.'DIR__.'.var_export('/'.$projectDir, true); } else { diff --git a/Resolver/DebugClosureResolver.php b/Resolver/DebugClosureResolver.php index 923ae90..026b273 100644 --- a/Resolver/DebugClosureResolver.php +++ b/Resolver/DebugClosureResolver.php @@ -28,7 +28,7 @@ static function (...$arguments) use ($closure) { $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())); + 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/Runner/ClosureRunner.php b/Runner/ClosureRunner.php index 4b12ff1..2b54f5a 100644 --- a/Runner/ClosureRunner.php +++ b/Runner/ClosureRunner.php @@ -36,7 +36,7 @@ public function run(): int 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())); + 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/SymfonyRuntime.php b/SymfonyRuntime.php index 2891815..4667bbd 100644 --- a/SymfonyRuntime.php +++ b/SymfonyRuntime.php @@ -41,6 +41,7 @@ class_exists(MissingDotenv::class, false) || class_exists(Dotenv::class) || clas * - "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 * * 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" @@ -86,6 +87,7 @@ class SymfonyRuntime extends GenericRuntime * env_var_name?: string, * debug_var_name?: string, * dotenv_overload?: ?bool, + * dotenv_extra_paths?: ?string[], * } $options */ public function __construct(array $options = []) @@ -93,6 +95,12 @@ 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)) { @@ -101,14 +109,24 @@ public function __construct(array $options = []) } if (!($options['disable_dotenv'] ?? false) && isset($options['project_dir']) && !class_exists(MissingDotenv::class, false)) { - (new Dotenv($envKey, $debugKey)) + $overrideExistingVars = $options['dotenv_overload'] ?? false; + $dotenv = (new Dotenv($envKey, $debugKey)) ->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['dotenv_overload'] ?? false); + ->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(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) && ($options['dotenv_overload'] ?? false)) { + 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)); + 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)) { @@ -158,10 +176,6 @@ public function getRunner(?object $application): RunnerInterface } 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 = !isset($this->options['env']) ? ($_SERVER[$this->options['env_var_name']] ?? 'dev') : null; $output = $this->output ??= new ConsoleOutput(); @@ -207,10 +221,6 @@ protected static function register(GenericRuntime $runtime): GenericRuntime private function getInput(): ArgvInput { - if (!empty($_GET) && filter_var(ini_get('register_argc_argv'), \FILTER_VALIDATE_BOOL)) { - throw new \Exception('CLI applications cannot be run safely on non-CLI SAPIs with register_argc_argv=On.'); - } - if (isset($this->input)) { return $this->input; } diff --git a/Tests/phpt/.env.extra b/Tests/phpt/.env.extra new file mode 100644 index 0000000..0e7e46a --- /dev/null +++ b/Tests/phpt/.env.extra @@ -0,0 +1 @@ +SOME_VAR=foo_bar_extra diff --git a/Tests/phpt/dotenv_extra_load.php b/Tests/phpt/dotenv_extra_load.php new file mode 100644 index 0000000..3564499 --- /dev/null +++ b/Tests/phpt/dotenv_extra_load.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +$_SERVER['SOME_VAR'] = 'ccc'; +$_SERVER['APP_RUNTIME_OPTIONS'] = [ + 'dotenv_extra_paths' => [ + '.env.extra', + ], + 'dotenv_overload' => false, +]; + +require __DIR__.'/autoload.php'; + +return fn (Request $request, array $context) => new Response('OK Request '.$context['SOME_VAR']); diff --git a/Tests/phpt/dotenv_extra_load.phpt b/Tests/phpt/dotenv_extra_load.phpt new file mode 100644 index 0000000..89da5c2 --- /dev/null +++ b/Tests/phpt/dotenv_extra_load.phpt @@ -0,0 +1,12 @@ +--TEST-- +Test Dotenv extra paths load +--INI-- +display_errors=1 +--FILE-- + +--EXPECTF-- +OK Request ccc diff --git a/Tests/phpt/dotenv_extra_overload.php b/Tests/phpt/dotenv_extra_overload.php new file mode 100644 index 0000000..e834257 --- /dev/null +++ b/Tests/phpt/dotenv_extra_overload.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +$_SERVER['SOME_VAR'] = 'ccc'; +$_SERVER['APP_RUNTIME_OPTIONS'] = [ + 'dotenv_extra_paths' => [ + '.env.extra', + ], + 'dotenv_overload' => true, +]; + +require __DIR__.'/autoload.php'; + +return fn (Request $request, array $context) => new Response('OK Request '.$context['SOME_VAR']); diff --git a/Tests/phpt/dotenv_extra_overload.phpt b/Tests/phpt/dotenv_extra_overload.phpt new file mode 100644 index 0000000..88fa4c5 --- /dev/null +++ b/Tests/phpt/dotenv_extra_overload.phpt @@ -0,0 +1,12 @@ +--TEST-- +Test Dotenv extra paths overload +--INI-- +display_errors=1 +--FILE-- + +--EXPECTF-- +OK Request foo_bar_extra diff --git a/Tests/phpt/kernel.php b/Tests/phpt/kernel.php index f664967..7326849 100644 --- a/Tests/phpt/kernel.php +++ b/Tests/phpt/kernel.php @@ -15,8 +15,7 @@ require __DIR__.'/autoload.php'; -class TestKernel implements HttpKernelInterface -{ +return fn (array $context) => new class($context['APP_ENV'], $context['SOME_VAR']) implements HttpKernelInterface { private string $env; private string $var; @@ -30,6 +29,4 @@ public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = tr { return new Response('OK Kernel (env='.$this->env.') '.$this->var); } -} - -return fn (array $context) => new TestKernel($context['APP_ENV'], $context['SOME_VAR']); +}; diff --git a/composer.json b/composer.json index 4241bae..fa9c2cb 100644 --- a/composer.json +++ b/composer.json @@ -16,18 +16,18 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "composer-plugin-api": "^1.0|^2.0" }, "require-dev": { - "composer/composer": "^1.0.2|^2.0", - "symfony/console": "^5.4.9|^6.0.9|^7.0", - "symfony/dotenv": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0" + "composer/composer": "^2.6", + "symfony/console": "^6.4|^7.0", + "symfony/dotenv": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" }, "conflict": { - "symfony/dotenv": "<5.4" + "symfony/dotenv": "<6.4" }, "autoload": { "psr-4": {