Factory   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 285
Duplicated Lines 0 %

Test Coverage

Coverage 96.23%

Importance

Changes 3
Bugs 1 Features 1
Metric Value
eloc 128
c 3
b 1
f 1
dl 0
loc 285
ccs 102
cts 106
cp 0.9623
rs 9.44
wmc 37

13 Methods

Rating   Name   Duplication   Size   Complexity  
A repository() 0 18 2
A collection() 0 18 6
A source() 0 30 6
A mapper() 0 15 2
A database() 0 3 1
A loader() 0 26 3
A makeTypecastHandler() 0 15 2
A withDefaultSchemaClasses() 0 7 1
A relation() 0 19 1
B typecast() 0 43 9
A withCollectionFactory() 0 14 2
A make() 0 5 1
A __construct() 0 9 1
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
Bug Best Practice introduced by
The expression $rules of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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
Bug Best Practice introduced by
The expression return $this->factory->m...emaInterface::SCHEMA))) could return the type null which is incompatible with the type-hinted return Cycle\ORM\MapperInterface. Consider adding an additional type-check to rule them out.
Loading history...
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
Bug Best Practice introduced by
The expression return $this->config->ge...ORM\Relation::SCHEMA])) could return the type null which is incompatible with the type-hinted return Cycle\ORM\Select\LoaderInterface. Consider adding an additional type-check to rule them out.
Loading history...
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
Bug Best Practice introduced by
The expression return $this->collection...s->factory->make($name) could return the type null which is incompatible with the type-hinted return Cycle\ORM\Collection\CollectionFactoryInterface. Consider adding an additional type-check to rule them out.
Loading history...
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
Bug Best Practice introduced by
The expression return $this->config->ge...ECTION_TYPE] ?? null))) could return the type null which is incompatible with the type-hinted return Cycle\ORM\Relation\RelationInterface. Consider adding an additional type-check to rule them out.
Loading history...
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
Bug Best Practice introduced by
The expression return $this->factory->m...$orm, 'role' => $role)) could return the type null which is incompatible with the type-hinted return Cycle\ORM\RepositoryInterface. Consider adding an additional type-check to rule them out.
Loading history...
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
Bug Best Practice introduced by
The expression return $source could return the type null which is incompatible with the type-hinted return Cycle\ORM\Select\SourceInterface. Consider adding an additional type-check to rule them out.
Loading history...
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
Bug introduced by
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 ignore-call  annotation

267
        return $source->/** @scrutinizer ignore-call */ withScope(\is_object($scope) ? $scope : $this->factory->make($scope));

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.

Loading history...
Bug Best Practice introduced by
The expression return $source->withScop...>factory->make($scope)) could return the type null which is incompatible with the type-hinted return Cycle\ORM\Select\SourceInterface. Consider adding an additional type-check to rule them out.
Loading history...
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, [
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->factory->m...hema, 'role' => $role)) could return the type null which is incompatible with the type-hinted return Cycle\ORM\Parser\TypecastInterface. Consider adding an additional type-check to rule them out.
Loading history...
312
            'database' => $database,
313
            'schema' => $schema,
314
            'role' => $role,
315
        ]);
316
    }
317
}
318