ObjectBeanPropertyDescriptor::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 17
rs 10
cc 1
nc 1
nop 7
1
<?php
2
3
declare(strict_types=1);
4
5
namespace TheCodingMachine\TDBM\Utils;
6
7
use Doctrine\DBAL\Schema\Table;
8
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
9
use TheCodingMachine\TDBM\Schema\ForeignKey;
10
use TheCodingMachine\TDBM\TDBMException;
11
use TheCodingMachine\TDBM\Utils\Annotation\AnnotationParser;
12
use TheCodingMachine\TDBM\Utils\Annotation;
13
use Laminas\Code\Generator\AbstractMemberGenerator;
14
use Laminas\Code\Generator\DocBlockGenerator;
15
use Laminas\Code\Generator\MethodGenerator;
16
use Laminas\Code\Generator\ParameterGenerator;
17
18
/**
19
 * This class represent a property in a bean that points to another table.
20
 */
21
class ObjectBeanPropertyDescriptor extends AbstractBeanPropertyDescriptor
22
{
23
    use ForeignKeyAnalyzerTrait;
24
25
    /**
26
     * @var ForeignKeyConstraint
27
     */
28
    private $foreignKey;
29
    /**
30
     * @var string
31
     */
32
    private $beanNamespace;
33
    /**
34
     * @var BeanDescriptor
35
     */
36
    private $foreignBeanDescriptor;
37
    /**
38
     * @var string
39
     */
40
    private $resultIteratorNamespace;
41
42
    /**
43
     * ObjectBeanPropertyDescriptor constructor.
44
     * @param Table $table
45
     * @param ForeignKeyConstraint $foreignKey
46
     * @param NamingStrategyInterface $namingStrategy
47
     * @param string $beanNamespace
48
     * @param AnnotationParser $annotationParser
49
     * @param BeanDescriptor $foreignBeanDescriptor The BeanDescriptor of FK foreign table
50
     */
51
    public function __construct(
52
        Table $table,
53
        ForeignKeyConstraint $foreignKey,
54
        NamingStrategyInterface $namingStrategy,
55
        string $beanNamespace,
56
        AnnotationParser $annotationParser,
57
        BeanDescriptor $foreignBeanDescriptor,
58
        string $resultIteratorNamespace
59
    ) {
60
        parent::__construct($table, $namingStrategy);
61
        $this->foreignKey = $foreignKey;
62
        $this->beanNamespace = $beanNamespace;
63
        $this->annotationParser = $annotationParser;
64
        $this->table = $table;
65
        $this->namingStrategy = $namingStrategy;
66
        $this->foreignBeanDescriptor = $foreignBeanDescriptor;
67
        $this->resultIteratorNamespace = $resultIteratorNamespace;
68
    }
69
70
    /**
71
     * Returns the foreignkey the column is part of, if any. null otherwise.
72
     *
73
     * @return ForeignKeyConstraint
74
     */
75
    public function getForeignKey(): ForeignKeyConstraint
76
    {
77
        return $this->foreignKey;
78
    }
79
80
    /**
81
     * Returns the name of the class linked to this property or null if this is not a foreign key.
82
     *
83
     * @return string
84
     */
85
    public function getClassName(): string
86
    {
87
        return $this->namingStrategy->getBeanClassName($this->foreignKey->getForeignTableName());
88
    }
89
90
    /**
91
     * Returns the PHP type for the property (it can be a scalar like int, bool, or class names, like \DateTimeInterface, App\Bean\User....)
92
     *
93
     * @return string
94
     */
95
    public function getPhpType(): string
96
    {
97
        return '\\' . $this->beanNamespace . '\\' . $this->getClassName();
98
    }
99
100
    /**
101
     * Returns true if the property is compulsory (and therefore should be fetched in the constructor).
102
     *
103
     * @return bool
104
     */
105
    public function isCompulsory(): bool
106
    {
107
        // Are all columns nullable?
108
        foreach ($this->getLocalColumns() as $column) {
109
            if ($column->getNotnull()) {
110
                return true;
111
            }
112
        }
113
114
        return false;
115
    }
116
117
    /**
118
     * Returns true if the property has a default value.
119
     *
120
     * @return bool
121
     */
122
    public function hasDefault(): bool
123
    {
124
        return false;
125
    }
126
127
    /**
128
     * Returns the code that assigns a value to its default value.
129
     *
130
     * @return string
131
     *
132
     * @throws TDBMException
133
     */
134
    public function assignToDefaultCode(): string
135
    {
136
        throw new TDBMException('Foreign key based properties cannot be assigned a default value.');
137
    }
138
139
    /**
140
     * Returns true if the property is the primary key.
141
     *
142
     * @return bool
143
     */
144
    public function isPrimaryKey(): bool
145
    {
146
        $fkColumns = $this->foreignKey->getUnquotedLocalColumns();
147
        sort($fkColumns);
148
149
        $pkColumns = TDBMDaoGenerator::getPrimaryKeyColumnsOrFail($this->table);
150
        sort($pkColumns);
151
152
        return $fkColumns == $pkColumns;
153
    }
154
155
    /**
156
     * Returns the PHP code for getters and setters.
157
     *
158
     * @return (MethodGenerator|null)[]
159
     */
160
    public function getGetterSetterCode(): array
161
    {
162
        $tableName = $this->table->getName();
163
        $foreignTableName = $this->foreignKey->getForeignTableName();
164
        $getterName = $this->getGetterName();
165
        $setterName = $this->getSetterName();
166
        $isNullable = !$this->isCompulsory();
167
168
        $referencedBeanName = $this->namingStrategy->getBeanClassName($foreignTableName);
169
        $referencedResultIteratorName = $this->namingStrategy->getResultIteratorClassName($foreignTableName);
170
171
        $getter = new MethodGenerator($getterName);
172
        $getter->setDocBlock(new DocBlockGenerator('Returns the ' . $referencedBeanName . ' object bound to this object via the ' . implode(' and ', $this->foreignKey->getUnquotedLocalColumns()) . ' column.'));
173
174
        $getter->setReturnType(($isNullable ? '?' : '') . $this->beanNamespace . '\\' . $referencedBeanName);
175
        $tdbmFk = ForeignKey::createFromFk($this->foreignKey);
176
177
        $getter->setBody(sprintf(
178
            'return $this->getRef(%s, %s, %s, %s);',
179
            var_export($tdbmFk->getCacheKey(), true),
180
            var_export($tableName, true),
181
            '\\' . $this->beanNamespace . '\\' . $referencedBeanName . '::class',
182
            '\\' . $this->resultIteratorNamespace . '\\' . $referencedResultIteratorName . '::class'
183
        ));
184
185
        if ($this->isGetterProtected()) {
186
            $getter->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED);
187
        }
188
189
        if (!$this->isReadOnly()) {
190
            $setter = new MethodGenerator($setterName);
191
            $setter->setDocBlock(new DocBlockGenerator('The setter for the ' . $referencedBeanName . ' object bound to this object via the ' . implode(' and ', $this->foreignKey->getUnquotedLocalColumns()) . ' column.'));
192
193
            $setter->setParameter(new ParameterGenerator('object', ($isNullable ? '?' : '') . $this->beanNamespace . '\\' . $referencedBeanName));
194
195
            $setter->setReturnType('void');
196
197
            $setter->setBody(sprintf(
198
                '$this->setRef(%s, %s, %s, %s, %s);',
199
                var_export($tdbmFk->getCacheKey(), true),
200
                '$object',
201
                var_export($tableName, true),
202
                '\\' . $this->beanNamespace . '\\' . $referencedBeanName . '::class',
203
                '\\' . $this->resultIteratorNamespace . '\\' . $referencedResultIteratorName . '::class'
204
            ));
205
206
            if ($this->isSetterProtected()) {
207
                $setter->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED);
208
            }
209
        } else {
210
            $setter = null;
211
        }
