Refactor Codeception configuration to support PHP format · Codeception/Codeception@8ddfeaf · GitHub
Skip to content

Commit 8ddfeaf

Browse files
committed
Refactor Codeception configuration to support PHP format
- Introduced support for PHP configuration files alongside YAML. - Updated Api, Unit, and Bootstrap templates to generate PHP config files. - Added methods to handle PHP config generation and validation. - Enhanced test cases to validate the new PHP configuration format. - Migrated existing YAML configurations to PHP format in tests. - Improved error handling and output for configuration validation. - Added new test cases to ensure compatibility and correctness of the new configuration system.
1 parent eb7a6a2 commit 8ddfeaf

48 files changed

Lines changed: 2399 additions & 113 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/build.yml‎

Lines changed: 6 additions & 7 deletions

‎app.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
new Codeception\Command\GherkinSteps('gherkin:steps'),
3030
new Codeception\Command\DryRun('dry-run'),
3131
new Codeception\Command\ConfigValidate('config:validate'),
32+
new Codeception\Command\ConfigToPhp('config:to-php'),
3233
];
3334

3435
// Suggests package

‎src/Codeception/Command/Bootstrap.php‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* By default, it will create 3 suites **Acceptance**, **Functional**, and **Unit**.
2020
*
2121
* * `codecept bootstrap` - creates `tests` dir and `codeception.yml` in current dir.
22+
* * `codecept bootstrap --php` - generates PHP config files (`codeception.php`) instead of YAML
2223
* * `codecept bootstrap --empty` - creates `tests` dir without suites
2324
* * `codecept bootstrap --namespace Frontend` - creates tests, and use `Frontend` namespace for actor classes and helpers.
2425
* * `codecept bootstrap --actor Wizard` - sets actor as Wizard, to have `TestWizard` actor in tests.
@@ -37,7 +38,8 @@ protected function configure(): void
3738
->addArgument('path', InputArgument::OPTIONAL, 'custom installation dir')
3839
->addOption('namespace', 's', InputOption::VALUE_OPTIONAL, 'Namespace to add for actor classes and helpers')
3940
->addOption('actor', 'a', InputOption::VALUE_OPTIONAL, 'Custom actor instead of Tester')
40-
->addOption('empty', 'e', InputOption::VALUE_NONE, "Don't create standard suites");
41+
->addOption('empty', 'e', InputOption::VALUE_NONE, "Don't create standard suites")
42+
->addOption('php', null, InputOption::VALUE_NONE, 'Generate PHP config files instead of YAML');
4143
}
4244

