• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DoclerLabs / api-client-generator / 22295254932

23 Feb 2026 06:24AM UTC coverage: 86.854% (+0.2%) from 86.69%
22295254932

Pull #134

github

web-flow
Merge 0fc1b2b40 into 6239ec50b
Pull Request #134: feat: RFC 6839 support #133

50 of 53 new or added lines in 6 files covered. (94.34%)

3495 of 4024 relevant lines covered (86.85%)

7.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.67
/src/Command/GenerateCommand.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace DoclerLabs\ApiClientGenerator\Command;
6

7
use DoclerLabs\ApiClientGenerator\CodeGeneratorFacade;
8
use DoclerLabs\ApiClientGenerator\Entity\ContentType;
9
use DoclerLabs\ApiClientGenerator\Generator\Security\BasicAuthenticationSecurityStrategy;
10
use DoclerLabs\ApiClientGenerator\Input\Configuration;
11
use DoclerLabs\ApiClientGenerator\Input\FileReader;
12
use DoclerLabs\ApiClientGenerator\Input\Parser;
13
use DoclerLabs\ApiClientGenerator\Input\Specification;
14
use DoclerLabs\ApiClientGenerator\MetaTemplateFacade;
15
use DoclerLabs\ApiClientGenerator\Output\Copy\Request\AuthenticationCredentials;
16
use DoclerLabs\ApiClientGenerator\Output\Copy\Serializer\ContentType\FormUrlencodedContentTypeSerializer;
17
use DoclerLabs\ApiClientGenerator\Output\Copy\Serializer\ContentType\JsonContentTypeSerializer;
18
use DoclerLabs\ApiClientGenerator\Output\Copy\Serializer\ContentType\VdnApiJsonContentTypeSerializer;
19
use DoclerLabs\ApiClientGenerator\Output\Copy\Serializer\ContentType\XmlContentTypeSerializer;
20
use DoclerLabs\ApiClientGenerator\Output\DirectoryPrinter;
21
use DoclerLabs\ApiClientGenerator\Output\Meta\MetaFile;
22
use DoclerLabs\ApiClientGenerator\Output\Meta\MetaFileCollection;
23
use DoclerLabs\ApiClientGenerator\Output\MetaFilePrinter;
24
use DoclerLabs\ApiClientGenerator\Output\Php\PhpFile;
25
use DoclerLabs\ApiClientGenerator\Output\Php\PhpFileCollection;
26
use DoclerLabs\ApiClientGenerator\Output\PhpFilePrinter;
27
use DoclerLabs\ApiClientGenerator\Output\StaticPhpFileCopier;
28
use DoclerLabs\ApiClientGenerator\Output\WarningFormatter;
29
use ReflectionClass;
30
use Symfony\Component\Console\Command\Command;
31
use Symfony\Component\Console\Input\InputInterface;
32
use Symfony\Component\Console\Output\OutputInterface;
33
use Symfony\Component\Console\Style\StyleInterface;
34
use Symfony\Component\Console\Style\SymfonyStyle;
35
use Symfony\Component\Filesystem\Filesystem;
36
use Symfony\Component\Finder\Finder;
37
use Throwable;
38

