Passed
Pull Request — master (#157)
by David
03:18
created

TDBMDaoGenerator::psr2Fix()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 12
rs 10
cc 1
nc 1
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);
94
        });
95
96
        $this->cleanUpGenerated();
97
98
        $beanDescriptors = [];
99
100
        foreach ($tableList as $table) {
101
            $beanDescriptors[] = $this->generateDaoAndBean($table);
102
        }
103
104
105
        $this->generateFactory($beanDescriptors);
106
107
        // Let's call the list of listeners
108
        $this->eventDispatcher->onGenerate($this->configuration, $beanDescriptors);
109
    }
110
111
    /**
112
     * Removes all files from the Generated folders.
113
     * This is a way to ensure that when a table is deleted, the matching bean/dao are deleted.
114
     * Note: only abstract generated classes are deleted. We do not delete the code that might have been customized
115
     * by the user. The user will need to delete this code him/herself
116
     */
117
    private function cleanUpGenerated(): void
118
    {
119
        $generatedBeanDir = $this->configuration->getPathFinder()->getPath($this->configuration->getBeanNamespace().'\\Generated\\Xxx')->getPath();
120
        $this->deleteAllPhpFiles($generatedBeanDir);
121
122
        $generatedDaoDir = $this->configuration->getPathFinder()->getPath($this->configuration->getDaoNamespace().'\\Generated\\Xxx')->getPath();
123
        $this->deleteAllPhpFiles($generatedDaoDir);
124
    }
125
126
    private function deleteAllPhpFiles(string $directory): void
127
    {
128
        $files = glob($directory.'/*.php');
129
        $fileSystem = new Filesystem();
130
        $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

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

479
        /** @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...
480
    }
481
482
    /**
483
     * Transforms a DBAL type into a PHP type (for PHPDoc purpose).
484
     *
485
     * @param Type $type The DBAL type
486
     *
487
     * @return string The PHP type
488
     */
489
    public static function dbalTypeToPhpType(Type $type) : string
490
    {
491
        $map = [
492
            Type::TARRAY => 'array',
493
            Type::SIMPLE_ARRAY => 'array',
494
            'json' => 'array',  // 'json' is supported from Doctrine DBAL 2.6 only.
495
            Type::JSON_ARRAY => 'array',
496
            Type::BIGINT => 'string',
497
            Type::BOOLEAN => 'bool',
498
            Type::DATETIME_IMMUTABLE => '\DateTimeImmutable',
499
            Type::DATETIMETZ_IMMUTABLE => '\DateTimeImmutable',
500
            Type::DATE_IMMUTABLE => '\DateTimeImmutable',
501
            Type::TIME_IMMUTABLE => '\DateTimeImmutable',
502
            Type::DECIMAL => 'string',
503
            Type::INTEGER => 'int',
504
            Type::OBJECT => 'string',
505
            Type::SMALLINT => 'int',
506
            Type::STRING => 'string',
507
            Type::TEXT => 'string',
508
            Type::BINARY => 'resource',
509
            Type::BLOB => 'resource',
510
            Type::FLOAT => 'float',
511
            Type::GUID => 'string',
512
        ];
513
514
        return isset($map[$type->getName()]) ? $map[$type->getName()] : $type->getName();
515
    }
516
517
    /**
518
     * @param Table $table
519
     * @return string[]
520
     * @throws TDBMException
521
     */
522
    public static function getPrimaryKeyColumnsOrFail(Table $table): array
523
    {
524
        if ($table->getPrimaryKey() === null) {
525
            // Security check: a table MUST have a primary key
526
            throw new TDBMException(sprintf('Table "%s" does not have any primary key', $table->getName()));
527
        }
528
        return $table->getPrimaryKey()->getUnquotedColumns();
529
    }
530
}
531