Completed
Push — master ( f50fbb...681f17 )
by David
20s queued 12s
created

getGetterSetterCode()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 45
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 23
c 1
b 0
f 0
dl 0
loc 45
rs 8.9297
cc 6
nc 6
nop 0
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
            if ($descriptor instanceof InheritanceReferencePropertyDescriptor) {
266
                $descriptor = $descriptor->getNonScalarReferencedPropertyDescriptor();
267
            }
268
            if ($descriptor instanceof ObjectBeanPropertyDescriptor) {
269
                $rows[] = trim($descriptor->getLazySerializeCode($propertyAccess), '[]');
270
            } elseif ($descriptor instanceof ScalarBeanPropertyDescriptor) {
271
                $indexName = ltrim($descriptor->getVariableName(), '$');
272
                $columnGetterName = $descriptor->getGetterName();
273
                $rows[] = "'$indexName' => $propertyAccess->$columnGetterName()";
274
            } else {
275
                throw new TDBMException('PropertyDescriptor of class `' . get_class($descriptor) . '` cannot be serialized.');
276
            }
277
        }
278
        return '[' . implode(', ', $rows) . ']';
279
    }
280
281
    private function getBeanPropertyDescriptor(string $column): AbstractBeanPropertyDescriptor
282
    {
283
        foreach ($this->foreignBeanDescriptor->getBeanPropertyDescriptors() as $descriptor) {
284
            if ($descriptor instanceof ScalarBeanPropertyDescriptor && $descriptor->getColumnName() === $column) {
285
                return $descriptor;
286
            }
287
        }
288
        throw new TDBMException('PropertyDescriptor for `'.$this->table->getName().'`.`' . $column . '` not found in `' . $this->foreignBeanDescriptor->getTable()->getName() . '`');
289
    }
290
291
    /**
292
     * The code to past in the __clone method.
293
     * @return null|string
294
     */
295
    public function getCloneRule(): ?string
296
    {
297
        return null;
298
    }
299
300
    /**
301
     * Tells if this property is a type-hintable in PHP (resource isn't for example)
302
     *
303
     * @return bool
304
     */
305
    public function isTypeHintable(): bool
306
    {
307
        return true;
308
    }
309
310
    private function isGetterProtected(): bool
311
    {
312
        return $this->findAnnotation(Annotation\ProtectedGetter::class) !== null;
313
    }
314
315
    private function isSetterProtected(): bool
316
    {
317
        return $this->findAnnotation(Annotation\ProtectedSetter::class) !== null;
318
    }
319
320
    public function isReadOnly(): bool
321
    {
322
        return $this->findAnnotation(Annotation\ReadOnly::class) !== null;
323
    }
324
325
    /**
326
     * @param string $type
327
     * @return null|object
328
     */
329
    private function findAnnotation(string $type)
330
    {
331
        foreach ($this->getAnnotations() as $annotations) {
332
            $annotation = $annotations->findAnnotation($type);
333
            if ($annotation !== null) {
334
                return $annotation;
335
            }
336
        }
337
        return null;
338
    }
339
}
340