Passed
Pull Request — master (#126)
by David
05:59
created

TDBMDaoGenerator::psr2Fix()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 10
rs 10
c 0
b 0
f 0
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 function str_replace;
11
use TheCodingMachine\TDBM\ConfigurationInterface;
12
use TheCodingMachine\TDBM\TDBMException;
13
use TheCodingMachine\TDBM\TDBMSchemaAnalyzer;
14
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
15
use Symfony\Component\Filesystem\Filesystem;
16
17
/**
18
 * This class generates automatically DAOs and Beans for TDBM.
19
 */
20
class TDBMDaoGenerator
21
{
22
    /**
23
     * @var Schema
24
     */
25
    private $schema;
26
27
    /**
28
     * @var TDBMSchemaAnalyzer
29
     */
30
    private $tdbmSchemaAnalyzer;
31
32
    /**
33
     * @var GeneratorListenerInterface
34
     */
35
    private $eventDispatcher;
36
37
    /**
38
     * @var NamingStrategyInterface
39
     */
40
    private $namingStrategy;
41
    /**
42
     * @var ConfigurationInterface
43
     */
44
    private $configuration;
45
46
    /**
47
     * Constructor.
48
     *
49
     * @param ConfigurationInterface $configuration
50
     * @param TDBMSchemaAnalyzer $tdbmSchemaAnalyzer
51
     */
52
    public function __construct(ConfigurationInterface $configuration, TDBMSchemaAnalyzer $tdbmSchemaAnalyzer)
53
    {
54
        $this->configuration = $configuration;
55
        $this->schema = $tdbmSchemaAnalyzer->getSchema();
56
        $this->tdbmSchemaAnalyzer = $tdbmSchemaAnalyzer;
57
        $this->namingStrategy = $configuration->getNamingStrategy();
58
        $this->eventDispatcher = $configuration->getGeneratorEventDispatcher();
59
    }
60
61
    /**
62
     * Generates all the daos and beans.
63
     *
64
     * @throws TDBMException
65
     */
66
    public function generateAllDaosAndBeans(): void
67
    {
68
        // TODO: check that no class name ends with "Base". Otherwise, there will be name clash.
69
70
        $tableList = $this->schema->getTables();
71
72
        // Remove all beans and daos from junction tables
73
        $junctionTables = $this->configuration->getSchemaAnalyzer()->detectJunctionTables(true);
74
        $junctionTableNames = array_map(function (Table $table) {
75
            return $table->getName();
76
        }, $junctionTables);
77
78
        $tableList = array_filter($tableList, function (Table $table) use ($junctionTableNames) {
79
            return !in_array($table->getName(), $junctionTableNames);
80
        });
81
82
        $this->cleanUpGenerated();
83
84
        $beanDescriptors = [];
85
86
        foreach ($tableList as $table) {
87
            $beanDescriptors[] = $this->generateDaoAndBean($table);
88
        }
89
90
91
        $this->generateFactory($tableList);
92
93
        // Let's call the list of listeners
94
        $this->eventDispatcher->onGenerate($this->configuration, $beanDescriptors);
95
    }
96
97
    /**
98
     * Removes all files from the Generated folders.
99
     * This is a way to ensure that when a table is deleted, the matching bean/dao are deleted.
100
     * Note: only abstract generated classes are deleted. We do not delete the code that might have been customized
101
     * by the user. The user will need to delete this code him/herself
102
     */
103
    private function cleanUpGenerated(): void
104
    {
105
        $generatedBeanDir = $this->configuration->getPathFinder()->getPath($this->configuration->getBeanNamespace().'\\Generated\\Xxx')->getPath();
106
        $this->deleteAllPhpFiles($generatedBeanDir);
107
108
        $generatedDaoDir = $this->configuration->getPathFinder()->getPath($this->configuration->getDaoNamespace().'\\Generated\\Xxx')->getPath();
109
        $this->deleteAllPhpFiles($generatedDaoDir);
110
    }
111
112
    private function deleteAllPhpFiles(string $directory): void
113
    {
114
        $files = glob($directory.'/*.php');
115
        $fileSystem = new Filesystem();
116
        $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

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

437
        /** @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...
438
    }
439
440
    /**
441
     * Transforms a DBAL type into a PHP type (for PHPDoc purpose).
442
     *
443
     * @param Type $type The DBAL type
444
     *
445
     * @return string The PHP type
446
     */
447
    public static function dbalTypeToPhpType(Type $type) : string
448
    {
449
        $map = [
450
            Type::TARRAY => 'array',
451
            Type::SIMPLE_ARRAY => 'array',
452
            'json' => 'array',  // 'json' is supported from Doctrine DBAL 2.6 only.
453
            Type::JSON_ARRAY => 'array',
454
            Type::BIGINT => 'string',
455
            Type::BOOLEAN => 'bool',
456
            Type::DATETIME_IMMUTABLE => '\DateTimeImmutable',
457
            Type::DATETIMETZ_IMMUTABLE => '\DateTimeImmutable',
458
            Type::DATE_IMMUTABLE => '\DateTimeImmutable',
459
            Type::TIME_IMMUTABLE => '\DateTimeImmutable',
460
            Type::DECIMAL => 'string',
461
            Type::INTEGER => 'int',
462
            Type::OBJECT => 'string',
463
            Type::SMALLINT => 'int',
464
            Type::STRING => 'string',
465
            Type::TEXT => 'string',
466
            Type::BINARY => 'resource',
467
            Type::BLOB => 'resource',
468
            Type::FLOAT => 'float',
469
            Type::GUID => 'string',
470
        ];
471
472
        return isset($map[$type->getName()]) ? $map[$type->getName()] : $type->getName();
473
    }
474
475
    /**
476
     * @param Table $table
477
     * @return string[]
478
     * @throws TDBMException
479
     */
480
    public static function getPrimaryKeyColumnsOrFail(Table $table): array
481
    {
482
        if ($table->getPrimaryKey() === null) {
483
            // Security check: a table MUST have a primary key
484
            throw new TDBMException(sprintf('Table "%s" does not have any primary key', $table->getName()));
485
        }
486
        return $table->getPrimaryKey()->getUnquotedColumns();
487
    }
488
}
489