Completed
Pull Request — master (#125)
by David
04:02
created

ScalarBeanPropertyDescriptor::isGetterProtected()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
declare(strict_types=1);
3
4
namespace TheCodingMachine\TDBM\Utils;
5
6
use Doctrine\DBAL\Platforms\MySQL57Platform;
7
use Doctrine\DBAL\Schema\Column;
8
use Doctrine\DBAL\Schema\Table;
9
use Doctrine\DBAL\Types\Type;
10
use TheCodingMachine\TDBM\TDBMException;
11
use TheCodingMachine\TDBM\Utils\Annotation\AnnotationParser;
12
use TheCodingMachine\TDBM\Utils\Annotation\Annotations;
13
use \TheCodingMachine\TDBM\Utils\Annotation;
14
use Zend\Code\Generator\AbstractMemberGenerator;
15
use Zend\Code\Generator\DocBlock\Tag\ParamTag;
16
use Zend\Code\Generator\DocBlock\Tag\ReturnTag;
17
use Zend\Code\Generator\MethodGenerator;
18
use Zend\Code\Generator\ParameterGenerator;
19
20
/**
21
 * This class represent a property in a bean (a property has a getter, a setter, etc...).
22
 */
23
class ScalarBeanPropertyDescriptor extends AbstractBeanPropertyDescriptor
24
{
25
    /**
26
     * @var Column
27
     */
28
    private $column;
29
30
    /**
31
     * @var Annotations
32
     */
33
    private $annotations;
34
35
    /**
36
     * @var AnnotationParser
37
     */
38
    private $annotationParser;
39
40
    /**
41
     * ScalarBeanPropertyDescriptor constructor.
42
     * @param Table $table
43
     * @param Column $column
44
     * @param NamingStrategyInterface $namingStrategy
45
     */
46
    public function __construct(Table $table, Column $column, NamingStrategyInterface $namingStrategy, AnnotationParser $annotationParser)
47
    {
48
        parent::__construct($table, $namingStrategy);
49
        $this->table = $table;
50
        $this->column = $column;
51
        $this->annotationParser = $annotationParser;
52
    }
53
54
    /**
55
     * Returns the name of the class linked to this property or null if this is not a foreign key.
56
     *
57
     * @return null|string
58
     */
59
    public function getClassName(): ?string
60
    {
61
        return null;
62
    }
63
64
    /**
65
     * Returns the PHP type for the property (it can be a scalar like int, bool, or class names, like \DateTimeInterface, App\Bean\User....)
66
     *
67
     * @return string
68
     */
69
    public function getPhpType(): string
70
    {
71
        $type = $this->column->getType();
72
        return TDBMDaoGenerator::dbalTypeToPhpType($type);
73
    }
74
75
    /**
76
     * Returns true if the property is compulsory (and therefore should be fetched in the constructor).
77
     *
78
     * @return bool
79
     */
80
    public function isCompulsory(): bool
81
    {
82
        return $this->column->getNotnull() && !$this->isAutoincrement() && $this->column->getDefault() === null && !$this->hasUuidAnnotation();
83
    }
84
85
    private function isAutoincrement() : bool
86
    {
87
        return $this->column->getAutoincrement() || $this->getAutoincrementAnnotation() !== null;
88
    }
89
90
    private function hasUuidAnnotation(): bool
91
    {
92
        return $this->getUuidAnnotation() !== null;
93
    }
94
95
    private function getUuidAnnotation(): ?Annotation\UUID
96
    {
97
        /** @var Annotation\UUID $annotation */
98
        $annotation = $this->getAnnotations()->findAnnotation(Annotation\UUID::class);
99
        return $annotation;
100
    }
101
102
    private function getAutoincrementAnnotation(): ?Annotation\Autoincrement
103
    {
104
        /** @var Annotation\Autoincrement $annotation */
105
        $annotation = $this->getAnnotations()->findAnnotation(Annotation\Autoincrement::class);
106
        return $annotation;
107
    }
108
109
    private function getAnnotations(): Annotations
110
    {
111
        if ($this->annotations === null) {
112
            $this->annotations = $this->annotationParser->getColumnAnnotations($this->column, $this->table);
113
        }
114
        return $this->annotations;
115
    }
116
117
    /**
118
     * Returns true if the property has a default value (or if the @UUID annotation is set for the column)
119
     *
120
     * @return bool
121
     */
122
    public function hasDefault(): bool
123
    {
124
        // MariaDB 10.3 issue: it returns "NULL" (the string) instead of *null*
125
        return ($this->column->getDefault() !== null && $this->column->getDefault() !== 'NULL') || $this->hasUuidAnnotation();
126
    }
127
128
    /**
129
     * Returns the code that assigns a value to its default value.
130
     *
131
     * @return string
132
     */
133
    public function assignToDefaultCode(): string
134
    {
135
        $str = '$this->%s(%s);';
136
137
        $uuidAnnotation = $this->getUuidAnnotation();
138
        if ($uuidAnnotation !== null) {
139
            $defaultCode = $this->getUuidCode($uuidAnnotation);
140
        } else {
141
            $default = $this->column->getDefault();
142
            $type = $this->column->getType();
143
144
            if (in_array($type->getName(), [
145
                'datetime',
146
                'datetime_immutable',
147
                'datetimetz',
148
                'datetimetz_immutable',
149
                'date',
150
                'date_immutable',
151
                'time',
152
                'time_immutable',
153
            ], true)) {
154
                if ($default !== null && in_array(strtoupper($default), ['CURRENT_TIMESTAMP' /* MySQL */, 'NOW()' /* PostgreSQL */, 'SYSDATE' /* Oracle */ , 'CURRENT_TIMESTAMP()' /* MariaDB 10.3 */], true)) {
155
                    $defaultCode = 'new \DateTimeImmutable()';
156
                } else {
157
                    throw new TDBMException('Unable to set default value for date in "'.$this->table->getName().'.'.$this->column->getName().'". Database passed this default value: "'.$default.'"');
158
                }
159
            } else {
160
                $defaultValue = $type->convertToPHPValue($this->column->getDefault(), new MySQL57Platform());
161
                $defaultCode = var_export($defaultValue, true);
162
            }
163
        }
164
165
        return sprintf($str, $this->getSetterName(), $defaultCode);
166
    }
167
168
    private function getUuidCode(Annotation\UUID $uuidAnnotation): string
169
    {
170
        $comment = $uuidAnnotation->value;
171
        switch ($comment) {
172
            case '':
173
            case 'v1':
174
                return 'Uuid::uuid1()->toString()';
175
            case 'v4':
176
                return 'Uuid::uuid4()->toString()';
177
            default:
178
                throw new TDBMException('@UUID annotation accepts either "v1" or "v4" parameter. Unexpected parameter: ' . $comment);
179
        }
180
    }
181
182
    /**
183
     * Returns true if the property is the primary key.
184
     *
185
     * @return bool
186
     */
187
    public function isPrimaryKey(): bool
188
    {
189
        $primaryKey = $this->table->getPrimaryKey();
190
        if ($primaryKey === null) {
191
            return false;
192
        }
193
        return in_array($this->column->getName(), $primaryKey->getUnquotedColumns());
194
    }
195
196
    /**
197
     * Returns the PHP code for getters and setters.
198
     *
199
     * @return MethodGenerator[]
200
     */
201
    public function getGetterSetterCode(): array
202
    {
203
        $normalizedType = $this->getPhpType();
204
205
        $columnGetterName = $this->getGetterName();
206
        $columnSetterName = $this->getSetterName();
207
208
        // A column type can be forced if it is not nullable and not auto-incrementable (for auto-increment columns, we can get "null" as long as the bean is not saved).
209
        $isNullable = !$this->column->getNotnull() || $this->isAutoincrement();
210
211
        $resourceTypeCheck = '';
212
        if ($normalizedType === 'resource') {
213
            $resourceTypeCheck .= <<<EOF
214
215
if (!\is_resource($%s)) {
216
    throw \TheCodingMachine\TDBM\TDBMInvalidArgumentException::badType('resource', $%s, __METHOD__);
217
}
218
EOF;
219
            $resourceTypeCheck = sprintf($resourceTypeCheck, $this->column->getName(), $this->column->getName());
220
        }
221
222
223
        $paramType = null;
224
        if ($this->isTypeHintable()) {
225
            $paramType = ($isNullable?'?':'').$normalizedType;
226
        }
227
228
        $getter = new MethodGenerator($columnGetterName);
229
        $getter->setDocBlock(sprintf('The getter for the "%s" column.', $this->column->getName()));
230
231
        $types = [ $normalizedType ];
232
        if ($isNullable) {
233
            $types[] = 'null';
234
        }
235
        $getter->getDocBlock()->setTag(new ReturnTag($types));
236
237
        $getter->setReturnType($paramType);
238
239
        $getter->setBody(sprintf('return $this->get(%s, %s);',
240
            var_export($this->column->getName(), true),
241
            var_export($this->table->getName(), true)));
242
243
        if ($this->isGetterProtected()) {
244
            $getter->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED);
245
        }
246
247
        $setter = new MethodGenerator($columnSetterName);
248
        $setter->setDocBlock(sprintf('The setter for the "%s" column.', $this->column->getName()));
249
250
        $types = [ $normalizedType ];
251
        if ($isNullable) {
252
            $types[] = 'null';
253
        }
254
        $setter->getDocBlock()->setTag(new ParamTag($this->column->getName(), $types));
255
256
        $parameter = new ParameterGenerator($this->column->getName(), $paramType);
257
        $setter->setParameter($parameter);
258
        $setter->setReturnType('void');
259
260
        $setter->setBody(sprintf('%s
261
$this->set(%s, $%s, %s);',
262
            $resourceTypeCheck,
263
            var_export($this->column->getName(), true),
264
            $this->column->getName(),
265
            var_export($this->table->getName(), true)));
266
267
        if ($this->isSetterProtected()) {
268
            $setter->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED);
269
        }
270
271
        return [$getter, $setter];
272
    }
