Passed
Pull Request — master (#116)
by David
03:19
created

TDBMDaoGenerator::getPrimaryKeyColumnsOrFail()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 2
nc 2
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
     * Name of composer file.
29
     *
30
     * @var string
31
     */
32
    private $composerFile;
0 ignored issues
show
introduced by
The private property $composerFile is not used, and could be removed.
Loading history...
33
34
    /**
35
     * @var TDBMSchemaAnalyzer
36
     */
37
    private $tdbmSchemaAnalyzer;
38
39
    /**
40
     * @var GeneratorListenerInterface
41
     */
42
    private $eventDispatcher;
43
44
    /**
45
     * @var NamingStrategyInterface
46
     */
47
    private $namingStrategy;
48
    /**
49
     * @var ConfigurationInterface
50
     */
51
    private $configuration;
52
53
    /**
54
     * Constructor.
55
     *
56
     * @param ConfigurationInterface $configuration
57
     * @param TDBMSchemaAnalyzer $tdbmSchemaAnalyzer
58
     */
59
    public function __construct(ConfigurationInterface $configuration, TDBMSchemaAnalyzer $tdbmSchemaAnalyzer)
60
    {
61
        $this->configuration = $configuration;
62
        $this->schema = $tdbmSchemaAnalyzer->getSchema();
63
        $this->tdbmSchemaAnalyzer = $tdbmSchemaAnalyzer;
64
        $this->namingStrategy = $configuration->getNamingStrategy();
65
        $this->eventDispatcher = $configuration->getGeneratorEventDispatcher();
66
    }
67
68
    /**
69
     * Generates all the daos and beans.
70
     *
71
     * @throws TDBMException
72
     */
73
    public function generateAllDaosAndBeans(): void
74
    {
75
        // TODO: check that no class name ends with "Base". Otherwise, there will be name clash.
76
77
        $tableList = $this->schema->getTables();
78
79
        // Remove all beans and daos from junction tables
80
        $junctionTables = $this->configuration->getSchemaAnalyzer()->detectJunctionTables(true);
81
        $junctionTableNames = array_map(function (Table $table) {
82
            return $table->getName();
83
        }, $junctionTables);
84
85
        $tableList = array_filter($tableList, function (Table $table) use ($junctionTableNames) {
86
            return !in_array($table->getName(), $junctionTableNames);
87
        });
88
89
        $this->cleanUpGenerated();
90
91
        $beanDescriptors = [];
92
93
        foreach ($tableList as $table) {
94
            $beanDescriptors[] = $this->generateDaoAndBean($table);
95
        }
96
97
98
        $this->generateFactory($tableList);
99
100
        // Let's call the list of listeners
101
        $this->eventDispatcher->onGenerate($this->configuration, $beanDescriptors);
102
    }
103
104
    /**
105
     * Removes all files from the Generated folders.
106
     * This is a way to ensure that when a table is deleted, the matching bean/dao are deleted.
107
     * Note: only abstract generated classes are deleted. We do not delete the code that might have been customized
108
     * by the user. The user will need to delete this code him/herself
109
     */
110
    private function cleanUpGenerated(): void
111
    {
112
        $generatedBeanDir = $this->configuration->getPathFinder()->getPath($this->configuration->getBeanNamespace().'\\Generated\\Xxx')->getPath();
113
        $this->deleteAllPhpFiles($generatedBeanDir);
114
115
        $generatedDaoDir = $this->configuration->getPathFinder()->getPath($this->configuration->getDaoNamespace().'\\Generated\\Xxx')->getPath();
116
        $this->deleteAllPhpFiles($generatedDaoDir);
117
    }
118
119
    private function deleteAllPhpFiles(string $directory): void
120
    {
121
        $files = glob($directory.'/*.php');
122
        $fileSystem = new Filesystem();
123
        $fileSystem->remove($files);
124
    }
125
126
    /**
127
     * Generates in one method call the daos and the beans for one table.
128
     *
129
     * @param Table $table
130
     *
131
     * @return BeanDescriptor
132
     * @throws TDBMException
133
     */
134
    private function generateDaoAndBean(Table $table) : BeanDescriptor
135
    {
136
        $tableName = $table->getName();
137
        $daoName = $this->namingStrategy->getDaoClassName($tableName);
138
        $beanName = $this->namingStrategy->getBeanClassName($tableName);
139
        $baseBeanName = $this->namingStrategy->getBaseBeanClassName($tableName);
140
        $baseDaoName = $this->namingStrategy->getBaseDaoClassName($tableName);
141
142
        $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);
143
        $this->generateBean($beanDescriptor, $beanName, $baseBeanName, $table);
144
        $this->generateDao($beanDescriptor, $daoName, $baseDaoName, $beanName, $table);
145
        return $beanDescriptor;
146
    }
