Passed
Pull Request — 2.x (#537)
by Aleksei
17:46
created

BelongsToMorphedLoader::getTarget()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\ORM\Select\Loader\Morphed;
6
7
use Cycle\Database\Query\SelectQuery;
8
use Cycle\ORM\Exception\LoaderException;
9
use Cycle\ORM\FactoryInterface;
10
use Cycle\ORM\Parser\AbstractNode;
11
use Cycle\ORM\Parser\ProxyNode;
12
use Cycle\ORM\Parser\SingularNode;
13
use Cycle\ORM\Relation;
14
use Cycle\ORM\SchemaInterface;
15
use Cycle\ORM\Select\LoaderInterface;
16
use Cycle\ORM\Select\RootLoader;
17
use Cycle\ORM\Service\SourceProviderInterface;
18
19
/**
20
 * Creates an additional query constrain based on parent entity alias.
21
 */
22
final class BelongsToMorphedLoader implements LoaderInterface
23
{
24
    /**
25
     * Loader that contains current loader
26
     */
27
    protected ?LoaderInterface $parent = null;
28
29
    protected array $options = [
30
        'load' => false,
31
        'scope' => true,
32
    ];
33
    private ProxyNode $node;
34
35
    /** @var non-empty-string */
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
36
    private string $morphKey;
37
38
    /** @var array<non-empty-string> */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<non-empty-string> at position 2 could not be parsed: Unknown type name 'non-empty-string' at position 2 in array<non-empty-string>.
Loading history...
39
    private array $innerKey;
40
41
    /** @var array<non-empty-string> */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<non-empty-string> at position 2 could not be parsed: Unknown type name 'non-empty-string' at position 2 in array<non-empty-string>.
Loading history...
42
    private array $outerKey;
43
44
    /**
45
     * @param class-string $target Target entity interface
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
46
     * @param array<non-empty-string, mixed> $schema Relation schema
47
     */
48
    public function __construct(
49
        private SchemaInterface $ormSchema,
50
        private SourceProviderInterface $sourceProvider,
51
        private FactoryInterface $factory,
52
        private string $target,
53
        array $schema,
54
    ) {
55
        $this->morphKey = $schema[Relation::MORPH_KEY];
56
        $this->innerKey = (array) $schema[Relation::INNER_KEY];
57
        $this->outerKey = (array) $schema[Relation::OUTER_KEY];
58
        $this->node = new ProxyNode([$this->morphKey, ...$this->innerKey]);
59
    }
60
61
    /**
62
     * Return the relation alias.
63
     */
64
    public function getAlias(): string
65
    {
66
        throw new \RuntimeException('Not implemented');
67
    }
68
69
    /**
70
     * Loader specific entity class.
71
     */
72
    public function getTarget(): string
73
    {
74
        return $this->target;
75
    }
76
77
    /**
78
     * Get column name related to internal key.
79
     */
80
    public function fieldAlias(string $field): ?string
81
    {
82
        throw new \RuntimeException('Not implemented');
83
    }
84
85
    public function withContext(LoaderInterface $parent, array $options = []): static
86
    {
87
        // check that given options are known
88
        if (!empty($wrong = \array_diff(\array_keys($options), \array_keys($this->options)))) {
89
            throw new LoaderException(
90
                \sprintf(
91
                    'Relation %s does not support option: %s',
92
                    $this::class,
93
                    \implode(', ', $wrong),
94
                ),
95
            );
96
        }
97
98
        $loader = clone $this;
99
        $loader->parent = $parent;
100
        $loader->options = $options + $this->options;
101
102
        return $loader;
103
    }
104
105
    public function createNode(): AbstractNode
106
    {
107
        return $this->node;
108
    }
109
110
    public function loadData(AbstractNode $node, bool $includeRole = false): void
111
    {
112
        // Get references from the parent node
113
        $references = $node->getReferenceValues();
114
115
        // Group references by their role (morph type)
116
        $groupedByRole = $this->groupReferencesByRole($references);
117
118
        // Load data for each role
119
        foreach ($groupedByRole as $role => $roleReferences) {
120
            $this->loadRoleData($node, $role, $roleReferences);
121
        }
122
    }
123
124
    public function setSubclassesLoading(bool $enabled): void {}
125
126
    public function isHierarchical(): bool
127
    {
128
        return false;
129
    }
130
131
    protected function configureQuery(SelectQuery $query, array $criteria): SelectQuery
132
    {
133
        // Map criteria to inner keys
134
        $where = [];
135
        foreach ($this->innerKey as $i => $key) {
136
            $where[$this->outerKey[$i]] = $criteria[$key];
137
        }
138
139
        $query->where(\key($where), \reset($where));
140
141
        return $query;
142
    }
143
144
    private function groupReferencesByRole(array $references): array
145
    {
146
        $grouped = [];
147
        foreach ($references as $ref) {
148
            $role = $ref[$this->morphKey];
149
            unset($ref[$this->morphKey]);
150
            $grouped[$role] = $ref;
151
        }
152
153
        return $grouped;
154
    }
155
156
    private function loadRoleData(
157
        AbstractNode $node,
158
        string $role,
159
        array $references,
160
    ): void {
161
        $columns = $this->normalizeColumns($this->ormSchema->define($role, SchemaInterface::COLUMNS));
162
        $pk = (array) $this->ormSchema->define($role, SchemaInterface::PRIMARY_KEY);
163
        $newNode = new SingularNode($columns, $pk, $this->outerKey, [$this->morphKey, ...$this->innerKey], $role);
164
165
        // Register this role in the morphed node
166
        $roleNode = $node->addNode($role, $newNode);
0 ignored issues
show
Bug introduced by
The method addNode() does not exist on Cycle\ORM\Parser\AbstractNode. It seems like you code against a sub-type of Cycle\ORM\Parser\AbstractNode such as Cycle\ORM\Parser\ProxyNode. ( Ignorable by Annotation )

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

166
        /** @scrutinizer ignore-call */ 
167
        $roleNode = $node->addNode($role, $newNode);
Loading history...
167
168
        // Create loader for the specific role
169
        $loader = new RootLoader(
170
            $this->ormSchema,
171
            $this->sourceProvider,
172
            $this->factory,
173
            $role,
174
            loadRelations: false, // Don't auto-load eager relations
175
        );
176
177
        // Ensure all nested relations
178
        // todo @see src/Select/JoinableLoader.php:134
179
        // $query = $this->initQuery($role);
180
181
        // Configure query with WHERE IN condition
182
        $query = $loader->getQuery();
183
        $this->configureQuery($query, $references);
184
185
        // $node = $loader->createNode();
186
        // $loader->loadData($node, includeRole: true);
187
188
        // Execute query
189
        $statement = $query->run();
190
191
        // Parse fetched rows into the ROLE-SPECIFIC node
192
        foreach ($statement->fetchAll(\Cycle\Database\StatementInterface::FETCH_NUM) as $row) {
193
            $roleNode->parseRow(0, $row);
194
        }
195
196
        $statement->close();
197
    }
198
199
    private function normalizeColumns(array $columns): array
200
    {
201
        $result = [];
202
        foreach ($columns as $alias => $column) {
203
            $result[] = \is_int($alias) ? $column : $alias;
204
        }
205
206
        return $result;
207
    }
208
}
209