Merge branch '7.4' into 8.0

* 7.4:
  prioritize property type over is/has/can accessors
  [Dotenv] Windows-related tweak
  [Dotenv] Use `APP_RUNTIME_OPTIONS` variable when dumping dotenv
This commit is contained in:
Christian Flothmann
2026-02-13 13:00:38 +01:00
4 changed files with 92 additions and 31 deletions

View File

@@ -32,7 +32,7 @@ final class DebugCommand extends Command
{
public function __construct(
private string $kernelEnvironment,
private string $projectDirectory,
private string $projectDir,
) {
parent::__construct();
}
@@ -67,29 +67,17 @@ final class DebugCommand extends Command
return 1;
}
if (!$filePath = $_SERVER['SYMFONY_DOTENV_PATH'] ?? null) {
$dotenvPath = $this->projectDirectory;
$dotenvPath = $this->getDotenvPath();
if (is_file($composerFile = $this->projectDirectory.'/composer.json')) {
$runtimeConfig = json_decode(file_get_contents($composerFile), true)['extra']['runtime'] ?? [];
if (isset($runtimeConfig['dotenv_path'])) {
$dotenvPath = $this->projectDirectory.'/'.$runtimeConfig['dotenv_path'];
}
}
$filePath = $dotenvPath.'/.env';
}
$envFiles = $this->getEnvFiles($filePath);
$envFiles = $this->getEnvFiles($dotenvPath);
$availableFiles = array_filter($envFiles, 'is_file');
if (\in_array(\sprintf('%s.local.php', $filePath), $availableFiles, true)) {
$io->warning(\sprintf('Due to existing dump file (%s.local.php) all other dotenv files are skipped.', $this->getRelativeName($filePath)));
if (\in_array(\sprintf('%s.local.php', $dotenvPath), $availableFiles, true)) {
$io->warning(\sprintf('Due to existing dump file (%s.local.php) all other dotenv files are skipped.', $this->getRelativeName($dotenvPath)));
}
if (is_file($filePath) && is_file(\sprintf('%s.dist', $filePath))) {
$io->warning(\sprintf('The file %s.dist gets skipped due to the existence of %1$s.', $this->getRelativeName($filePath)));
if (is_file($dotenvPath) && is_file(\sprintf('%s.dist', $dotenvPath))) {
$io->warning(\sprintf('The file %s.dist gets skipped due to the existence of %1$s.', $this->getRelativeName($dotenvPath)));
}
$io->section('Scanned Files (in descending priority)');
@@ -165,12 +153,27 @@ final class DebugCommand extends Command
private function getAvailableVars(): array
{
$filePath = $_SERVER['SYMFONY_DOTENV_PATH'] ?? $this->projectDirectory.\DIRECTORY_SEPARATOR.'.env';
$envFiles = $this->getEnvFiles($filePath);
$envFiles = $this->getEnvFiles($this->getDotenvPath());
return array_keys($this->getVariables(array_filter($envFiles, 'is_file'), null));
}
private function getDotenvPath(): string
{
$config = [];
$projectDir = $this->projectDir;
if (is_file($projectDir)) {
$config = ['dotenv_path' => basename($projectDir)];
$projectDir = \dirname($projectDir);
}
$composerFile = $projectDir.'/composer.json';
$config += $_SERVER['APP_RUNTIME_OPTIONS'] ?? (is_file($composerFile) ? json_decode(file_get_contents($composerFile), true) : [])['extra']['runtime'] ?? [];
return $projectDir.'/'.($config['dotenv_path'] ?? '.env');
}
private function getEnvFiles(string $filePath): array
{
$files = [
@@ -194,8 +197,10 @@ final class DebugCommand extends Command
private function getRelativeName(string $filePath): string
{
if (str_starts_with($filePath, $this->projectDirectory)) {
return substr($filePath, \strlen($this->projectDirectory) + 1);
$projectDir = is_file($this->projectDir) ? \dirname($this->projectDir) : $this->projectDir;
if (str_starts_with($filePath, $projectDir.'/') || str_starts_with($filePath, $projectDir.\DIRECTORY_SEPARATOR)) {
return substr($filePath, \strlen($projectDir) + 1);
}
return basename($filePath);

View File

@@ -61,14 +61,8 @@ final class DotenvDumpCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): int
{
$config = [];
if (is_file($projectDir = $this->projectDir)) {
$config = ['dotenv_path' => basename($projectDir)];
$projectDir = \dirname($projectDir);
}
$dotenvPath = $this->getDotenvPath($config);
$composerFile = $projectDir.'/composer.json';
$config += (is_file($composerFile) ? json_decode(file_get_contents($composerFile), true) : [])['extra']['runtime'] ?? [];
$dotenvPath = $projectDir.'/'.($config['dotenv_path'] ?? '.env');
$env = $input->getArgument('env') ?? $this->defaultEnv;
$envKey = $config['env_var_name'] ?? 'APP_ENV';
@@ -90,11 +84,27 @@ final class DotenvDumpCommand extends Command
EOF;
file_put_contents($dotenvPath.'.local.php', $vars, \LOCK_EX);
$output->writeln(\sprintf('Successfully dumped .env files in <info>.env.local.php</> for the <info>%s</> environment.', $env));
$output->writeln(\sprintf('Successfully dumped %s files in <info>%1$s.local.php</> for the <info>%s</> environment.', basename($dotenvPath), $env));
return 0;
}
private function getDotenvPath(array &$config): string
{
$config = [];
$projectDir = $this->projectDir;
if (is_file($projectDir)) {
$config = ['dotenv_path' => basename($projectDir)];
$projectDir = \dirname($projectDir);
}
$composerFile = $projectDir.'/composer.json';
$config += $_SERVER['APP_RUNTIME_OPTIONS'] ?? (is_file($composerFile) ? json_decode(file_get_contents($composerFile), true) : [])['extra']['runtime'] ?? [];
return $projectDir.'/'.($config['dotenv_path'] ?? '.env');
}
private function loadEnv(string $dotenvPath, string $env, array $config): array
{
$envKey = $config['env_var_name'] ?? 'APP_ENV';

View File

@@ -290,6 +290,14 @@ class DebugCommandTest extends TestCase
private function executeCommand(string $projectDirectory, string $env, array $input = [], ?string $dotenvPath = null): string
{
if (null === $dotenvPath) {
unset($_SERVER['APP_RUNTIME_OPTIONS']);
} elseif (str_starts_with($dotenvPath, $projectDirectory.'/')) {
$_SERVER['APP_RUNTIME_OPTIONS'] = ['dotenv_path' => substr($dotenvPath, \strlen($projectDirectory) + 1)];
} else {
$_SERVER['APP_RUNTIME_OPTIONS'] = ['dotenv_path' => $dotenvPath];
}
$_SERVER['TEST_ENV_KEY'] = $env;
(new Dotenv('TEST_ENV_KEY'))->bootEnv($dotenvPath ?? $projectDirectory.'/.env');

View File

@@ -20,6 +20,9 @@ class DotenvDumpCommandTest extends TestCase
{
protected function setUp(): void
{
unset($_SERVER['SYMFONY_DOTENV_PATH']);
unset($_SERVER['APP_RUNTIME_OPTIONS']);
file_put_contents(__DIR__.'/.env', <<<EOF
APP_ENV=dev
APP_SECRET=abc123
@@ -36,8 +39,14 @@ class DotenvDumpCommandTest extends TestCase
{
@unlink(__DIR__.'/.env');
@unlink(__DIR__.'/.env.local');
@unlink(__DIR__.'/.env.path');
@unlink(__DIR__.'/.env.path.local');
@unlink(__DIR__.'/.env.local.php');
@unlink(__DIR__.'/.env.path.local.php');
@unlink(__DIR__.'/composer.json');
unset($_SERVER['SYMFONY_DOTENV_PATH']);
unset($_SERVER['APP_RUNTIME_OPTIONS']);
}
public function testExecute()
@@ -92,6 +101,35 @@ class DotenvDumpCommandTest extends TestCase
], $vars);
}
public function testExecuteWithRuntimeOptionsDotenvPath()
{
file_put_contents(__DIR__.'/.env.path', <<<EOF
APP_ENV=test
APP_SECRET=newpath123
EOF
);
file_put_contents(__DIR__.'/.env.path.local', <<<EOF
LOCAL_PATH=yes
EOF
);
$_SERVER['APP_RUNTIME_OPTIONS'] = ['dotenv_path' => '.env.path'];
$command = $this->createCommand();
$command->execute([
'env' => 'dev',
]);
$this->assertFileExists(__DIR__.'/.env.path.local.php');
$vars = require __DIR__.'/.env.path.local.php';
$this->assertSame([
'APP_ENV' => 'dev',
'APP_SECRET' => 'newpath123',
'LOCAL_PATH' => 'yes',
], $vars);
}
private function createCommand(): CommandTester
{
$application = new Application();