212
213
        return [$getter, $setter];
214
    }
215
216
    /**
217
     * Returns the part of code useful when doing json serialization.
218
     *
219
     * @return string
220
     */
221
    public function getJsonSerializeCode(): string
222
    {
223
        if ($this->findAnnotation(Annotation\JsonIgnore::class)) {
224
            return '';
225
        }
226
227
        if ($this->isGetterProtected()) {
228
            return '';
229
        }
230
231
        if ($this->findAnnotation(Annotation\JsonCollection::class)) {
232
            if ($this->findAnnotation(Annotation\JsonInclude::class) ||
233
                $this->findAnnotation(Annotation\JsonRecursive::class)) {
234
                return '';
235
            }
236
            $isIncluded = false;
237
            $format = 'jsonSerialize(true)';
238
        } else {
239
            $isIncluded = $this->findAnnotation(Annotation\JsonInclude::class) !== null;
240
            /** @var Annotation\JsonFormat|null $jsonFormat */
241
            $jsonFormat = $this->findAnnotation(Annotation\JsonFormat::class);
242
            if ($jsonFormat !== null) {
243
                $method = $jsonFormat->method ?? 'get' . ucfirst($jsonFormat->property);
244
                $format = "$method()";
245
            } else {
246
                $stopRecursion = $this->findAnnotation(Annotation\JsonRecursive::class) ? '' : 'true';
247
                $format = "jsonSerialize($stopRecursion)";
248
            }
249
        }
250
        /** @var Annotation\JsonKey|null $jsonKey */
251
        $jsonKey = $this->findAnnotation(Annotation\JsonKey::class);
252
        $index = $jsonKey ? $jsonKey->key : $this->namingStrategy->getJsonProperty($this);
253
        $getter = $this->getGetterName();
254
        if (!$this->isCompulsory()) {
255
            $recursiveCode = "\$array['$index'] = (\$object = \$this->$getter()) ? \$object->$format : null;";
256
            $lazyCode = "\$array['$index'] = (\$object = \$this->$getter()) ? {$this->getLazySerializeCode('$object')} : null;";
257
        } else {
258
            $recursiveCode = "\$array['$index'] = \$this->$getter()->$format;";
259
            $lazyCode = "\$array['$index'] = {$this->getLazySerializeCode("\$this->$getter()")};";
260
        }
261
262
        if ($isIncluded) {
263
            $code = $recursiveCode;
264
        } else {
265
            $code = <<<PHP
266
if (\$stopRecursion) {
267
    $lazyCode
268
} else {
269
    $recursiveCode
270
}
271
PHP;
272
        }
273
        return $code;
274
    }
