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) { |
|||
0 ignored issues
–
show
|
|||||
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( |
||||
0 ignored issues
–
show
|
|||||
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( |
|||
0 ignored issues
–
show
|
|||||
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); |
||||
0 ignored issues
–
show
|
|||||
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( |
|||
0 ignored issues
–
show
|
|||||
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( |
||||
0 ignored issues
–
show
|
|||||
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; |
||||
0 ignored issues
–
show
|
|||||
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)); |
||||
0 ignored issues
–
show
The method
withScope() does not exist on null .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||
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 | private function makeTypecastHandler( |
||||
299 | 30 | string|TypecastInterface $handler, |
|||
300 | DatabaseInterface $database, |
||||
301 | SchemaInterface $schema, |
||||
302 | string $role, |
||||
303 | ): TypecastInterface { |
||||
304 | // If handler is an object we don't need to use factory, we should return it as is |
||||
305 | if (\is_object($handler)) { |
||||
306 | 30 | return $handler; |
|||
307 | 2 | } |
|||
308 | |||||
309 | return $this->factory->make($handler, [ |
||||
0 ignored issues
–
show
|
|||||
310 | 30 | 'database' => $database, |
|||
311 | 'schema' => $schema, |
||||
312 | 'role' => $role, |
||||
313 | ]); |
||||
314 | } |
||||
315 | } |
||||
316 |
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.