147
148
    /**
149
     * Writes the PHP bean file with all getters and setters from the table passed in parameter.
150
     *
151
     * @param BeanDescriptor  $beanDescriptor
152
     * @param string          $className       The name of the class
153
     * @param string          $baseClassName   The name of the base class which will be extended (name only, no directory)
154
     * @param Table           $table           The table
155
     *
156
     * @throws TDBMException
157
     */
158
    public function generateBean(BeanDescriptor $beanDescriptor, string $className, string $baseClassName, Table $table): void
159
    {
160
        $beannamespace = $this->configuration->getBeanNamespace();
161
        $file = $beanDescriptor->generatePhpCode();
162
        if ($file === null) {
163
            return;
164
        }
165
166
        $possibleBaseFileName = $this->configuration->getPathFinder()->getPath($beannamespace.'\\Generated\\'.$baseClassName)->getPathname();
167
168
        $fileContent = $file->generate();
169
170
        // Hard code PSR-2 fix
171
        $fileContent = str_replace("\n\n}\n", '}', $fileContent);
172
        // Add the declare strict-types directive
173
        $commentEnd = strpos($fileContent, ' */') + 3;
174
        $fileContent = substr($fileContent, 0, $commentEnd) . "\n\ndeclare(strict_types=1);" . substr($fileContent, $commentEnd + 1);
175
176
        $this->dumpFile($possibleBaseFileName, $fileContent);
177
178
        $possibleFileName = $this->configuration->getPathFinder()->getPath($beannamespace.'\\'.$className)->getPathname();
179
180
        if (!file_exists($possibleFileName)) {
181
            $tableName = $table->getName();
182
            $str = "<?php
183
/*
184
 * This file has been automatically generated by TDBM.
185
 * You can edit this file as it will not be overwritten.
186
 */
187
188
declare(strict_types=1);
189
190
namespace {$beannamespace};
191
192
use {$beannamespace}\\Generated\\{$baseClassName};
193
194
/**
195
 * The $className class maps the '$tableName' table in database.
196
 */
197
class $className extends $baseClassName
198
{
199
}
200
";
201
202
            $this->dumpFile($possibleFileName, $str);
203
        }
204
    }
205
206
    /**
207
     * Writes the PHP bean DAO with simple functions to create/get/save objects.
208
     *
209
     * @param BeanDescriptor  $beanDescriptor
210
     * @param string          $className       The name of the class
211
     * @param string          $baseClassName
212
     * @param string          $beanClassName
213
     * @param Table           $table
214
     *
215
     * @throws TDBMException
216
     */
217
    private function generateDao(BeanDescriptor $beanDescriptor, string $className, string $baseClassName, string $beanClassName, Table $table): void
218
    {
219
        $file = $beanDescriptor->generateDaoPhpCode();
220
        if ($file === null) {
221
            return;
222
        }
223
        $daonamespace = $this->configuration->getDaoNamespace();
224
        $tableName = $table->getName();
225
226
        $beanClassWithoutNameSpace = $beanClassName;
227
228
        $possibleBaseFileName = $this->configuration->getPathFinder()->getPath($daonamespace.'\\Generated\\'.$baseClassName)->getPathname();
229
230
        $fileContent = $file->generate();
231
232
        // Hard code PSR-2 fix
233
        $fileContent = str_replace("\n\n}\n", '}', $fileContent);
234
        // Add the declare strict-types directive
235
        $commentEnd = strpos($fileContent, ' */') + 3;
236
        $fileContent = substr($fileContent, 0, $commentEnd) . "\n\ndeclare(strict_types=1);" . substr($fileContent, $commentEnd + 1);
237
238
        $this->dumpFile($possibleBaseFileName, $fileContent);
239
240
241
        $possibleFileName = $this->configuration->getPathFinder()->getPath($daonamespace.'\\'.$className)->getPathname();
242
243
        // Now, let's generate the "editable" class
244
        if (!file_exists($possibleFileName)) {
245
            $str = "<?php
246
/*
247
 * This file has been automatically generated by TDBM.
248
 * You can edit this file as it will not be overwritten.
249
 */
250
251
declare(strict_types=1);
252
253
namespace {$daonamespace};
254
255
use {$daonamespace}\\Generated\\{$baseClassName};
256
257
/**
258
 * The $className class will maintain the persistence of $beanClassWithoutNameSpace class into the $tableName table.
259
 */
260
class $className extends $baseClassName
261
{
262
}
263
";
264
            $this->dumpFile($possibleFileName, $str);
265
        }
266
    }
267
268
    /**
269
     * Generates the factory bean.
270
     *
271
     * @param Table[] $tableList
272
     * @throws TDBMException
273
     */
274
    private function generateFactory(array $tableList) : void