39
class GenerateCommand extends Command
40
{
41
    public function __construct(
42
        private Configuration $configuration,
43
        private FileReader $fileReader,
44
        private Parser $parser,
45
        private CodeGeneratorFacade $codeGenerator,
46
        private PhpFilePrinter $phpPrinter,
47
        private DirectoryPrinter $directoryPrinter,
48
        private MetaTemplateFacade $metaTemplate,
49
        private MetaFilePrinter $templatePrinter,
50
        private Finder $fileFinder,
51
        private StaticPhpFileCopier $staticPhpPrinter,
52
        private Filesystem $filesystem,
53
        private WarningFormatter $warningFormatter
54
    ) {
55
        parent::__construct();
2✔
56
    }
57

58
    public function configure(): void
59
    {
60
        $this->setName('generate');
2✔
61
        $this->setDescription('Generate an api client based on a given OpenApi specification');
2✔
62
        $this->addUsage(
2✔
63
            'OPENAPI={path}/swagger.yaml NAMESPACE=Group\SomeApiClient PACKAGE=dh-group/some-api-client OUTPUT_DIR={path}/generated CODE_STYLE={path}/.php-cs-fixer.php.dist ./bin/api-client-generator generate'
2✔
64
        );
2✔
65
    }
66

67
    public function execute(InputInterface $input, OutputInterface $output): int
68
    {
69
        $this->initWarningPrinting($input);
2✔
70
        $specificationFilePath = $this->configuration->specificationFilePath;
2✔
71

72
        $specification = $this->parser->parse(
2✔
73
            $this->fileReader->read($specificationFilePath),
2✔
74
            $specificationFilePath
2✔
75
        );
2✔
76

77
        $ss = new SymfonyStyle($input, $output);
2✔
78

79
        $this->backup($ss);
2✔
80

81
        try {
82
            $this->generatePhpFiles($ss, $specification);
2✔
83
            $this->copyStaticPhpFiles($ss, $specification);
2✔
84
            $this->copySpecification($ss);
2✔
85
            $this->generateMetaFiles($ss, $specification);
2✔
86
        } catch (Throwable $throwable) {
×
87
            $this->restoreBackup($ss);
×
88
            trigger_error($throwable->getMessage(), E_USER_WARNING);
×
89

90
            return Command::FAILURE;
×
91
        }
92

93
        $this->removeBackup($ss);
2✔
94

95
        return Command::SUCCESS;
2✔
96
    }
97

98
    private function generatePhpFiles(StyleInterface $ss, Specification $specification): void
99
    {
100
        $phpFiles = new PhpFileCollection();
2✔
101
        $this->codeGenerator->generate($specification, $phpFiles);
2✔
102

103
        $ss->text(sprintf('<info>AST generated for %d PHP files.</info>', $phpFiles->count()));
2✔
104
        $ss->text(sprintf('Write PHP files to %s:', $this->configuration->outputDirectory));
2✔
105

106
        $ss->progressStart($phpFiles->count());
2✔
107
        foreach ($phpFiles as $phpFile) {
2✔
108
            /** @var PhpFile $phpFile */
109
            $this->phpPrinter->print(
2✔
110
                sprintf(
2✔
111
                    '%s/%s/%s',
2✔
112
                    $this->configuration->outputDirectory,
2✔
113
                    $this->configuration->sourceDirectory,
2✔
114
                    $phpFile->fileName
2✔
115
                ),
2✔
116
                $phpFile
2✔
117
            );
2✔
118
            $ss->progressAdvance();
2✔
119
        }
120
        $ss->progressFinish();
2✔
121
    }
122

123
    private function generateMetaFiles(StyleInterface $ss, Specification $specification): void
124
    {
125
        $metaFiles = new MetaFileCollection();
2✔
126
        $this->metaTemplate->render($specification, $metaFiles);
2✔
127

128
        $ss->text(sprintf('<info>Templates rendered for %d meta files.</info>', $metaFiles->count()));
2✔
129
        $ss->text(sprintf('Write meta files to %s:', $this->configuration->outputDirectory));
2✔
130

131
        $ss->progressStart($metaFiles->count());
2✔
132
        foreach ($metaFiles as $metaFile) {
2✔
133
            /** @var MetaFile $metaFile */
134
            $this->templatePrinter->print(
2✔
135
                sprintf('%s/%s', $this->configuration->outputDirectory, $metaFile->filePath),
2✔
136
                $metaFile
2✔
137
            );
2✔
138
            $ss->progressAdvance();
2✔
139
        }
140
        $ss->progressFinish();
2✔
141
    }
142

143
    private function copyStaticPhpFiles(StyleInterface $ss, Specification $specification): void
144
    {
145
        $blacklistedFiles = $this->getBlacklistedFiles($specification);
2✔
146
        $originalFiles    = $this->fileFinder
2✔
147
            ->files()
2✔
148
            ->name('*.php')
2✔
149
            ->in(Configuration::STATIC_PHP_FILE_DIRECTORY);
2✔
150

151
        $ss->text(sprintf('<info>Collected %d static PHP files.</info>', $originalFiles->count()));
2✔
152
        $ss->text(sprintf('Copy static PHP files to %s:', $this->configuration->outputDirectory));
2✔
153

154
        $ss->progressStart($originalFiles->count());
2✔
155
        foreach ($originalFiles as $originalFile) {
2✔
156
            if (!in_array($originalFile->getBasename(), $blacklistedFiles, true)) {
2✔
157
                $destinationPath = sprintf(
2✔
158
                    '%s/%s/%s',
2✔
159
                    $this->configuration->outputDirectory,
2✔
160
                    $this->configuration->sourceDirectory,
2✔
161
                    $originalFile->getRelativePathname()
2✔
162
                );
2✔
163

164
                $this->staticPhpPrinter->copy(
2✔
165
                    $destinationPath,
2✔
166
                    $originalFile
2✔
167
                );
2✔
168
            }
169

170
            $ss->progressAdvance();
2✔
171
        }
172
        $ss->progressFinish();
2✔
173
    }
174

175
    private function copySpecification(StyleInterface $ss): void
176
    {
177
        $destinationPath = sprintf(
2✔
178
            '%s/doc/%s',
2✔
179
            $this->configuration->outputDirectory,
2✔
180
            basename($this->configuration->specificationFilePath)
2✔
181
        );
2✔
182

183
        $ss->text(sprintf('Copy specification file to %s.', $destinationPath));
2✔
184

185
        $this->filesystem->copy(
2✔
186
            $this->configuration->specificationFilePath,
2✔
187
            $destinationPath,
2✔
188
            true
2✔
189
        );
2✔
190
    }
191

192
    private function backup(StyleInterface $ss): void
193
    {
194
        $ss->text('<info>Backup original source.</info>');
2✔
195

196
        $originalPath = sprintf(
2✔
197
            '%s/%s',
2✔
198
            $this->configuration->outputDirectory,
2✔
199
            $this->configuration->sourceDirectory
2✔
200
        );
2✔
201

202
        $backupPath = $originalPath . '_old';
2✔
203

204
        $this->directoryPrinter->move(
2✔
205
            $backupPath,
2✔
206
            $originalPath
2✔
207
        );
2✔
208
    }
209

210
    private function restoreBackup(StyleInterface $ss): void
211
    {
212
        $ss->text('<error>Restore original source from backup.</error>');
×
213

214
        $originalPath = sprintf(
×
215
            '%s/%s',
×
216
            $this->configuration->outputDirectory,
×
217
            $this->configuration->sourceDirectory
×
218
        );
×
219

220
        $backupPath = $originalPath . '_old';
×
221

222
        $this->directoryPrinter->move(
×
223
            $originalPath,
×
224
            $backupPath
×
225
        );
×
226
    }
227

228
    private function removeBackup(StyleInterface $ss): void
229
    {
230
        $ss->text('<info>Delete backup.</info>');
2✔
231

232
        $backupPath = sprintf(
2✔
233
            '%s/%s_old',
2✔
234
            $this->configuration->outputDirectory,
2✔
235
            $this->configuration->sourceDirectory
2✔
236
        );
2✔
237

238
        $this->directoryPrinter->delete($backupPath);
2✔
239
    }
240

241
    private function initWarningPrinting(InputInterface $input): void
242
    {
243
        if ($input->getOption('quiet')) {
2✔
244
            set_error_handler(
2✔
245
                static function (): bool {
2✔
246
                    return true;
2✔
247
                },
2✔
248
                E_USER_WARNING
2✔
249
            );
2✔
250
        } else {
251
            set_error_handler($this->warningFormatter, E_USER_WARNING);
×
252
        }
253
    }
254

255
    private function getUnusedSerializers(Specification $specification): array
256
    {
257
        $allContentTypes   = $specification->getAllContentTypes();
2✔
258
        $unusedSerializers = [];
2✔
259

260
        // JSON serializer is needed if any JSON-based content type is used (RFC 6839)
261
        $needsJsonSerializer = array_reduce(
2✔
262
            $allContentTypes,
2✔
263
            static fn (bool $carry, string $ct): bool => $carry || ContentType::isJsonBased($ct),
2✔
264
            false
2✔
265
        );
2✔
266

267
        if (!$needsJsonSerializer) {
2✔
NEW
268
            $unusedSerializers[] = JsonContentTypeSerializer::class;
×
269
        }
270

271
        if (!in_array(VdnApiJsonContentTypeSerializer::MIME_TYPE, $allContentTypes, true)) {
2✔
272
            $unusedSerializers[] = VdnApiJsonContentTypeSerializer::class;
2✔
273
        }
274

275
        if (!in_array(FormUrlencodedContentTypeSerializer::MIME_TYPE, $allContentTypes, true)) {
2✔
NEW
276
            $unusedSerializers[] = FormUrlencodedContentTypeSerializer::class;
×
277
        }
278

279
        if (!in_array(XmlContentTypeSerializer::MIME_TYPE, $allContentTypes, true)) {
2✔
NEW
280
            $unusedSerializers[] = XmlContentTypeSerializer::class;
×
281
        }
282

283
        return $unusedSerializers;
2✔
284
    }
285

286
    /**
287
     * @return string[]
288
     */
289
    private function getUnusedValueObjects(Specification $specification): array
290
    {
291
        $unusedClasses = [];
2✔
292

293
        if (!$specification->isSecuritySchemeEnabled(BasicAuthenticationSecurityStrategy::SCHEME)) {
2✔
294
            $unusedClasses[] = AuthenticationCredentials::class;
×
295
        }
296

297
        return $unusedClasses;
2✔
298
    }
299

300
    /**
301
     * @return string[]
302
     */
303
    private function getBlacklistedFiles(Specification $specification): array
304
    {
305
        return array_map(
2✔
306
            static fn ($class) => basename((string)(new ReflectionClass($class))->getFileName()),
2✔
307
            array_merge(
2✔
308
                $this->getUnusedSerializers($specification),
2✔
309
                $this->getUnusedValueObjects($specification)
2✔
310
            )
2✔
311
        );
2✔
312
    }
313
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc