Passed
Push — master ( fed116...a49d62 )
by
unknown
03:54
created

getLazySerializeCode()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 27
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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