1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Cycle\ORM; |
6
|
|
|
|
7
|
|
|
use Cycle\Database\DatabaseInterface; |
8
|
|
|
use Cycle\Database\DatabaseProviderInterface; |
9
|
|
|
use Cycle\ORM\Collection\ArrayCollectionFactory; |
10
|
|
|
use Cycle\ORM\Collection\CollectionFactoryInterface; |
11
|
|
|
use Cycle\ORM\Config\RelationConfig; |
12
|
|
|
use Cycle\ORM\Exception\FactoryTypecastException; |
13
|
|
|
use Cycle\ORM\Exception\TypecastException; |
14
|
|
|
use Cycle\ORM\Mapper\Mapper; |
15
|
|
|
use Cycle\ORM\Parser\CompositeTypecast; |
16
|
|
|
use Cycle\ORM\Parser\Typecast; |
17
|
|
|
use Cycle\ORM\Parser\TypecastInterface; |
18
|
|
|
use Cycle\ORM\Service\SourceProviderInterface; |
19
|
|
|
use Cycle\ORM\Relation\RelationInterface; |
20
|
|
|
use Cycle\ORM\Select\Loader\ParentLoader; |
21
|
|
|
use Cycle\ORM\Select\Loader\SubclassLoader; |
22
|
|
|
use Cycle\ORM\Select\LoaderInterface; |
23
|
|
|
use Cycle\ORM\Select\Repository; |
24
|
|
|
use Cycle\ORM\Select\ScopeInterface; |
25
|
|
|
use Cycle\ORM\Select\Source; |
26
|
|
|
use Cycle\ORM\Select\SourceInterface; |
27
|
|
|
use Spiral\Core\Container; |
28
|
|
|
use Spiral\Core\FactoryInterface as CoreFactory; |
29
|
|
|
|
30
|
|
|
final class Factory implements FactoryInterface |
31
|
|
|
{ |
32
|
|
|
private RelationConfig $config; |
33
|
|
|
private CoreFactory $factory; |
34
|
|
|
|
35
|
|
|
/** @var array<int, string> */ |
36
|
|
|
private array $defaults = [ |
37
|
|
|
SchemaInterface::REPOSITORY => Repository::class, |
38
|
|
|
SchemaInterface::SOURCE => Source::class, |
39
|
|
|
SchemaInterface::MAPPER => Mapper::class, |
40
|
|
|
SchemaInterface::SCOPE => null, |
41
|
|
|
SchemaInterface::TYPECAST_HANDLER => null, |
42
|
|
|
]; |
43
|
|
|
|
44
|
|
|
/** @var array<string, CollectionFactoryInterface> */ |
45
|
|
|
private array $collectionFactoryAlias = []; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* @var array<string, CollectionFactoryInterface> |
49
|
|
|
* |
50
|
|
|
* @psalm-var array<class-string, CollectionFactoryInterface> |
51
|
|
|
*/ |
52
|
|
|
private array $collectionFactoryInterface = []; |
53
|
|
|
|
54
|
|
|
private CollectionFactoryInterface $defaultCollectionFactory; |
55
|
7414 |
|
|
56
|
|
|
public function __construct( |
57
|
|
|
private DatabaseProviderInterface $dbal, |
58
|
|
|
RelationConfig $config = null, |
59
|
|
|
CoreFactory $factory = null, |
60
|
|
|
CollectionFactoryInterface $defaultCollectionFactory = null |
61
|
7414 |
|
) { |
62
|
7414 |
|
$this->config = $config ?? RelationConfig::getDefault(); |
63
|
7414 |
|
$this->factory = $factory ?? new Container(); |
64
|
|
|
$this->defaultCollectionFactory = $defaultCollectionFactory ?? new ArrayCollectionFactory(); |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
public function make( |
68
|
|
|
string $alias, |
69
|
|
|
array $parameters = [] |
70
|
|
|
): mixed { |
71
|
|
|
return $this->factory->make($alias, $parameters); |
72
|
|
|
} |
73
|
6860 |
|
|
74
|
|
|
public function typecast(SchemaInterface $schema, DatabaseInterface $database, string $role): ?TypecastInterface |
75
|
|
|
{ |
76
|
6860 |
|
// Get parent's typecast |
77
|
6860 |
|
$parent = $schema->define($role, SchemaInterface::PARENT); |
78
|
6860 |
|
$parentHandler = $parent === null ? null : $this->typecast($schema, $database, $parent); |
79
|
|
|
$handlers = []; |
80
|
|
|
|
81
|
6860 |
|
// Schema's `typecast` option |
82
|
6860 |
|
$rules = (array)$schema->define($role, SchemaInterface::TYPECAST); |
83
|
6830 |
|
$handler = $schema->define($role, SchemaInterface::TYPECAST_HANDLER) |
84
|
|
|
?? $this->defaults[SchemaInterface::TYPECAST_HANDLER]; |
85
|
|
|
|
86
|
|
|
// Create basic typecast implementation |
87
|
6860 |
|
try { |
88
|
6830 |
|
if ($handler === null) { |
89
|
3696 |
|
if (!$rules) { |
|
|
|
|
90
|
|
|
return $parentHandler; |
91
|
|
|
} |
92
|
3600 |
|
|
93
|
30 |
|
$handlers[] = new Typecast($database); |
94
|
20 |
|
} elseif (\is_array($handler)) { // We need to use composite typecast for array |
95
|
20 |
|
foreach ($handler as $type) { |
96
|
|
|
$handlers[] = $this->makeTypecastHandler($type, $database, $schema, $role); |
97
|
|
|
} |
98
|
3628 |
|
} else { |
99
|
|
|
$handlers[] = $this->makeTypecastHandler($handler, $database, $schema, $role); |
100
|
6 |
|
} |
101
|
6 |
|
} catch (\Throwable $e) { |
102
|
6 |
|
throw new FactoryTypecastException( |
103
|
|
|
message: \sprintf( |
104
|
|
|
'Bad typecast handler declaration for the `%s` role. %s', |
105
|
6 |
|
$role, |
106
|
|
|
$e->getMessage() |
107
|
6 |
|
), |
108
|
|
|
code: $e->getCode(), |
109
|
|
|
previous: $e, |
110
|
|
|
); |
111
|
3624 |
|
} |
112
|
3624 |
|
$handler = count($handlers) === 1 ? reset($handlers) : new CompositeTypecast(...$handlers); |
113
|
3624 |
|
$handler->setRules($rules); |
114
|
3624 |
|
return $parentHandler === null |
115
|
3624 |
|
? $handler |
116
|
|
|
: new CompositeTypecast($parentHandler, $handler); |
117
|
|
|
} |
118
|
6850 |
|
|
119
|
|
|
public function mapper(ORMInterface $orm, string $role): MapperInterface |
120
|
6850 |
|
{ |
121
|
6850 |
|
$schema = $orm->getSchema(); |
122
|
|
|
$class = $schema->define($role, SchemaInterface::MAPPER) ?? $this->defaults[SchemaInterface::MAPPER]; |
123
|
6850 |
|
|
124
|
8 |
|
if (!\is_subclass_of($class, MapperInterface::class)) { |
125
|
|
|
throw new TypecastException(sprintf('%s does not implement %s.', $class, MapperInterface::class)); |
126
|
|
|
} |
127
|
6842 |
|
|
128
|
|
|
return $this->factory->make( |
|
|
|
|
129
|
|
|
$class, |
130
|
|
|
[ |
131
|
|
|
'orm' => $orm, |
132
|
6842 |
|
'role' => $role, |
133
|
|
|
'schema' => $schema->define($role, SchemaInterface::SCHEMA), |
134
|
|
|
] |
135
|
|
|
); |
136
|
|
|
} |
137
|
4554 |
|
|
138
|
|
|
public function loader( |
139
|
|
|
SchemaInterface $schema, |
140
|
|
|
SourceProviderInterface $sourceProvider, |
141
|
|
|
string $role, |
142
|
|
|
string $relation |
143
|
4554 |
|
): LoaderInterface { |
144
|
744 |
|
if ($relation === self::PARENT_LOADER) { |
145
|
744 |
|
$parent = $schema->define($role, SchemaInterface::PARENT); |
146
|
|
|
return new ParentLoader($schema, $sourceProvider, $this, $role, $parent); |
147
|
4394 |
|
} |
148
|
616 |
|
if ($relation === self::CHILD_LOADER) { |
149
|
616 |
|
$parent = $schema->define($role, SchemaInterface::PARENT); |
150
|
|
|
return new SubclassLoader($schema, $sourceProvider, $this, $parent, $role); |
151
|
4082 |
|
} |
152
|
|
|
$definition = $schema->defineRelation($role, $relation); |
153
|
4082 |
|
|
154
|
4082 |
|
return $this->config->getLoader($definition[Relation::TYPE])->resolve( |
|
|
|
|
155
|
|
|
$this->factory, |
156
|
|
|
[ |
157
|
|
|
'ormSchema' => $schema, |
158
|
|
|
'sourceProvider' => $sourceProvider, |
159
|
|
|
'factory' => $this, |
160
|
|
|
'role' => $role, |
161
|
4082 |
|
'name' => $relation, |
162
|
4082 |
|
'target' => $definition[Relation::TARGET], |
163
|
|
|
'schema' => $definition[Relation::SCHEMA], |
164
|
|
|
] |
165
|
|
|
); |
166
|
|
|
} |
167
|
1900 |
|
|
168
|
|
|
public function collection( |
169
|
|
|
string $name = null |
170
|
1900 |
|
): CollectionFactoryInterface { |
171
|
1824 |
|
if ($name === null) { |
172
|
|
|
return $this->defaultCollectionFactory; |
173
|
76 |
|
} |
174
|
60 |
|
if (array_key_exists($name, $this->collectionFactoryAlias)) { |
175
|
|
|
return $this->collectionFactoryAlias[$name]; |
176
|
|
|
} |
177
|
16 |
|
// Find by interface |
178
|
16 |
|
if (\class_exists($name)) { |
179
|
16 |
|
foreach ($this->collectionFactoryInterface as $interface => $factory) { |
180
|
16 |
|
if (\is_subclass_of($name, $interface, true)) { |
181
|
|
|
return $this->collectionFactoryAlias[$name] = $factory->withCollectionClass($name); |
182
|
|
|
} |
183
|
|
|
} |
184
|
|
|
} |
185
|
|
|
return $this->collectionFactoryAlias[$name] = $this->factory->make($name); |
|
|
|
|
186
|
|
|
} |
187
|
5364 |
|
|
188
|
|
|
public function relation( |
189
|
|
|
ORMInterface $orm, |
190
|
|
|
SchemaInterface $schema, |
191
|
|
|
string $role, |
192
|
|
|
string $relation |
193
|
5364 |
|
): RelationInterface { |
194
|
5364 |
|
$relSchema = $schema->defineRelation($role, $relation); |
195
|
|
|
$type = $relSchema[Relation::TYPE]; |
196
|
5364 |
|
|
197
|
5364 |
|
return $this->config->getRelation($type)->resolve( |
|
|
|
|
198
|
|
|
$this->factory, |
199
|
|
|
[ |
200
|
|
|
'orm' => $orm, |
201
|
|
|
'role' => $role, |
202
|
5364 |
|
'name' => $relation, |
203
|
5364 |
|
'target' => $relSchema[Relation::TARGET], |
204
|
5364 |
|
'schema' => $relSchema[Relation::SCHEMA] |
205
|
5364 |
|
+ [Relation::LOAD => $relSchema[Relation::LOAD] ?? null] |
206
|
|
|
+ [Relation::COLLECTION_TYPE => $relSchema[Relation::COLLECTION_TYPE] ?? null], |
207
|
|
|
] |
208
|
|
|
); |
209
|
|
|
} |
210
|
7172 |
|
|
211
|
|
|
public function database(string $database = null): DatabaseInterface |
212
|
7172 |
|
{ |
213
|
|
|
return $this->dbal->database($database); |
214
|
|
|
} |
215
|
918 |
|
|
216
|
|
|
public function repository( |
217
|
|
|
ORMInterface $orm, |
218
|
|
|
SchemaInterface $schema, |
219
|
|
|
string $role, |
220
|
|
|
?Select $select |
221
|
918 |
|
): RepositoryInterface { |
222
|
|
|
$class = $schema->define($role, SchemaInterface::REPOSITORY) ?? $this->defaults[SchemaInterface::REPOSITORY]; |
223
|
918 |
|
|
224
|
8 |
|
if (!\is_subclass_of($class, RepositoryInterface::class)) { |
225
|
|
|
throw new TypecastException($class . ' does not implement ' . RepositoryInterface::class); |
226
|
|
|
} |
227
|
910 |
|
|
228
|
|
|
return $this->factory->make( |
|
|
|
|
229
|
|
|
$class, |
230
|
|
|
[ |
231
|
|
|
'select' => $select, |
232
|
|
|
'orm' => $orm, |
233
|
|
|
'role' => $role, |
234
|
|
|
] |
235
|
|
|
); |
236
|
|
|
} |
237
|
7180 |
|
|
238
|
|
|
public function source( |
239
|
|
|
SchemaInterface $schema, |
240
|
|
|
string $role |
241
|
|
|
): SourceInterface { |
242
|
7180 |
|
/** @var class-string<SourceInterface> $source */ |
243
|
|
|
$source = $schema->define($role, SchemaInterface::SOURCE) ?? $this->defaults[SchemaInterface::SOURCE]; |
244
|
7180 |
|
|
245
|
8 |
|
if (!\is_subclass_of($source, SourceInterface::class)) { |
246
|
|
|
throw new TypecastException($source . ' does not implement ' . SourceInterface::class); |
247
|
|
|
} |
248
|
7172 |
|
|
249
|
7172 |
|
$table = $schema->define($role, SchemaInterface::TABLE); |
250
|
|
|
$database = $this->database($schema->define($role, SchemaInterface::DATABASE)); |
251
|
7172 |
|
|
252
|
8 |
|
$source = $source !== Source::class |
253
|
7164 |
|
? $this->factory->make($source, ['role' => $role, 'table' => $table, 'database' => $database]) |
254
|
|
|
: new Source($database, $table); |
255
|
|
|
|
256
|
7172 |
|
/** @var class-string<ScopeInterface>|ScopeInterface|null $scope */ |
257
|
|
|
$scope = $schema->define($role, SchemaInterface::SCOPE) ?? $this->defaults[SchemaInterface::SCOPE]; |
258
|
7172 |
|
|
259
|
6764 |
|
if ($scope === null) { |
260
|
|
|
return $source; |
|
|
|
|
261
|
|
|
} |
262
|
1736 |
|
|
263
|
|
|
if (!\is_subclass_of($scope, ScopeInterface::class)) { |
264
|
|
|
throw new TypecastException(sprintf('%s does not implement %s.', $scope, ScopeInterface::class)); |
265
|
|
|
} |
266
|
1736 |
|
|
267
|
|
|
return $source->withScope(\is_object($scope) ? $scope : $this->factory->make($scope)); |
|
|
|
|
268
|
|
|
} |
269
|
48 |
|
|
270
|
|
|
public function withDefaultSchemaClasses(array $defaults): self |
271
|
48 |
|
{ |
272
|
|
|
$clone = clone $this; |
273
|
48 |
|
|
274
|
|
|
$clone->defaults = $defaults + $this->defaults; |
275
|
48 |
|
|
276
|
|
|
return $clone; |
277
|
|
|
} |
278
|
7404 |
|
|
279
|
|
|
public function withCollectionFactory( |
280
|
|
|
string $alias, |
281
|
|
|
CollectionFactoryInterface $factory, |
282
|
|
|
string $interface = null |
283
|
7404 |
|
): self { |
284
|
7404 |
|
$clone = clone $this; |
285
|
|
|
$interface = $interface ?? $factory->getInterface(); |
286
|
7404 |
|
|
287
|
7404 |
|
$clone->collectionFactoryAlias[$alias] = $factory; |
288
|
192 |
|
if ($interface !== null) { |
289
|
|
|
$clone->collectionFactoryInterface[$interface] = $factory; |
290
|
|
|
} |
291
|
7404 |
|
|
292
|
|
|
return $clone; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* Make typecast handler from giver string or object |
297
|
|
|
* |
298
|
|
|
* @return TypecastInterface |
299
|
30 |
|
*/ |
300
|
|
|
private function makeTypecastHandler( |
301
|
|
|
string|TypecastInterface $handler, |
302
|
|
|
DatabaseInterface $database, |
303
|
|
|
SchemaInterface $schema, |
304
|
|
|
string $role |
305
|
|
|
): TypecastInterface { |
306
|
30 |
|
// If handler is an object we don't need to use factory, we should return it as is |
307
|
2 |
|
if (is_object($handler)) { |
308
|
|
|
return $handler; |
309
|
|
|
} |
310
|
30 |
|
|
311
|
|
|
return $this->factory->make($handler, [ |
|
|
|
|
312
|
|
|
'database' => $database, |
313
|
|
|
'schema' => $schema, |
314
|
|
|
'role' => $role, |
315
|
|
|
]); |
316
|
|
|
} |
317
|
|
|
} |
318
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.