273
274
    /**
275
     * Returns the part of code useful when doing json serialization.
276
     *
277
     * @return string
278
     */
279
    public function getJsonSerializeCode(): string
280
    {
281
        $normalizedType = $this->getPhpType();
282
283
        if (!$this->canBeSerialized()) {
284
            return '';
285
        }
286
287
        // Do not export the property is the getter is protected.
288
        if ($this->isGetterProtected()) {
289
            return '';
290
        }
291
292
        if ($normalizedType == '\\DateTimeImmutable') {
293
            if ($this->column->getNotnull()) {
294
                return '$array['.var_export($this->namingStrategy->getJsonProperty($this), true).'] = $this->'.$this->getGetterName()."()->format('c');\n";
295
            } else {
296
                return '$array['.var_export($this->namingStrategy->getJsonProperty($this), true).'] = ($this->'.$this->getGetterName().'() === null) ? null : $this->'.$this->getGetterName()."()->format('c');\n";
297
            }
298
        } else {
299
            return '$array['.var_export($this->namingStrategy->getJsonProperty($this), true).'] = $this->'.$this->getGetterName()."();\n";
300
        }
301
    }
302
303
    /**
304
     * Returns the column name.
305
     *
306
     * @return string
307
     */
308
    public function getColumnName(): string
