Passed
Pull Request — master (#155)
by
unknown
02:30
created

ObjectBeanPropertyDescriptor::getForeignKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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