Skip to content

Commit cd940a2

Browse files
committed
Add usability improvements
- Add --force option to force regeneration - Add error handling when config file not found - Add dto:init command to create starter configuration - Improve dry-run output message - Use PHP as default engine (consistent with Laravel)
1 parent 446fa2b commit cd940a2

File tree

5 files changed

+398
-12
lines changed

5 files changed

+398
-12
lines changed

src/Command/GenerateDtoCommand.php

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ protected function configure(): void
3838
{
3939
$this
4040
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Preview changes without writing files')
41+
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force regeneration of all DTOs')
4142
->addOption('config-path', null, InputOption::VALUE_REQUIRED, 'Path to DTO config files')
4243
->addOption('output-path', null, InputOption::VALUE_REQUIRED, 'Path for generated DTOs')
4344
->addOption('namespace', null, InputOption::VALUE_REQUIRED, 'Namespace for generated DTOs');
@@ -59,13 +60,34 @@ protected function execute(InputInterface $input, OutputInterface $output): int
5960
$outputPath .= '/';
6061
}
6162

63+
// Validate config path exists
64+
if (!is_dir($configPath)) {
65+
$io->error("Config path does not exist: {$configPath}");
66+
67+
return Command::FAILURE;
68+
}
69+
70+
// Check if any config files exist
71+
$engine = $this->detectEngine($configPath);
72+
if ($engine === null) {
73+
$io->error("No DTO configuration files found in: {$configPath}");
74+
$io->writeln('');
75+
$io->writeln('Expected one of:');
76+
$io->writeln(' - dtos.php, dtos.xml, dtos.yml, dtos.yaml');
77+
$io->writeln(' - dto.php, dto.xml, dto.yml, dto.yaml');
78+
$io->writeln(' - dto/ subdirectory with config files');
79+
$io->writeln('');
80+
$io->writeln('Run "bin/console dto:init" to create a starter configuration.');
81+
82+
return Command::FAILURE;
83+
}
84+
6285
$config = new ArrayConfig([
6386
'namespace' => $namespace,
6487
'dryRun' => $input->getOption('dry-run'),
6588
'verbose' => $output->isVerbose(),
6689
]);
6790

68-
$engine = $this->detectEngine($configPath);
6991
$builder = new Builder($engine, $config);
7092
$renderer = new TwigRenderer(null, $config);
7193
$consoleIo = new SymfonyConsoleIo($io);
@@ -74,14 +96,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7496
$generator->generate($configPath, $outputPath, [
7597
'dryRun' => $input->getOption('dry-run'),
7698
'verbose' => $output->isVerbose(),
99+
'force' => $input->getOption('force'),
77100
]);
78101

79-
$io->success('DTOs generated successfully.');
102+
if ($input->getOption('dry-run')) {
103+
$io->success('Dry run complete. No files were written.');
104+
} else {
105+
$io->success('DTOs generated successfully.');
106+
}
80107

81108
return Command::SUCCESS;
82109
}
83110

84-
private function detectEngine(string $configPath): PhpEngine|XmlEngine|YamlEngine
111+
private function detectEngine(string $configPath): PhpEngine|XmlEngine|YamlEngine|null
85112
{
86113
$sep = str_ends_with($configPath, '/') ? '' : '/';
87114

@@ -121,6 +148,6 @@ private function detectEngine(string $configPath): PhpEngine|XmlEngine|YamlEngin
121148
}
122149
}
123150

124-
return new XmlEngine();
151+
return null;
125152
}
126153
}

src/Command/InitDtoCommand.php

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpCollective\SymfonyDto\Command;
6+
7+
use Symfony\Component\Console\Attribute\AsCommand;
8+
use Symfony\Component\Console\Command\Command;
9+
use Symfony\Component\Console\Input\InputInterface;
10+
use Symfony\Component\Console\Input\InputOption;
11+
use Symfony\Component\Console\Output\OutputInterface;
12+
use Symfony\Component\Console\Style\SymfonyStyle;
13+
14+
#[AsCommand(
15+
name: 'dto:init',
16+
description: 'Create a starter DTO configuration file',
17+
)]
18+
class InitDtoCommand extends Command
19+
{
20+
public function __construct(
21+
private string $projectDir,
22+
private string $configPath = 'config',
23+
) {
24+
parent::__construct();
25+
}
26+
27+
protected function configure(): void
28+
{
29+
$this
30+
->addOption('format', null, InputOption::VALUE_REQUIRED, 'Config format (php, xml, yaml)', 'php')
31+
->addOption('force', 'f', InputOption::VALUE_NONE, 'Overwrite existing config file');
32+
}
33+
34+
protected function execute(InputInterface $input, OutputInterface $output): int
35+
{
36+
$io = new SymfonyStyle($input, $output);
37+
38+
$format = strtolower((string)$input->getOption('format'));
39+
$configPath = $this->projectDir . '/' . $this->configPath;
40+
41+
if (!in_array($format, ['php', 'xml', 'yaml', 'yml'], true)) {
42+
$io->error("Invalid format: {$format}. Use php, xml, or yaml.");
43+
44+
return Command::FAILURE;
45+
}
46+
47+
if ($format === 'yml') {
48+
$format = 'yaml';
49+
}
50+
51+
$filename = $format === 'php' ? 'dtos.php' : 'dto.' . $format;
52+
$filePath = rtrim($configPath, '/') . '/' . $filename;
53+
54+
if (file_exists($filePath) && !$input->getOption('force')) {
55+
$io->error("Config file already exists: {$filePath}");
56+
$io->writeln('Use --force to overwrite.');
57+
58+
return Command::FAILURE;
59+
}
60+
61+
$content = $this->getConfigContent($format);
62+
63+
if (!is_dir(dirname($filePath))) {
64+
mkdir(dirname($filePath), 0755, true);
65+
}
66+
67+
file_put_contents($filePath, $content);
68+
69+
$io->success("Created DTO configuration: {$filePath}");
70+
$io->writeln('');
71+
$io->writeln('Next steps:');
72+
$io->writeln(' 1. Edit the configuration to define your DTOs');
73+
$io->writeln(' 2. Run "bin/console dto:generate" to generate the DTO classes');
74+
75+
return Command::SUCCESS;
76+
}
77+
78+
private function getConfigContent(string $format): string
79+
{
80+
return match ($format) {
81+
'php' => $this->getPhpConfig(),
82+
'xml' => $this->getXmlConfig(),
83+
'yaml' => $this->getYamlConfig(),
84+
};
85+
}
86+
87+
private function getPhpConfig(): string
88+
{
89+
return <<<'PHP'
90+
<?php
91+
92+
/**
93+
* DTO Configuration
94+
*
95+
* Define your Data Transfer Objects here.
96+
*
97+
* @see https://github.com/php-collective/dto
98+
*/
99+
return [
100+
// Example DTO definition
101+
[
102+
'name' => 'User',
103+
'fields' => [
104+
[
105+
'name' => 'id',
106+
'type' => 'int',
107+
],
108+
[
109+
'name' => 'email',
110+
'type' => 'string',
111+
],
112+
[
113+
'name' => 'name',
114+
'type' => 'string',
115+
'nullable' => true,
116+
],
117+
[
118+
'name' => 'createdAt',
119+
'type' => '\DateTimeInterface',
120+
'nullable' => true,
121+
],
122+
],
123+
],
124+
125+
// Add more DTOs here...
126+
];
127+
PHP;
128+
}
129+
130+
private function getXmlConfig(): string
131+
{
132+
return <<<'XML'
133+
<?xml version="1.0" encoding="UTF-8"?>
134+
<!--
135+
DTO Configuration
136+
137+
Define your Data Transfer Objects here.
138+
139+
@see https://github.com/php-collective/dto
140+
-->
141+
<dtos xmlns="php-collective-dto">
142+
<!-- Example DTO definition -->
143+
<dto name="User">
144+
<field name="id" type="int"/>
145+
<field name="email" type="string"/>
146+
<field name="name" type="string" nullable="true"/>
147+
<field name="createdAt" type="\DateTimeInterface" nullable="true"/>
148+
</dto>
149+
150+
<!-- Add more DTOs here... -->
151+
</dtos>
152+
XML;
153+
}
154+
155+
private function getYamlConfig(): string
156+
{
157+
return <<<'YAML'
158+
# DTO Configuration
159+
#
160+
# Define your Data Transfer Objects here.
161+
#
162+
# @see https://github.com/php-collective/dto
163+
164+
# Example DTO definition
165+
User:
166+
fields:
167+
id:
168+
type: int
169+
email:
170+
type: string
171+
name:
172+
type: string
173+
nullable: true
174+
createdAt:
175+
type: \DateTimeInterface
176+
nullable: true
177+
178+
# Add more DTOs here...
179+
YAML;
180+
}
181+
}

src/DependencyInjection/PhpCollectiveDtoExtension.php

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace PhpCollective\SymfonyDto\DependencyInjection;
66

77
use PhpCollective\SymfonyDto\Command\GenerateDtoCommand;
8+
use PhpCollective\SymfonyDto\Command\InitDtoCommand;
89
use Symfony\Component\DependencyInjection\ContainerBuilder;
910
use Symfony\Component\DependencyInjection\Definition;
1011
use Symfony\Component\DependencyInjection\Extension\Extension;
@@ -16,13 +17,20 @@ public function load(array $configs, ContainerBuilder $container): void
1617
$configuration = new Configuration();
1718
$config = $this->processConfiguration($configuration, $configs);
1819

19-
$definition = new Definition(GenerateDtoCommand::class);
20-
$definition->setArgument('$projectDir', '%kernel.project_dir%');
21-
$definition->setArgument('$configPath', $config['config_path']);
22-
$definition->setArgument('$outputPath', $config['output_path']);
23-
$definition->setArgument('$namespace', $config['namespace']);
24-
$definition->addTag('console.command');
20+
// Register dto:generate command
21+
$generateDefinition = new Definition(GenerateDtoCommand::class);
22+
$generateDefinition->setArgument('$projectDir', '%kernel.project_dir%');
23+
$generateDefinition->setArgument('$configPath', $config['config_path']);
24+
$generateDefinition->setArgument('$outputPath', $config['output_path']);
25+
$generateDefinition->setArgument('$namespace', $config['namespace']);
26+
$generateDefinition->addTag('console.command');
27+
$container->setDefinition(GenerateDtoCommand::class, $generateDefinition);
2528

26-
$container->setDefinition(GenerateDtoCommand::class, $definition);
29+
// Register dto:init command
30+
$initDefinition = new Definition(InitDtoCommand::class);
31+
$initDefinition->setArgument('$projectDir', '%kernel.project_dir%');
32+
$initDefinition->setArgument('$configPath', $config['config_path']);
33+
$initDefinition->addTag('console.command');
34+
$container->setDefinition(InitDtoCommand::class, $initDefinition);
2735
}
2836
}

tests/GenerateDtoCommandTest.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PhpCollective\SymfonyDto\Command\GenerateDtoCommand;
88
use PHPUnit\Framework\TestCase;
99
use Symfony\Component\Console\Application;
10+
use Symfony\Component\Console\Command\Command;
1011
use Symfony\Component\Console\Tester\CommandTester;
1112

1213
class GenerateDtoCommandTest extends TestCase
@@ -31,7 +32,7 @@ protected function removeDirectory(string $dir): void
3132
if (!is_dir($dir)) {
3233
return;
3334
}
34-
$files = array_diff(scandir($dir), ['.', '..']);
35+
$files = array_diff((array)scandir($dir), ['.', '..']);
3536
foreach ($files as $file) {
3637
$path = $dir . '/' . $file;
3738
is_dir($path) ? $this->removeDirectory($path) : unlink($path);
@@ -51,6 +52,25 @@ public function testCommandName(): void
5152
$this->assertSame('dto:generate', $command->getName());
5253
}
5354

55+
public function testCommandFailsWhenNoConfigFound(): void
56+
{
57+
$command = new GenerateDtoCommand(
58+
$this->tempDir,
59+
'config',
60+
'output',
61+
'Test\\Dto',
62+
);
63+
64+
$application = new Application();
65+
$application->add($command);
66+
67+
$commandTester = new CommandTester($command);
68+
$commandTester->execute([]);
69+
70+
$this->assertSame(Command::FAILURE, $commandTester->getStatusCode());
71+
$this->assertStringContainsString('No DTO configuration files found', $commandTester->getDisplay());
72+
}
73+
5474
public function testCommandWithDryRun(): void
5575
{
5676
$configContent = <<<'XML'

0 commit comments

Comments
 (0)