275
276
    private function getLazySerializeCode(string $propertyAccess): string
277
    {
278
        $rows = [];
279
        foreach ($this->getForeignKey()->getUnquotedForeignColumns() as $column) {
280
            $descriptor = $this->getBeanPropertyDescriptor($column);
281
            $shouldFlatten = false;
282
            if ($descriptor instanceof InheritanceReferencePropertyDescriptor) {
283
                $descriptor = $descriptor->getNonScalarReferencedPropertyDescriptor();
284
                $shouldFlatten = true;
285
            }
286
287
            $indexName = ltrim($descriptor->getVariableName(), '$');
288
            $columnGetterName = $descriptor->getGetterName();
289
            if ($descriptor instanceof ObjectBeanPropertyDescriptor) {
290
                if ($shouldFlatten) {
291
                    $rows[] = trim($descriptor->getLazySerializeCode($propertyAccess), '[]');
292
                } else {
293
                    $lazySerializeCode = $descriptor->getLazySerializeCode("$propertyAccess->$columnGetterName()");
294
                    $rows[] = "'$indexName' => $lazySerializeCode";
295
                }
296
            } elseif ($descriptor instanceof ScalarBeanPropertyDescriptor) {
297
                $rows[] = "'$indexName' => $propertyAccess->$columnGetterName()";
298
            } else {
299
                throw new TDBMException('PropertyDescriptor of class `' . get_class($descriptor) . '` cannot be serialized.');
300
            }
301
        }
302
        return '[' . implode(', ', $rows) . ']';
303
    }
304
305
    private function getBeanPropertyDescriptor(string $column): AbstractBeanPropertyDescriptor
306
    {
307
        foreach ($this->foreignBeanDescriptor->getBeanPropertyDescriptors() as $descriptor) {
308
            if ($descriptor instanceof ScalarBeanPropertyDescriptor && $descriptor->getColumnName() === $column) {
309
                return $descriptor;
310
            }
311
            if ($descriptor instanceof ObjectBeanPropertyDescriptor && in_array($column, $descriptor->getForeignKey()->getUnquotedLocalColumns(), true)) {
312
                return $descriptor;
313
            }
314
        }
315
        throw new TDBMException('PropertyDescriptor for `'.$this->table->getName().'`.`' . $column . '` not found in `' . $this->foreignBeanDescriptor->getTable()->getName() . '`');
316
    }
317
318
    /**
319
     * The code to past in the __clone method.
320
     * @return null|string
321
     */
322
    public function getCloneRule(): ?string
323
    {
324
        return null;
325
    }
326
327
    /**
328
     * Tells if this property is a type-hintable in PHP (resource isn't for example)
329
     *
330
     * @return bool
331
     */
332
    public function isTypeHintable(): bool
333
    {
334
        return true;
335
    }
336
337
    private function isGetterProtected(): bool
338
    {
339
        return $this->findAnnotation(Annotation\ProtectedGetter::class) !== null;
340
    }
341
342
    private function isSetterProtected(): bool
343
    {
344
        return $this->findAnnotation(Annotation\ProtectedSetter::class) !== null;
345
    }
346
347
    public function isReadOnly(): bool
348
    {
349
        return $this->findAnnotation(Annotation\ReadOnlyColumn::class) !== null;
350
    }
351
352
    /**
353
     * @param string $type
354
     * @return null|object
355
     */
356
    private function findAnnotation(string $type)
357
    {
358
        foreach ($this->getAnnotations() as $annotations) {
359
            $annotation = $annotations->findAnnotation($type);
360
            if ($annotation !== null) {
361
                return $annotation;
362
            }
363
        }
364
        return null;
365
    }
366
}
367