4345
protected function execute(InputInterface $input, OutputInterface $output): int
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Codeception\Command;
6+
7+
use Codeception\Lib\Generator\PhpConfigFile;
8+
use Symfony\Component\Console\Attribute\AsCommand;
9+
use Symfony\Component\Console\Command\Command;
10+
use Symfony\Component\Console\Input\InputArgument;
11+
use Symfony\Component\Console\Input\InputInterface;
12+
use Symfony\Component\Console\Input\InputOption;
13+
use Symfony\Component\Console\Output\OutputInterface;
14+
use Symfony\Component\Yaml\Yaml;
15+
16+
use function array_merge;
17+
use function basename;
18+
use function dirname;
19+
use function file_exists;
20+
use function file_get_contents;
21+
use function is_array;
22+
use function glob;
23+
use function is_dir;
24+
use function preg_replace;
25+
use function rtrim;
26+
27+
/**
28+
* Converts an existing YAML setup to the PHP config format (builder style).
29+
*
30+
* * `codecept config:to-php` - converts codeception.yml and each *.suite.yml in the current project
31+
* * `codecept config:to-php path/to/project` - converts a project in another directory
32+
* * `codecept config:to-php --dry-run` - prints the generated PHP without writing files
33+
*
34+
* Existing `.php` files are never overwritten. Whole-value `%param%` placeholders become getenv()
35+
* (global) or Params::get() (suites); after converting, delete the old `.yml` files.
36+
*/
37+
#[AsCommand(
38+
name: 'config:to-php',
39+
description: 'Converts YAML configuration to the PHP format'
40+
)]
41+
class ConfigToPhp extends Command
42+
{
43+
use Shared\FileSystemTrait;
44+
45+
protected function configure(): void
46+
{
47+
$this
48+
->addArgument('path', InputArgument::OPTIONAL, 'Project directory or codeception.yml to convert', '.')
49+
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Print generated PHP without writing files');
50+
}
51+
52+
protected function execute(InputInterface $input, OutputInterface $output): int
53+
{
54+
$path = (string) $input->getArgument('path');
55+
$dir = is_dir($path) ? rtrim($path, '/\\') : dirname($path);
56+
57+
if (is_dir($path)) {
58+
$globalYml = $dir . DIRECTORY_SEPARATOR . 'codeception.yml';
59+
if (!file_exists($globalYml) && file_exists($dir . DIRECTORY_SEPARATOR . 'codeception.dist.yml')) {
60+
$globalYml = $dir . DIRECTORY_SEPARATOR . 'codeception.dist.yml';
61+
}
62+
} else {
63+
$globalYml = $path;
64+
}
65+
66+
if (!file_exists($globalYml)) {
67+
$output->writeln("<error>Global config not found: {$globalYml}</error>");
68+
return Command::FAILURE;
69+
}
70+
71+
$dryRun = (bool) $input->getOption('dry-run');
72+
$renderer = new PhpConfigFile();
73+
74+
$globalContents = file_get_contents($globalYml);
75+
if ($globalContents === false) {
76+
$output->writeln("<error>Failed to read {$globalYml}</error>");
77+
return Command::FAILURE;
78+
}
79+
$globalArr = Yaml::parse($globalContents) ?? [];
80+
$this->emit($output, $globalYml, $renderer->renderGlobal($globalArr), $dryRun);
81+
82+
$paths = [];
83+
if (is_dir($path)) {
84+
foreach (['codeception.dist.yml', 'codeception.yml'] as $candidate) {
85+
$candidatePath = $dir . DIRECTORY_SEPARATOR . $candidate;
86+
if (!file_exists($candidatePath)) {
87+
continue;
88+
}
89+
$candidateArr = Yaml::parse((string) file_get_contents($candidatePath));
90+
if (is_array($candidateArr) && is_array($candidateArr['paths'] ?? null)) {
91+
$paths = array_merge($paths, $candidateArr['paths']);
92+
}
93+
}
94+
} elseif (is_array($globalArr) && is_array($globalArr['paths'] ?? null)) {
95+
$paths = $globalArr['paths'];
96+
}
97+
98+
$testsDir = $dir . DIRECTORY_SEPARATOR . ($paths['tests'] ?? 'tests');
99+
if (is_dir($testsDir)) {
100+
$suiteFiles = array_merge(
101+
glob($testsDir . DIRECTORY_SEPARATOR . '*.suite.yml') ?: [],
102+
glob($testsDir . DIRECTORY_SEPARATOR . '*.suite.dist.yml') ?: []
103+
);
104+
foreach ($suiteFiles as $suiteYml) {
105+
$suiteContents = file_get_contents($suiteYml);
106+
if ($suiteContents === false) {
107+
$output->writeln("<comment>Skipped {$suiteYml} (could not be read)</comment>");
108+
continue;
109+
}
110+
$suiteArr = Yaml::parse($suiteContents) ?? [];
111+
$this->emit($output, $suiteYml, $renderer->renderSuite($suiteArr), $dryRun);
112+
}
113+
}
114+
115+
foreach ($renderer->warnings() as $warning) {
116+
$output->writeln("<comment>! {$warning}</comment>");
117+
}
118+
if (!$dryRun) {
119+
$output->writeln("\n<info>Done.</info> Review the generated PHP, then remove the old .yml files.");
120+
}
121+
return Command::SUCCESS;
122+
}
123+
124+
private function emit(OutputInterface $output, string $from, string $contents, bool $dryRun): void
125+
{
126+
$to = (string) preg_replace('/\.yml$/', '.php', $from);
127+
if ($dryRun) {
128+
$output->writeln("<info># {$to}</info>");
129+
$output->writeln($contents);
130+
return;
131+
}
132+
if (file_exists($to)) {
133+
$output->writeln("<comment>Skipped {$to} (already exists)</comment>");
134+
return;
135+
}
136+
$this->createFile($to, $contents);
137+
$output->writeln("<info>Created {$to}</info> from " . basename($from));
138+
}
139+
}

‎src/Codeception/Command/ConfigValidate.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7979
$suites = Configuration::suites();
8080

8181
$output->writeln("------------------------------\n");
82+
$output->writeln('<info>Loaded config file</info>: ' . (Configuration::loadedConfigFile() ?? '(none)'));
8283
$output->writeln("<info>Codeception Config</info>:\n");
8384
$output->writeln($this->formatOutput($config));
8485

‎src/Codeception/Command/GenerateEnvironment.php‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use Symfony\Component\Console\Input\InputInterface;
1313
use Symfony\Component\Console\Output\OutputInterface;
1414

