Passed
Push — 2.x ( 0b5227...cb81b7 )
by butschster
16:17
created

ProxyEntityFactory::defineClass()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 42
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6.288

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 25
nc 6
nop 2
dl 0
loc 42
ccs 16
cts 20
cp 0.8
crap 6.288
rs 8.8977
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\ORM\Mapper\Proxy;
6
7
use Closure;
8
use Cycle\ORM\Mapper\Proxy\Hydrator\ClassPropertiesExtractor;
9
use Cycle\ORM\Mapper\Proxy\Hydrator\ClosureHydrator;
10
use Cycle\ORM\Mapper\Proxy\Hydrator\PropertyMap;
11
use Cycle\ORM\RelationMap;
12
use Doctrine\Instantiator\Instantiator;
13
14
/**
15
 * @internal
16
 */
17
class ProxyEntityFactory
18
{
19
    /**
20
     * @var string[]
21
     * @psalm-var class-string[]
22
     */
23
    private array $classMap = [];
24
25
    /** @var PropertyMap[] */
26
    private array $classProperties = [];
27
28
    private Instantiator $instantiator;
29
    private Closure $initializer;
30
31 6086
    public function __construct(
32
        private ClosureHydrator $hydrator,
33
        private ClassPropertiesExtractor $propertiesExtractor
34
    ) {
35 6086
        $this->instantiator = new Instantiator();
36 6086
        $this->initializer = static function (object $entity, array $properties): void {
37 3680
            foreach ($properties as $name) {
38 3680
                unset($entity->$name);
39
            }
40
        };
41
    }
42
43
    /**
44
     * Creates an empty Entity.
45
     */
46 4432
    public function create(
47
        RelationMap $relMap,
48
        string $sourceClass,
49
    ): object {
50 4432
        $class = array_key_exists($sourceClass, $this->classMap)
51 2380
            ? $this->classMap[$sourceClass]
52 4432
            : $this->defineClass($relMap, $sourceClass);
53
54 4430
        $proxy = $this->instantiator->instantiate($class);
55 4430
        $proxy->__cycle_orm_rel_map = $relMap;
56 4430
        $scopes = $this->getEntityProperties($proxy, $relMap);
57 4430
        $proxy->__cycle_orm_relation_props = $scopes[ClassPropertiesExtractor::KEY_RELATIONS];
58
59
        // init
60 4430
        foreach ($scopes[ClassPropertiesExtractor::KEY_RELATIONS]->getProperties() as $scope => $properties) {
61 3680
            Closure::bind($this->initializer, null, $scope === '' ? $class : $scope)($proxy, $properties);
62
        }
63
64 4430
        return $proxy;
65
    }
66
67
    /**
68
     * Sets an Entity's column values from data.
69
     *
70
     * @return object Entity with hydrated data
71
     */
72 4804
    public function upgrade(
73
        RelationMap $relMap,
74
        object $entity,
75
        array $data
76
    ): object {
77 4804
        $properties = $this->getEntityProperties($entity, $relMap);
78
79
        // new set of data and relations always overwrite entity state
80 4804
        return $this->hydrator->hydrate(
81
            $relMap,
82
            $properties,
83
            $entity,
84
            $data
85
        );
86
    }
87
88
    /**
89
     * Get an entity relation column values as array, except primitive columns.
90
     *
91
     * @return array array<string, mixed>
92
     */
93 4712
    public function extractRelations(RelationMap $relMap, object $entity): array
94
    {
95 4712
        if (!property_exists($entity, '__cycle_orm_rel_data')) {
96 1330
            return array_intersect_key($this->entityToArray($entity), $relMap->getRelations());
97
        }
98
99 4034
        $currentData = $entity->__cycle_orm_rel_data;
100 4034
        foreach ($relMap->getRelations() as $key => $relation) {
101 3334
            if (!array_key_exists($key, $currentData)) {
102 2712
                $arrayData ??= $this->entityToArray($entity);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $arrayData does not seem to be defined for all execution paths leading up to this point.
Loading history...
103 2712
                $currentData[$key] = $arrayData[$key];
104
            }
105
        }
106
107 4034
        return $currentData;
108
    }
109
110
    /**
111
     * Get an entity column values as array, except relations.
112
     *
113
     * @return array<string, mixed>
114
     */
115 4784
    public function extractData(RelationMap $relMap, object $entity): array
116
    {
117 4784
        return array_diff_key($this->entityToArray($entity), $relMap->getRelations());
118
    }
119
120 4784
    private function entityToArray(object $entity): array
121
    {
122 4784
        $result = [];
123 4784
        foreach ((array)$entity as $key => $value) {
124 4784
            $result[$key[0] === "\0" ? substr($key, strrpos($key, "\0", 1) + 1) : $key] = $value;
125
        }
126
127
        unset(
128 4784
            $result['__cycle_orm_rel_map'],
129 4784
            $result['__cycle_orm_rel_data'],
130 4784
            $result['__cycle_orm_relation_props']
131
        );
132
133 4784
        return $result;
134
    }
135
136 4432
    private function defineClass(RelationMap $relMap, string $class): string
0 ignored issues
show
Unused Code introduced by
The parameter $relMap is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

136
    private function defineClass(/** @scrutinizer ignore-unused */ RelationMap $relMap, string $class): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
137
    {
138 4432
        if (!class_exists($class, true)) {
139 2
            throw new \RuntimeException(sprintf(
140
                'The entity `%s` class does not exist. Proxy factory can not create classless entities.',
141
                $class
142
            ));
143
        }
144
145 4430
        if (array_key_exists($class, $this->classMap)) {
146
            return $this->classMap[$class];
147
        }
148
149 4430
        $reflection = new \ReflectionClass($class);
150 4430
        if ($reflection->isFinal()) {
151
            throw new \RuntimeException(sprintf('The entity `%s` class is final and can\'t be extended.', $class));
152
        }
153 4430
        $className = "{$class} Cycle ORM Proxy";
154 4430
        $this->classMap[$class] = $className;
155
156 4430
        if (!class_exists($className, false)) {
157 70
            if (str_contains($className, '\\')) {
158 70
                $pos = strrpos($className, '\\');
159 70
                $namespaceStr = sprintf("namespace %s;\n", substr($className, 0, $pos));
160 70
                $classNameStr = substr($className, $pos + 1);
161
            } else {
162
                $namespaceStr = '';
163
                $classNameStr = $className;
164
            }
165
166
            /** @see \Cycle\ORM\Mapper\Proxy\EntityProxyTrait */
167 70
            $classStr = <<<PHP
168
                {$namespaceStr}
169
                class {$classNameStr} extends \\{$class} implements \\Cycle\\ORM\\EntityProxyInterface {
170
                    use \\Cycle\ORM\\Mapper\\Proxy\\EntityProxyTrait;
171
                }
172
                PHP;
173
174 70
            eval($classStr);
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
175
        }
176
177 4430
        return $className;
178
    }
179
180
    /**
181
     * Gets property map (primitive fields, relations) for given Entity.
182
     *
183
     * @throws \ReflectionException
184
     *
185
     * @return PropertyMap[]
186
     */
187 4808
    private function getEntityProperties(object $entity, RelationMap $relMap): array
188
    {
189 4808
        return $this->classProperties[$entity::class] ??= $this->propertiesExtractor
190 4808
            ->extract($entity, array_keys($relMap->getRelations()));
191
    }
192
}
193