Passed
Pull Request — 5.1 (#230)
by
unknown
02:54
created

getBeanPropertyDescriptor()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 11
rs 9.2222
cc 6
nc 4
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[]
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
        $setter = new MethodGenerator($setterName);
181
        $setter->setDocBlock(new DocBlockGenerator('The setter for the ' . $referencedBeanName . ' object bound to this object via the ' . implode(' and ', $this->foreignKey->getUnquotedLocalColumns()) . ' column.'));
182
183
        $setter->setParameter(new ParameterGenerator('object', ($isNullable ? '?' : '') . $this->beanNamespace . '\\' . $referencedBeanName));
184
185
        $setter->setReturnType('void');
186
187
        $setter->setBody('$this->setRef(' . var_export($tdbmFk->getCacheKey(), true) . ', $object, ' . var_export($tableName, true) . ');');
188
189
        if ($this->isSetterProtected()) {
190
            $setter->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED);
191
        }
192
193
        return [$getter, $setter];
194
    }
195
196
    /**
197
     * Returns the part of code useful when doing json serialization.
198
     *
199
     * @return string
200
     */
201
    public function getJsonSerializeCode(): string
202
    {
203
        if ($this->findAnnotation(Annotation\JsonIgnore::class)) {
204
            return '';
205
        }
206
207
        if ($this->isGetterProtected()) {
208
            return '';
209
        }
210
211
        if ($this->findAnnotation(Annotation\JsonCollection::class)) {
212
            if ($this->findAnnotation(Annotation\JsonInclude::class) ||
213
                $this->findAnnotation(Annotation\JsonRecursive::class)) {
214
                return '';
215
            }
216
            $isIncluded = false;
217
            $format = 'jsonSerialize(true)';
218
        } else {
219
            $isIncluded = $this->findAnnotation(Annotation\JsonInclude::class) !== null;
220
            /** @var Annotation\JsonFormat|null $jsonFormat */
221
            $jsonFormat = $this->findAnnotation(Annotation\JsonFormat::class);
222
            if ($jsonFormat !== null) {
223
                $method = $jsonFormat->method ?? 'get' . ucfirst($jsonFormat->property);
224
                $format = "$method()";
225
            } else {
226
                $stopRecursion = $this->findAnnotation(Annotation\JsonRecursive::class) ? '' : 'true';
227
                $format = "jsonSerialize($stopRecursion)";
228
            }
229
        }
230
        /** @var Annotation\JsonKey|null $jsonKey */
231
        $jsonKey = $this->findAnnotation(Annotation\JsonKey::class);
232
        $index = $jsonKey ? $jsonKey->key : $this->namingStrategy->getJsonProperty($this);
233
        $getter = $this->getGetterName();
234
        if (!$this->isCompulsory()) {
235
            $recursiveCode = "\$array['$index'] = (\$object = \$this->$getter()) ? \$object->$format : null;";
236
            $lazyCode = "\$array['$index'] = (\$object = \$this->$getter()) ? {$this->getLazySerializeCode('$object')} : null;";
237
        } else {
238
            $recursiveCode = "\$array['$index'] = \$this->$getter()->$format;";
239
            $lazyCode = "\$array['$index'] = {$this->getLazySerializeCode("\$this->$getter()")};";
240
        }
241
242
        if ($isIncluded) {
243
            $code = $recursiveCode;
244
        } else {
245
            $code = <<<PHP
246
if (\$stopRecursion) {
247
    $lazyCode
248
} else {
249
    $recursiveCode
250
}
251
PHP;
252
        }
253
        return $code;
254
    }
255
256
    private function getLazySerializeCode(string $propertyAccess): string
257
    {
258
        $rows = [];
259
        foreach ($this->getForeignKey()->getUnquotedForeignColumns() as $column) {
260
            $descriptor = $this->getBeanPropertyDescriptor($column);
261
            if ($descriptor instanceof InheritanceReferencePropertyDescriptor) {
262
                $descriptor = $descriptor->getNonScalarReferencedPropertyDescriptor();
263
            }
264
            $indexName = ltrim($descriptor->getVariableName(), '$');
265
            $columnGetterName = $descriptor->getGetterName();
266
            if ($descriptor instanceof ObjectBeanPropertyDescriptor) {
267
                $varName = '$o' . lcfirst($indexName);
268
                $lazySerializeCode = $descriptor->getLazySerializeCode($varName);
269
                $rows[] = "'$indexName' => ($varName = $propertyAccess->$columnGetterName()) ? $lazySerializeCode : null";
270
            } elseif ($descriptor instanceof ScalarBeanPropertyDescriptor) {
271
                $rows[] = "'$indexName' => $propertyAccess->$columnGetterName()";
272
            } else {
273
                throw new TDBMException('PropertyDescriptor of class `' . get_class($descriptor) . '` cannot be serialized.');
274
            }
275
        }
276
        return '[' . implode(', ', $rows) . ']';
277
    }
278
279
    private function getBeanPropertyDescriptor(string $column): AbstractBeanPropertyDescriptor
280
    {
281
        foreach ($this->foreignBeanDescriptor->getBeanPropertyDescriptors() as $descriptor) {
282
            if ($descriptor instanceof ObjectBeanPropertyDescriptor && in_array($column, $descriptor->getForeignKey()->getLocalColumns(), true)) {
283
                return $descriptor;
284
            }
285
            if ($descriptor instanceof ScalarBeanPropertyDescriptor && $descriptor->getColumnName() === $column) {
286
                return $descriptor;
287
            }
288
        }
289
        throw new TDBMException('PropertyDescriptor for `'.$this->table->getName().'`.`' . $column . '` not found in `' . $this->foreignBeanDescriptor->getTable()->getName() . '`');
290
    }
291
292
    /**
293
     * The code to past in the __clone method.
294
     * @return null|string
295
     */
296
    public function getCloneRule(): ?string
297
    {
298
        return null;
299
    }
300
301
    /**
302
     * Tells if this property is a type-hintable in PHP (resource isn't for example)
303
     *
304
     * @return bool
305
     */
306
    public function isTypeHintable(): bool
307
    {
308
        return true;
309
    }
310
311
    private function isGetterProtected(): bool
312
    {
313
        return $this->findAnnotation(Annotation\ProtectedGetter::class) !== null;
314
    }
315
316
    private function isSetterProtected(): bool
317
    {
318
        return $this->findAnnotation(Annotation\ProtectedSetter::class) !== null;
319
    }
320
321
    /**
322
     * @param string $type
323
     * @return null|object
324
     */
325
    private function findAnnotation(string $type)
326
    {
327
        foreach ($this->getAnnotations() as $annotations) {
328
            $annotation = $annotations->findAnnotation($type);
329
            if ($annotation !== null) {
330
                return $annotation;
331
            }
332
        }
333
        return null;
334
    }
335
}
336