Passed
Pull Request — master (#176)
by
unknown
02:38
created

TDBMDaoGenerator::generateResultIterator()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 42
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 21
c 0
b 0
f 0
dl 0
loc 42
rs 9.584
cc 3
nc 3
nop 1
1
<?php
2
declare(strict_types=1);
3
4
namespace TheCodingMachine\TDBM\Utils;
5
6
use Doctrine\Common\Inflector\Inflector;
7
use Doctrine\DBAL\Schema\Schema;
8
use Doctrine\DBAL\Schema\Table;
9
use Doctrine\DBAL\Types\Type;
10
use Psr\Container\ContainerInterface;
11
use TheCodingMachine\TDBM\Schema\ForeignKeys;
12
use TheCodingMachine\TDBM\TDBMService;
13
use Zend\Code\Generator\AbstractMemberGenerator;
14
use Zend\Code\Generator\ClassGenerator;
15
use Zend\Code\Generator\DocBlock\Tag\VarTag;
16
use Zend\Code\Generator\DocBlockGenerator;
17
use Zend\Code\Generator\FileGenerator;
18
use Zend\Code\Generator\MethodGenerator;
19
use Zend\Code\Generator\ParameterGenerator;
20
use Zend\Code\Generator\PropertyGenerator;
21
use function str_replace;
22
use TheCodingMachine\TDBM\ConfigurationInterface;
23
use TheCodingMachine\TDBM\TDBMException;
24
use TheCodingMachine\TDBM\TDBMSchemaAnalyzer;
25
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
26
use Symfony\Component\Filesystem\Filesystem;
27
use function strpos;
28
use function substr;
29
use function var_export;
30
31
/**
32
 * This class generates automatically DAOs and Beans for TDBM.
33
 */
34
class TDBMDaoGenerator
35
{
36
    /**
37
     * @var Schema
38
     */
39
    private $schema;
40
41
    /**
42
     * @var TDBMSchemaAnalyzer
43
     */
44
    private $tdbmSchemaAnalyzer;
45
46
    /**
47
     * @var GeneratorListenerInterface
48
     */
49
    private $eventDispatcher;
50
51
    /**
52
     * @var NamingStrategyInterface
53
     */
54
    private $namingStrategy;
55
    /**
56
     * @var ConfigurationInterface
57
     */
58
    private $configuration;
59
60
    /**
61
     * Constructor.
62
     *
63
     * @param ConfigurationInterface $configuration
64
     * @param TDBMSchemaAnalyzer $tdbmSchemaAnalyzer
65
     */
66
    public function __construct(ConfigurationInterface $configuration, TDBMSchemaAnalyzer $tdbmSchemaAnalyzer)
67
    {
68
        $this->configuration = $configuration;
69
        $this->schema = $tdbmSchemaAnalyzer->getSchema();
70
        $this->tdbmSchemaAnalyzer = $tdbmSchemaAnalyzer;
71
        $this->namingStrategy = $configuration->getNamingStrategy();
72
        $this->eventDispatcher = $configuration->getGeneratorEventDispatcher();
73
    }
74
75
    /**
76
     * Generates all the daos and beans.
77
     *
78
     * @throws TDBMException
79
     */
80
    public function generateAllDaosAndBeans(): void
81
    {
82
        // TODO: check that no class name ends with "Base". Otherwise, there will be name clash.
83
84
        $tableList = $this->schema->getTables();
85
86
        // Remove all beans and daos from junction tables
87
        $junctionTables = $this->configuration->getSchemaAnalyzer()->detectJunctionTables(true);
88
        $junctionTableNames = array_map(function (Table $table) {
89
            return $table->getName();
90
        }, $junctionTables);
91
92
        $tableList = array_filter($tableList, function (Table $table) use ($junctionTableNames) {
93
            return !in_array($table->getName(), $junctionTableNames, true);
94
        });
95
96
        $this->cleanUpGenerated();
97
98
        $beanDescriptors = [];
99
100
        $beanRegistry = new BeanRegistry($this->configuration, $this->schema, $this->tdbmSchemaAnalyzer, $this->namingStrategy);
101
        foreach ($tableList as $table) {
102
            $beanDescriptors[] = $beanRegistry->addBeanForTable($table);
103
        }
104
        foreach ($beanDescriptors as $beanDescriptor) {
105
            $beanDescriptor->initBeanPropertyDescriptors();
106
        }
107
        foreach ($beanDescriptors as $beanDescriptor) {
108
            $this->generateBean($beanDescriptor);
109
            $this->generateDao($beanDescriptor);
110
            $this->generateResultIterator($beanDescriptor);
111
        }
112
113
        $this->generateFactory($beanDescriptors);
114
115
        // Let's call the list of listeners
116
        $this->eventDispatcher->onGenerate($this->configuration, $beanDescriptors);
117
    }
118
119
    /**
120
     * Removes all files from the Generated folders.
121
     * This is a way to ensure that when a table is deleted, the matching bean/dao are deleted.
122
     * Note: only abstract generated classes are deleted. We do not delete the code that might have been customized
123
     * by the user. The user will need to delete this code him/herself
124
     */
125
    private function cleanUpGenerated(): void
126
    {
127
        $generatedBeanDir = $this->configuration->getPathFinder()->getPath($this->configuration->getBeanNamespace().'\\Generated\\Xxx')->getPath();
128
        $this->deleteAllPhpFiles($generatedBeanDir);
129
130
        $generatedDaoDir = $this->configuration->getPathFinder()->getPath($this->configuration->getDaoNamespace().'\\Generated\\Xxx')->getPath();
131
        $this->deleteAllPhpFiles($generatedDaoDir);
132
133
        $generatedResultIteratorDir = $this->configuration->getPathFinder()->getPath($this->configuration->getResultIteratorNamespace().'\\Generated\\Xxx')->getPath();
134
        $this->deleteAllPhpFiles($generatedResultIteratorDir);
135
    }
136
137
    private function deleteAllPhpFiles(string $directory): void
138
    {
139
        $files = glob($directory.'/*.php', GLOB_NOSORT);
140
        $fileSystem = new Filesystem();
141
        $fileSystem->remove($files);
0 ignored issues
show
Bug introduced by
It seems like $files can also be of type false; however, parameter $files of Symfony\Component\Filesystem\Filesystem::remove() does only seem to accept iterable|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

141
        $fileSystem->remove(/** @scrutinizer ignore-type */ $files);
Loading history...
142
    }
143
144
    /**
145
     * Writes the PHP bean file with all getters and setters from the table passed in parameter.
146
     *
147
     * @param BeanDescriptor  $beanDescriptor
148
     *
149
     * @throws TDBMException
150
     */
151
    public function generateBean(BeanDescriptor $beanDescriptor): void
152
    {
153
        $className = $beanDescriptor->getBeanClassName();
154
        $baseClassName = $beanDescriptor->getBaseBeanClassName();
155
        $table = $beanDescriptor->getTable();
156
        $beannamespace = $this->configuration->getBeanNamespace();
157
        $file = $beanDescriptor->generatePhpCode();
158
        if ($file === null) {
159
            return;
160
        }
161
162
        $possibleBaseFileName = $this->configuration->getPathFinder()->getPath($beannamespace.'\\Generated\\'.$baseClassName)->getPathname();
163
164
        $fileContent = $file->generate();
165
166
        // Hard code PSR-2 fix
167
        $fileContent = $this->psr2Fix($fileContent);
168
        // Add the declare strict-types directive
169
        $commentEnd = strpos($fileContent, ' */') + 3;
170
        $fileContent = substr($fileContent, 0, $commentEnd) . "\n\ndeclare(strict_types=1);" . substr($fileContent, $commentEnd + 1);
171
172
        $this->dumpFile($possibleBaseFileName, $fileContent);
173
174
        $possibleFileName = $this->configuration->getPathFinder()->getPath($beannamespace.'\\'.$className)->getPathname();
175
176
        if (!file_exists($possibleFileName)) {
177
            $tableName = $table->getName();
178
            $str = "<?php
179
/*
180
 * This file has been automatically generated by TDBM.
181
 * You can edit this file as it will not be overwritten.
182
 */
183
184
declare(strict_types=1);
185
186
namespace {$beannamespace};
187
188
use {$beannamespace}\\Generated\\{$baseClassName};
189
190
/**
191
 * The $className class maps the '$tableName' table in database.
192
 */
193
class $className extends $baseClassName
194
{
195
}
196
";
197
198
            $this->dumpFile($possibleFileName, $str);
199
        }
200
    }
201
202
    /**
203
     * Writes the PHP bean DAO with simple functions to create/get/save objects.
204
     *
205
     * @param BeanDescriptor  $beanDescriptor
206
     *
207
     * @throws TDBMException
208
     */
209
    private function generateDao(BeanDescriptor $beanDescriptor): void
210
    {
211
        $className = $beanDescriptor->getDaoClassName();
212
        $baseClassName = $beanDescriptor->getBaseDaoClassName();
213
        $beanClassName = $beanDescriptor->getBeanClassName();
214
        $table = $beanDescriptor->getTable();
215
        $file = $beanDescriptor->generateDaoPhpCode();
216
        if ($file === null) {
217
            return;
218
        }
219
        $daonamespace = $this->configuration->getDaoNamespace();
220
        $tableName = $table->getName();
221
222
        $beanClassWithoutNameSpace = $beanClassName;
223
224
        $possibleBaseFileName = $this->configuration->getPathFinder()->getPath($daonamespace.'\\Generated\\'.$baseClassName)->getPathname();
225
226
        $fileContent = $file->generate();
227
228
        // Hard code PSR-2 fix
229
        $fileContent = $this->psr2Fix($fileContent);
230
        // Add the declare strict-types directive
231
        $commentEnd = strpos($fileContent, ' */') + 3;
232
        $fileContent = substr($fileContent, 0, $commentEnd) . "\n\ndeclare(strict_types=1);" . substr($fileContent, $commentEnd + 1);
233
234
        $this->dumpFile($possibleBaseFileName, $fileContent);
235
236
237
        $possibleFileName = $this->configuration->getPathFinder()->getPath($daonamespace.'\\'.$className)->getPathname();
238
239
        // Now, let's generate the "editable" class
240
        if (!file_exists($possibleFileName)) {
241
            $str = "<?php
242
/*
243
 * This file has been automatically generated by TDBM.
244
 * You can edit this file as it will not be overwritten.
245
 */
246
247
declare(strict_types=1);
248
249
namespace {$daonamespace};
250
251
use {$daonamespace}\\Generated\\{$baseClassName};
252
253
/**
254
 * The $className class will maintain the persistence of $beanClassWithoutNameSpace class into the $tableName table.
255
 */
256
class $className extends $baseClassName
257
{
258
}
259
";
260
            $this->dumpFile($possibleFileName, $str);
261
        }
262
    }
263
264
    /**
265
     * Fixes PSR-2 for files generated by Zend-Code
266
     */
267
    private function psr2Fix(string $content): string
268
    {
269
        return str_replace(
270
            [
271
                "\n\n}\n",
272
                "{\n\n    use",
273
            ],
274
            [
275
                '}',
276
                "{\n    use",
277
            ],
278
            $content
279
        );
280
    }
281
282
    /**
283
     * Writes the PHP ResultIterator file with typed accessors.
284
     */
285
    private function generateResultIterator(BeanDescriptor $beanDescriptor) : void
286
    {
287
        $resultIteratorClassName = $beanDescriptor->getResultIteratorClassName();
288
        $resultIteratorBaseClassName = $beanDescriptor->getBaseResultIteratorClassName();
289
        $resultIteratorNamespace = $this->configuration->getResultIteratorNamespace();
290
        $resultIteratorBaseNamespace = $resultIteratorNamespace . '\\Generated';
291
        $beanClassWithoutNameSpace = $beanDescriptor->getBeanClassName();
292
        $file = $beanDescriptor->generateResultIteratorPhpCode();
293
        if ($file === null) {
294
            return;
295
        }
296
297
        $fileContent = $this->psr2Fix($file->generate());
298
        $commentEnd = strpos($fileContent, ' */') + 3;
299
        $fileContent = substr($fileContent, 0, $commentEnd) . "\n\ndeclare(strict_types=1);" . substr($fileContent, $commentEnd + 1);
300
301
        $baseResultIteratorFilePath = $this->configuration->getPathFinder()->getPath($resultIteratorBaseNamespace.'\\'.$resultIteratorBaseClassName)->getPathname();
302
        $this->dumpFile($baseResultIteratorFilePath, $fileContent);
303
304
        $resultIteratorFilePath = $this->configuration->getPathFinder()->getPath($resultIteratorNamespace.'\\'.$resultIteratorClassName)->getPathname();
305
        // Now, let's generate the "editable" class
306
        if (!file_exists($resultIteratorFilePath)) {
307
            $str = "<?php
308
/*
309
 * This file has been automatically generated by TDBM.
310
 * You can edit this file as it will not be overwritten.
311
 */
312
313
declare(strict_types=1);
314
315
namespace {$resultIteratorNamespace};
316
317
use {$resultIteratorBaseNamespace}\\{$resultIteratorBaseClassName};
318
319
/**
320
 * The $resultIteratorClassName class will iterate over results of $beanClassWithoutNameSpace class.
321
 */
322
class $resultIteratorClassName extends $resultIteratorBaseClassName
323
{
324
}
325
";
326
            $this->dumpFile($resultIteratorFilePath, $str);
327
        }
328
    }
329
330
    /**
331
     * Generates the factory bean.
332
     *
333
     * @param BeanDescriptor[] $beanDescriptors
334
     * @throws TDBMException
335
     */
336
    private function generateFactory(array $beanDescriptors) : void
337
    {
338
        $daoNamespace = $this->configuration->getDaoNamespace();
339
        $daoFactoryClassName = $this->namingStrategy->getDaoFactoryClassName();
340
341
        $file = new FileGenerator();
342
        $file->setDocBlock(
343
            new DocBlockGenerator(
344
                <<<DOC
345
This file has been automatically generated by TDBM.
346
DO NOT edit this file, as it might be overwritten.
347
DOC
348
            )
349
        );
350
        $class = new ClassGenerator();
351
        $file->setClass($class);
352
        $class->setName($daoFactoryClassName);
353
        $file->setNamespace($daoNamespace.'\\Generated');
354
355
        $class->setDocBlock(new DocBlockGenerator("The $daoFactoryClassName provides an easy access to all DAOs generated by TDBM."));
356
357
        $containerProperty = new PropertyGenerator('container');
358
        $containerProperty->setVisibility(AbstractMemberGenerator::VISIBILITY_PRIVATE);
359
        $containerProperty->setDocBlock(new DocBlockGenerator(null, null, [new VarTag(null, ['\\'.ContainerInterface::class])]));
360
        $class->addPropertyFromGenerator($containerProperty);
361
362
        $constructorMethod = new MethodGenerator(
363
            '__construct',
364
            [ new ParameterGenerator('container', ContainerInterface::class) ],
365
            MethodGenerator::FLAG_PUBLIC,
366
            '$this->container = $container;'
367
        );
368
        $constructorMethod = $this->configuration->getCodeGeneratorListener()->onDaoFactoryConstructorGenerated($constructorMethod, $beanDescriptors, $this->configuration, $class);
369
        if ($constructorMethod !== null) {
370
            $class->addMethodFromGenerator($constructorMethod);
371
        }
372
373
        // For each table, let's write a property.
374
        foreach ($beanDescriptors as $beanDescriptor) {
375
            $daoClassName = $beanDescriptor->getDaoClassName();
376
            $daoInstanceName = self::toVariableName($daoClassName);
377
378
            $daoInstanceProperty = new PropertyGenerator($daoInstanceName);
379
            $daoInstanceProperty->setVisibility(AbstractMemberGenerator::VISIBILITY_PRIVATE);
380
            $daoInstanceProperty->setDocBlock(new DocBlockGenerator(null, null, [new VarTag(null, ['\\' . $daoNamespace . '\\' . $daoClassName, 'null'])]));
381
            $class->addPropertyFromGenerator($daoInstanceProperty);
382
383
            $fullClassNameVarExport = var_export($daoNamespace.'\\'.$daoClassName, true);
384
            $getterBody = <<<BODY
385
if (!\$this->$daoInstanceName) {
386
    \$this->$daoInstanceName = \$this->container->get($fullClassNameVarExport);
387
}
388
389
return \$this->$daoInstanceName;
390
BODY;
391
392
            $getterMethod = new MethodGenerator(
393
                'get' . $daoClassName,
394
                [],
395
                MethodGenerator::FLAG_PUBLIC,
396
                $getterBody
397
            );
398
            $getterMethod->setReturnType($daoNamespace.'\\'.$daoClassName);
399
            $getterMethod = $this->configuration->getCodeGeneratorListener()->onDaoFactoryGetterGenerated($getterMethod, $beanDescriptor, $this->configuration, $class);
400
            if ($getterMethod !== null) {
401
                $class->addMethodFromGenerator($getterMethod);
402
            }
403
404
            $setterMethod = new MethodGenerator(
405
                'set' . $daoClassName,
406
                [new ParameterGenerator($daoInstanceName, '\\' . $daoNamespace . '\\' . $daoClassName)],
407
                MethodGenerator::FLAG_PUBLIC,
408
                '$this->' . $daoInstanceName . ' = $' . $daoInstanceName . ';'
409
            );
410
            $setterMethod->setReturnType('void');
411
            $setterMethod = $this->configuration->getCodeGeneratorListener()->onDaoFactorySetterGenerated($setterMethod, $beanDescriptor, $this->configuration, $class);
412
            if ($setterMethod !== null) {
413
                $class->addMethodFromGenerator($setterMethod);
414
            }
415
        }
416
417
        $file = $this->configuration->getCodeGeneratorListener()->onDaoFactoryGenerated($file, $beanDescriptors, $this->configuration);
418
419
        if ($file !== null) {
420
            $possibleFileName = $this->configuration->getPathFinder()->getPath($daoNamespace.'\\Generated\\'.$daoFactoryClassName)->getPathname();
421
422
            $fileContent = $file->generate();
423
424
            // Hard code PSR-2 fix
425
            $fileContent = $this->psr2Fix($fileContent);
426
            // Add the declare strict-types directive
427
            $commentEnd = strpos($fileContent, ' */') + 3;
428
            $fileContent = substr($fileContent, 0, $commentEnd) . "\n\ndeclare(strict_types=1);" . substr($fileContent, $commentEnd + 1);
429
430
            $this->dumpFile($possibleFileName, $fileContent);
431
        }
432
    }
433
434
    /**
435
     * Transforms a string to camelCase (except the first letter will be uppercase too).
436
     * Underscores and spaces are removed and the first letter after the underscore is uppercased.
437
     * Quoting is removed if present.
438
     *
439
     * @param string $str
440
     *
441
     * @return string
442
     */
443
    public static function toCamelCase(string $str) : string
444
    {
445
        $str = str_replace(array('`', '"', '[', ']'), '', $str);
446
447
        $str = strtoupper(substr($str, 0, 1)).substr($str, 1);
448
        while (true) {
449
            $pos = strpos($str, '_');
450
            if ($pos === false) {
451
                $pos = strpos($str, ' ');
452
                if ($pos === false) {
453
                    break;
454
                }
455
            }
456
457
            $before = substr($str, 0, $pos);
458
            $after = substr($str, $pos + 1);
459
            $str = $before.strtoupper(substr($after, 0, 1)).substr($after, 1);
460
        }
461
462
        return $str;
463
    }
464
465
    /**
466
     * Tries to put string to the singular form (if it is plural).
467
     * We assume the table names are in english.
468
     *
469
     * @param string $str
470
     *
471
     * @return string
472
     */
473
    public static function toSingular(string $str): string
474
    {
475
        return Inflector::singularize($str);
476
    }
477
478
    /**
479
     * Put the first letter of the string in lower case.
480
     * Very useful to transform a class name into a variable name.
481
     *
482
     * @param string $str
483
     *
484
     * @return string
485
     */
486
    public static function toVariableName(string $str): string
487
    {
488
        return strtolower(substr($str, 0, 1)).substr($str, 1);
489
    }
490
491
    /**
492
     * Ensures the file passed in parameter can be written in its directory.
493
     *
494
     * @param string $fileName
495
     *
496
     * @throws TDBMException
497
     */
498
    private function ensureDirectoryExist(string $fileName): void
499
    {
500
        $dirName = dirname($fileName);
501
        if (!file_exists($dirName)) {
502
            $old = umask(0);
503
            $result = mkdir($dirName, 0775, true);
504
            umask($old);
505
            if ($result === false) {
506
                throw new TDBMException("Unable to create directory: '".$dirName."'.");
507
            }
508
        }
509
    }
510
511
    private function dumpFile(string $fileName, string $content) : void
512
    {
513
        $this->ensureDirectoryExist($fileName);
514
        $fileSystem = new Filesystem();
515
        $fileSystem->dumpFile($fileName, $content);
516
        @chmod($fileName, 0664);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

516
        /** @scrutinizer ignore-unhandled */ @chmod($fileName, 0664);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
517
    }
518
519
    /**
520
     * Transforms a DBAL type into a PHP type (for PHPDoc purpose).
521
     *
522
     * @param Type $type The DBAL type
523
     *
524
     * @return string The PHP type
525
     */
526
    public static function dbalTypeToPhpType(Type $type) : string
527
    {
528
        $map = [
529
            Type::TARRAY => 'array',
530
            Type::SIMPLE_ARRAY => 'array',
531
            'json' => 'array',  // 'json' is supported from Doctrine DBAL 2.6 only.
532
            Type::JSON_ARRAY => 'array',
533
            Type::BIGINT => 'string',
534
            Type::BOOLEAN => 'bool',
535
            Type::DATETIME_IMMUTABLE => '\DateTimeImmutable',
536
            Type::DATETIMETZ_IMMUTABLE => '\DateTimeImmutable',
537
            Type::DATE_IMMUTABLE => '\DateTimeImmutable',
538
            Type::TIME_IMMUTABLE => '\DateTimeImmutable',
539
            Type::DECIMAL => 'string',
540
            Type::INTEGER => 'int',
541
            Type::OBJECT => 'string',
542
            Type::SMALLINT => 'int',
543
            Type::STRING => 'string',
544
            Type::TEXT => 'string',
545
            Type::BINARY => 'resource',
546
            Type::BLOB => 'resource',
547
            Type::FLOAT => 'float',
548
            Type::GUID => 'string',
549
        ];
550
551
        return isset($map[$type->getName()]) ? $map[$type->getName()] : $type->getName();
552
    }
553
554
    /**
555
     * @param Table $table
556
     * @return string[]
557
     * @throws TDBMException
558
     */
559
    public static function getPrimaryKeyColumnsOrFail(Table $table): array
560
    {
561
        if ($table->getPrimaryKey() === null) {
562
            // Security check: a table MUST have a primary key
563
            throw new TDBMException(sprintf('Table "%s" does not have any primary key', $table->getName()));
564
        }
565
        return $table->getPrimaryKey()->getUnquotedColumns();
566
    }
567
}
568