ValidateConfigCommand::configure()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 10
c 1
b 0
f 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Gacela\Console\Infrastructure\Command;
6
7
use Gacela\Framework\Container\Container;
8
use Gacela\Framework\Gacela;
9
use Symfony\Component\Console\Command\Command;
10
use Symfony\Component\Console\Input\InputInterface;
11
use Symfony\Component\Console\Output\OutputInterface;
12
use Throwable;
13
14
use function class_exists;
15
use function count;
16
use function file_exists;
17
use function interface_exists;
18
use function is_callable;
19
use function is_object;
20
use function is_string;
21
use function sprintf;
22
23
final class ValidateConfigCommand extends Command
24
{
25
    protected function configure(): void
26
    {
27
        $this->setName('validate:config')
28
            ->setDescription('Validate Gacela configuration for errors and best practices')
29
            ->setHelp($this->getHelpText());
30
    }
31
32
    protected function execute(InputInterface $input, OutputInterface $output): int
33
    {
34
        $output->writeln('');
35
        $output->writeln('<info>Validating Gacela Configuration</info>');
36
        $output->writeln(sprintf('<info>%s</info>', str_repeat('=', 60)));
37
        $output->writeln('');
38
39
        $hasErrors = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $hasErrors is dead and can be removed.
Loading history...
40
        $hasWarnings = false;
41
42
        // Check if gacela.php exists
43
        $gacelaConfigPath = Gacela::rootDir() . '/gacela.php';
44
        if (!file_exists($gacelaConfigPath)) {
45
            $output->writeln('<fg=yellow>⚠ Warning:</> No gacela.php configuration file found');
46
            $output->writeln(sprintf('  Expected at: %s', $gacelaConfigPath));
47
            $output->writeln('');
48
            $hasWarnings = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $hasWarnings is dead and can be removed.
Loading history...
49
        } else {
50
            $output->writeln(sprintf('<fg=green>✓</> Configuration file found: %s', $gacelaConfigPath));
51
            $output->writeln('');
52
        }
53
54
        // Validate bindings
55
        $container = Gacela::container();
56
        $bindingsValidation = $this->validateBindings($container, $output);
57
        $hasErrors = $bindingsValidation['errors'];
58
        $hasWarnings = $bindingsValidation['warnings'];
59
60
        // Validate config paths
61
        $configValidation = $this->validateConfigPaths($output);
62
        $hasErrors = $hasErrors || $configValidation['errors'];
63
        $hasWarnings = $hasWarnings || $configValidation['warnings'];
64
65
        // Check for circular dependencies (basic check)
66
        $circularDepsValidation = $this->checkCircularDependencies($container, $output);
67
        $hasErrors = $hasErrors || $circularDepsValidation['errors'];
68
        $hasWarnings = $hasWarnings || $circularDepsValidation['warnings'];
69
70
        // Summary
71
        $output->writeln('');
72
        $output->writeln(sprintf('<info>%s</info>', str_repeat('=', 60)));
73
74
        if ($hasErrors) {
75
            $output->writeln('<error>✗ Validation failed with errors</error>');
76
            $output->writeln('');
77
            return Command::FAILURE;
78
        }
79
80
        if ($hasWarnings) {
81
            $output->writeln('<fg=yellow>⚠ Validation completed with warnings</fg=yellow>');
82
            $output->writeln('');
83
            return Command::SUCCESS;
84
        }
85
86
        $output->writeln('<fg=green>✓ Configuration is valid!</fg=green>');
87
        $output->writeln('');
88
89
        return Command::SUCCESS;
90
    }
91
92
    /**
93
     * @return array{errors: bool, warnings: bool}
94
     */
95
    private function validateBindings(Container $container, OutputInterface $output): array
96
    {
97
        $output->writeln('<comment>Checking bindings...</comment>');
98
99
        $hasErrors = false;
100
        $hasWarnings = false;
101
102
        try {
103
            $bindings = $container->getBindings();
104
105
            if ($bindings === []) {
106
                $output->writeln('  <fg=cyan>No bindings configured</fg=cyan>');
107
                $output->writeln('');
108
                return ['errors' => false, 'warnings' => false];
109
            }
110
111
            $output->writeln(sprintf('  Found %d binding%s', count($bindings), count($bindings) === 1 ? '' : 's'));
112
            $output->writeln('');
113
114
            foreach ($bindings as $key => $value) {
115
                // Validate key exists as class or interface
116
                if (!class_exists($key) && !interface_exists($key)) {
117
                    $output->writeln(sprintf('  <error>✗ Binding key does not exist:</> %s', $key));
118
                    $hasErrors = true;
119
                    continue;
120
                }
121
122
                // Validate value
123
                if (is_string($value)) {
124
                    if (!class_exists($value)) {
125
                        $output->writeln(sprintf('  <error>✗ Binding value class does not exist:</> %s -> %s', $key, $value));
126
                        $hasErrors = true;
127
                        continue;
128
                    }
129
130
                    // Check if value is assignable to key (basic check)
131
                    if (class_exists($key) && !is_subclass_of($value, $key) && $value !== $key) {
132
                        $output->writeln(sprintf('  <fg=yellow>⚠ Warning: Binding value may not be compatible with key:</> %s -> %s', $key, $value));
133
                        $hasWarnings = true;
134
                    }
135
                } elseif (is_object($value)) {
136
                    // Object binding (including callables) - check if it's assignable to key
137
                    if (!is_callable($value) && class_exists($key) && !($value instanceof $key)) {
138
                        $output->writeln(sprintf('  <error>✗ Binding object is not instance of key:</> %s', $key));
139
                        $hasErrors = true;
140
                        continue;
141
                    }
142
143
                    // Callable objects are valid - no further validation needed
144
                }
145
146
                $output->writeln(sprintf('  <fg=green>✓</> %s', $key));
147
            }
148
        } catch (Throwable $throwable) {
149
            $output->writeln(sprintf('  <error>Error validating bindings: %s</error>', $throwable->getMessage()));
150
            $hasErrors = true;
151
        }
152
153
        $output->writeln('');
154
155
        return ['errors' => $hasErrors, 'warnings' => $hasWarnings];
156
    }
157
158
    /**
159
     * @return array{errors: bool, warnings: bool}
160
     */
161
    private function validateConfigPaths(OutputInterface $output): array
162
    {
163
        $output->writeln('<comment>Checking configuration paths...</comment>');
164
165
        // Note: We can't easily access the config paths from the current API
166
        // This is a placeholder for future enhancement
167
        $output->writeln('  <fg=cyan>Config path validation requires runtime configuration access</fg=cyan>');
168
        $output->writeln('');
169
170
        return ['errors' => false, 'warnings' => false];
171
    }
172
173
    /**
174
     * @return array{errors: bool, warnings: bool}
175
     */
176
    private function checkCircularDependencies(Container $container, OutputInterface $output): array
177
    {
178
        $output->writeln('<comment>Checking for circular dependencies...</comment>');
179
180
        $hasErrors = false;
181
        $hasWarnings = false;
182
183
        try {
184
            $bindings = $container->getBindings();
185
186
            foreach ($bindings as $key => $value) {
187
                if (!is_string($value)) {
188
                    continue;
189
                }
190
191
                // Check if value class depends back on key (simple check)
192
                if (class_exists($value)) {
193
                    try {
194
                        /**
195
                         * @psalm-suppress UnusedVariable
196
                         * @psalm-suppress MixedAssignment
197
                         */
198
                        $resolved = $container->get($key);
0 ignored issues
show
Unused Code introduced by
The assignment to $resolved is dead and can be removed.
Loading history...
199
                        // If we can resolve it without errors, it's likely fine
200
                    } catch (Throwable $throwable) {
201
                        if (str_contains($throwable->getMessage(), 'circular') || str_contains($throwable->getMessage(), 'recursive')) {
202
                            $output->writeln(sprintf('  <error>✗ Circular dependency detected:</> %s', $key));
203
                            $hasErrors = true;
204
                        }
205
                    }
206
                }
207
            }
208
209
            if (!$hasErrors) {
210
                $output->writeln('  <fg=green>✓ No circular dependencies detected</fg=green>');
211
            }
212
        } catch (Throwable $throwable) {
213
            $output->writeln(sprintf('  <fg=yellow>⚠ Warning: Could not check circular dependencies: %s</fg=yellow>', $throwable->getMessage()));
214
            $hasWarnings = true;
215
        }
216
217
        $output->writeln('');
218
219
        return ['errors' => $hasErrors, 'warnings' => $hasWarnings];
220
    }
221
222
    private function getHelpText(): string
223
    {
224
        return <<<'HELP'
225
This command validates your Gacela configuration for common errors and potential issues.
226
227
<info>What it checks:</info>
228
  - Existence of gacela.php configuration file
229
  - Bindings configuration:
230
    - Validates that binding keys (interfaces/classes) exist
231
    - Validates that binding values (classes) exist
232
    - Checks type compatibility between keys and values
233
  - Circular dependency detection (basic check)
234
  - Configuration file paths
235
236
<info>Validation levels:</info>
237
  <error>Errors</error> - Critical issues that will cause runtime failures
238
  <fg=yellow>Warnings</fg=yellow> - Potential issues or best practice violations that may work but should be reviewed
239
240
<info>Examples:</info>
241
  # Validate configuration
242
  bin/gacela validate:config
243
244
<comment>Best practices:</comment>
245
  - Run this command before deploying to production
246
  - Add it to your CI/CD pipeline
247
  - Run after adding new bindings or changing configuration
248
  - Use with cache:warm for complete pre-deployment validation
249
250
<info>Exit codes:</info>
251
  0 - Validation successful (may have warnings)
252
  1 - Validation failed with errors
253
HELP;
254
    }
255
}
256