309
    {
310
        return $this->column->getName();
311
    }
312
313
    /**
314
     * The code to past in the __clone method.
315
     * @return null|string
316
     */
317
    public function getCloneRule(): ?string
318
    {
319
        $uuidAnnotation = $this->getUuidAnnotation();
320
        if ($uuidAnnotation !== null && $this->isPrimaryKey()) {
321
            return sprintf("\$this->%s(%s);\n", $this->getSetterName(), $this->getUuidCode($uuidAnnotation));
322
        }
323
        return null;
324
    }
325
326
    /**
327
     * tells is this type is suitable for Json Serialization
328
     *
329
     * @return bool
330
     */
331
    public function canBeSerialized() : bool
332
    {
333
        $type = $this->column->getType();
334
335
        $unserialisableTypes = [
336
            Type::BLOB,
337
            Type::BINARY
338
        ];
339
340
        return \in_array($type->getName(), $unserialisableTypes, true) === false;
341
    }
342
343
    /**
344
     * Tells if this property is a type-hintable in PHP (resource isn't for example)
345
     *
346
     * @return bool
347
     */
348
    public function isTypeHintable() : bool
349
    {
350
        $type = $this->getPhpType();
351
        $invalidScalarTypes = [
352
            'resource'
353
        ];
354
355
        return \in_array($type, $invalidScalarTypes, true) === false;
356
    }
357
358
    private function isGetterProtected(): bool
359
    {
360
        /** @var Annotation\ProtectedGetter $annotation */
361
        $annotation = $this->getAnnotations()->findAnnotation(Annotation\ProtectedGetter::class);
362
        return $annotation !== null;
363
    }
364
365
    private function isSetterProtected(): bool
366
    {
367
        /** @var Annotation\ProtectedSetter $annotation */
368
        $annotation = $this->getAnnotations()->findAnnotation(Annotation\ProtectedSetter::class);
369
        return $annotation !== null;
370
    }
371
}
372