275
    {
276
        $daoNamespace = $this->configuration->getDaoNamespace();
277
        $daoFactoryClassName = $this->namingStrategy->getDaoFactoryClassName();
278
279
        // For each table, let's write a property.
280
281
        $str = "<?php
282
declare(strict_types=1);
283
284
/*
285
 * This file has been automatically generated by TDBM.
286
 * DO NOT edit this file, as it might be overwritten.
287
 */
288
289
namespace {$daoNamespace}\\Generated;
290
291
";
292
        foreach ($tableList as $table) {
293
            $tableName = $table->getName();
294
            $daoClassName = $this->namingStrategy->getDaoClassName($tableName);
295
            $str .= "use {$daoNamespace}\\".$daoClassName.";\n";
296
        }
297
298
        $str .= "
299
/**
300
 * The $daoFactoryClassName provides an easy access to all DAOs generated by TDBM.
301
 *
302
 */
303
class $daoFactoryClassName
304
{
305
";
306
307
        foreach ($tableList as $table) {
308
            $tableName = $table->getName();
309
            $daoClassName = $this->namingStrategy->getDaoClassName($tableName);
310
            $daoInstanceName = self::toVariableName($daoClassName);
311
312
            $str .= '    /**
313
     * @var '.$daoClassName.'
314
     */
315
    private $'.$daoInstanceName.';
316
317
    /**
318
     * Returns an instance of the '.$daoClassName.' class.
319
     *
320
     * @return '.$daoClassName.'
321
     */
322
    public function get'.$daoClassName.'() : '.$daoClassName.'
323
    {
324
        return $this->'.$daoInstanceName.';
325
    }
326
327
    /**
328
     * Sets the instance of the '.$daoClassName.' class that will be returned by the factory getter.
329
     *
330
     * @param '.$daoClassName.' $'.$daoInstanceName.'
331
     */
332
    public function set'.$daoClassName.'('.$daoClassName.' $'.$daoInstanceName.') : void
333
    {
334
        $this->'.$daoInstanceName.' = $'.$daoInstanceName.';
335
    }';
336
        }
337
338
        $str .= '
339
}
340
';
341
342
        $possibleFileName = $this->configuration->getPathFinder()->getPath($daoNamespace.'\\Generated\\'.$daoFactoryClassName)->getPathname();
343
344
        $this->dumpFile($possibleFileName, $str);
345
    }
346
347
    /**
348
     * Transforms a string to camelCase (except the first letter will be uppercase too).
349
     * Underscores and spaces are removed and the first letter after the underscore is uppercased.
350
     * Quoting is removed if present.
351
     *
352
     * @param string $str
353
     *
354
     * @return string
355
     */
356
    public static function toCamelCase(string $str) : string
357
    {
358
        $str = str_replace(array('`', '"', '[', ']'), '', $str);
359
360
        $str = strtoupper(substr($str, 0, 1)).substr($str, 1);
361
        while (true) {
362
            $pos = strpos($str, '_');
363
            if ($pos === false) {
364
                $pos = strpos($str, ' ');
365
                if ($pos === false) {
366
                    break;
367
                }
368
            }
369
370
            $before = substr($str, 0, $pos);
371
            $after = substr($str, $pos + 1);
372
            $str = $before.strtoupper(substr($after, 0, 1)).substr($after, 1);
373
        }
374
375
        return $str;
376
    }
377
378
    /**
379
     * Tries to put string to the singular form (if it is plural).
380
     * We assume the table names are in english.
381
     *
382
     * @param string $str
383
     *
384
     * @return string
385
     */
386
    public static function toSingular(string $str): string
387
    {
388
        return Inflector::singularize($str);
389
    }
390
391
    /**
392
     * Put the first letter of the string in lower case.
393
     * Very useful to transform a class name into a variable name.
394
     *
395
     * @param string $str
396
     *
397
     * @return string
398
     */
399
    public static function toVariableName(string $str): string
400
    {
401
        return strtolower(substr($str, 0, 1)).substr($str, 1);
402
    }
403
404
    /**
405
     * Ensures the file passed in parameter can be written in its directory.
406
     *
407
     * @param string $fileName
408
     *
409
     * @throws TDBMException
410
     */
411
    private function ensureDirectoryExist(string $fileName): void
412
    {
413
        $dirName = dirname($fileName);
414
        if (!file_exists($dirName)) {
415
            $old = umask(0);
416
            $result = mkdir($dirName, 0775, true);
417
            umask($old);
418
            if ($result === false) {
419
                throw new TDBMException("Unable to create directory: '".$dirName."'.");
420
            }
421
        }
422
    }
423
424
    private function dumpFile(string $fileName, string $content) : void
425
    {
426
        $this->ensureDirectoryExist($fileName);
427
        $fileSystem = new Filesystem();
428
        $fileSystem->dumpFile($fileName, $content);
429
        @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

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