15+
use function file_exists;
16+
1517
/**
1618
* Generates empty environment configuration file into envs dir:
1719
*
@@ -39,17 +41,48 @@ protected function execute(InputInterface $input, OutputInterface $output): int
3941
if (Configuration::envsDir() === '') {
4042
throw new ConfigurationException(
4143
"Path for environments configuration is not set.\n"
42-
. "Please specify envs path in your `codeception.yml`\n \n"
44+
. "Please specify envs path in your configuration file (`codeception.yml` or `codeception.php`)\n \n"
4345
. "envs: tests/_envs"
4446
);
4547
}
4648

4749
$relativePath = $config['paths']['envs'];
4850
$env = $input->getArgument('env');
49-
$file = $env . '.yml';
51+
52+
if (Configuration::isPhpFormat()) {
53+
$file = $env . '.php';
54+
$contents = <<<PHP
55+
<?php
56+
57+
declare(strict_types=1);
58+
59+
use Codeception\\Config\\SuiteConfig;
60+
61+
return SuiteConfig::create();
62+
63+
PHP;
64+
} else {
65+
$file = $env . '.yml';
66+
$contents = sprintf('# `%s` environment config goes here', $env);
67+
}
5068

5169
$path = $this->createDirectoryFor($relativePath, $file);
52-
$saved = $this->createFile($path . $file, sprintf('# `%s` environment config goes here', $env));
70+
71+
$otherFile = $env . (Configuration::isPhpFormat() ? '.yml' : '.php');
72+
if (file_exists($path . $otherFile)) {
73+
$output->writeln(sprintf(
74+
'<error>Environment "%s" already has %s/%s; a %s would silently shadow one of them '
75+
. '(PHP config wins over YAML). Remove %s or edit it directly.</error>',
76+
$env,
77+
$relativePath,
78+
$otherFile,
79+
$file,
80+
$otherFile
81+
));
82+
return Command::FAILURE;
83+
}
84+
85+
$saved = $this->createFile($path . $file, $contents);
5386

5487
if ($saved) {
5588
$output->writeln(sprintf('<info>%s config was created in %s/%s</info>', $env, $relativePath, $file));

‎src/Codeception/Command/GenerateSuite.php‎

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
use Symfony\Component\Console\Input\InputArgument;
1414
use Symfony\Component\Console\Input\InputInterface;
1515
use Symfony\Component\Console\Output\OutputInterface;
16-
use Symfony\Component\Yaml\Yaml;
1716

17+
use function addcslashes;
1818
use function file_exists;
1919
use function preg_match;
2020
use function ucfirst;
@@ -57,9 +57,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int
5757
return Command::FAILURE;
5858
}
5959

60+
$isPhp = Configuration::isPhpFormat();
61+
$ext = $isPhp ? 'php' : 'yml';
6062
$dir = Configuration::testsDir();
61-
if (file_exists($dir . $suite . '.suite.yml')) {
62-
throw new Exception("Suite configuration file '{$suite}.suite.yml' already exists.");
63+
$existing = match (true) {
64+
file_exists($dir . $suite . '.suite.yml') => "{$suite}.suite.yml",
65+
file_exists($dir . $suite . '.suite.php') => "{$suite}.suite.php",
66+
default => null,
67+
};
68+
if ($existing !== null) {
69+
throw new Exception("Suite configuration file '{$existing}' already exists.");
6370
}
6471

6572
$this->createDirectoryFor($dir . $suite);
@@ -72,30 +79,45 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7279
);
7380
}
7481

75-
$yamlSuiteConfigTemplate = <<<EOF
82+
$suiteNamespace = $config['namespace'] . '\\' . $suite;
83+
if ($isPhp) {
84+
$actorLiteral = addcslashes($actor, "\\'");
85+
$suiteConfig = <<<PHP
86+
<?php
87+
88+
declare(strict_types=1);
89+
90+
use Codeception\\Config\\SuiteConfig;
91+
92+
return SuiteConfig::create()
93+
->actor('{$actorLiteral}');
94+
95+
PHP;
96+
} else {
97+
$suiteConfig = (new Template(<<<EOF
7698
actor: {{actor}}
7799
suite_namespace: {{suite_namespace}}
78100
modules:
79101
# enable helpers as array
80102
enabled: []
81-
EOF;
82-
$yamlSuiteConfig = (new Template($yamlSuiteConfigTemplate))
83-
->place('actor', $actor)
84-
->place('suite_namespace', $config['namespace'] . '\\' . $suite)
85-
->produce();
86-
$this->createFile($dir . $suite . '.suite.yml', $yamlSuiteConfig);
87-
Configuration::append(Yaml::parse($yamlSuiteConfig));
103+
EOF))
104+
->place('actor', $actor)
105+
->place('suite_namespace', $suiteNamespace)
106+
->produce();
107+
}
108+
$this->createFile($dir . $suite . '.suite.' . $ext, $suiteConfig);
109+
Configuration::append(['actor' => $actor, 'suite_namespace' => $suiteNamespace, 'modules' => ['enabled' => []]]);
88110
$actorGenerator = new ActorGenerator(Configuration::config());
89111

90112
$content = $actorGenerator->produce();
91113
$file = $this->createDirectoryFor(Configuration::supportDir(), $actor) . $this->getShortClassName($actor) . '.php';
92114
$this->createFile($file, $content);
93115

94116
$output->writeln("Actor <info>{$actor}</info> was created in {$file}");
95-
$output->writeln("Suite config <info>{$suite}.suite.yml</info> was created.");
117+
$output->writeln("Suite config <info>{$suite}.suite.{$ext}</info> was created.");
96118
$output->writeln(' ');
97119
$output->writeln("Next steps:");
98-
$output->writeln("1. Edit <bold>{$suite}.suite.yml</bold> to enable modules for this suite");
120+
$output->writeln("1. Edit <bold>{$suite}.suite.{$ext}</bold> to enable modules for this suite");
99121
$output->writeln("2. Create first test with <bold>generate:cest testName</bold> ( or test|cept) command");
100122
$output->writeln("3. Run tests of this suite with <bold>codecept run {$suite}</bold> command");
101123

‎src/Codeception/Command/Init.php‎

Lines changed: 2 additions & 1 deletion

0 commit comments

Comments
 (0)