Completed
Pull Request — master (#61)
by Anton
04:22
created

ORM::source()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 10
Ratio 100 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 10
loc 10
rs 9.4286
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\ORM;
9
10
use Spiral\Core\Component;
11
use Spiral\Core\Container\SingletonInterface;
12
use Spiral\Core\FactoryInterface;
13
use Spiral\Core\HippocampusInterface;
14
use Spiral\Database\DatabaseManager;
15
use Spiral\Database\Entities\Database;
16
use Spiral\Debug\Traits\LoggerTrait;
17
use Spiral\Models\DataEntity;
18
use Spiral\Models\SchematicEntity;
19
use Spiral\ORM\Configs\ORMConfig;
20
use Spiral\ORM\Entities\Loader;
21
use Spiral\ORM\Entities\RecordSelector;
22
use Spiral\ORM\Entities\RecordSource;
23
use Spiral\ORM\Entities\SchemaBuilder;
24
use Spiral\ORM\Entities\Schemas\RecordSchema;
25
use Spiral\ORM\Exceptions\ORMException;
26
use Spiral\Tokenizer\ClassLocatorInterface;
27
28
/**
29
 * ORM component used to manage state of cached Record's schema, record creation and schema
30
 * analysis.
31
 *
32
 * Attention, do not forget to reset cache between requests.
33
 *
34
 * @todo Think about using views for complex queries? Using views for entities? ViewRecord?
35
 * @todo ability to merge multiple tables into one entity - like SearchEntity? Partial entities?
36
 *
37
 * @todo think about entity cache and potential use cases when model can be accessed from outside
38
 */
39
class ORM extends Component implements SingletonInterface
40
{
41
    use LoggerTrait;
42
43
    /**
44
     * Declares to IoC that component instance should be treated as singleton.
45
     */
46
    const SINGLETON = self::class;
47
48
    /**
49
     * Memory section to store ORM schema.
50
     */
51
    const MEMORY = 'orm.schema';
52
53
    /**
54
     * Normalized record constants.
55
     */
56
    const M_ROLE_NAME   = 0;
57
    const M_SOURCE      = 1;
58
    const M_TABLE       = 2;
59
    const M_DB          = 3;
60
    const M_HIDDEN      = SchematicEntity::SH_HIDDEN;
61
    const M_SECURED     = SchematicEntity::SH_SECURED;
62
    const M_FILLABLE    = SchematicEntity::SH_FILLABLE;
63
    const M_MUTATORS    = SchematicEntity::SH_MUTATORS;
64
    const M_VALIDATES   = SchematicEntity::SH_VALIDATES;
65
    const M_COLUMNS     = 9;
66
    const M_NULLABLE    = 10;
67
    const M_RELATIONS   = 11;
68
    const M_PRIMARY_KEY = 12;
69
70
    /**
71
     * Normalized relation options.
72
     */
73
    const R_TYPE       = 0;
74
    const R_TABLE      = 1;
75
    const R_DEFINITION = 2;
76
    const R_DATABASE   = 3;
77
78
    /**
79
     * Pivot table data location in Record fields. Pivot data only provided when record is loaded
80
     * using many-to-many relation.
81
     */
82
    const PIVOT_DATA = '@pivot';
83
84
    /**
85
     * @var EntityCache
86
     */
87
    private $cache = null;
88
89
    /**
90
     * @var ORMConfig
91
     */
92
    protected $config = null;
93
94
    /**
95
     * Cached records schema.
96
     *
97
     * @var array|null
98
     */
99
    protected $schema = null;
100
101
    /**
102
     * @invisible
103
     * @var DatabaseManager
104
     */
105
    protected $databases = null;
106
107
    /**
108
     * @invisible
109
     * @var FactoryInterface
110
     */
111
    protected $factory = null;
112
113
    /**
114
     * @invisible
115
     * @var HippocampusInterface
116
     */
117
    protected $memory = null;
118
119
    /**
120
     * @param ORMConfig            $config
121
     * @param EntityCache          $cache
122
     * @param HippocampusInterface $memory
123
     * @param DatabaseManager      $databases
124
     * @param FactoryInterface     $factory
125
     */
126
    public function __construct(
127
        ORMConfig $config,
128
        EntityCache $cache,
129
        HippocampusInterface $memory,
130
        DatabaseManager $databases,
131
        FactoryInterface $factory
132
    ) {
133
        $this->config = $config;
134
        $this->memory = $memory;
135
136
        $this->cache = $cache;
137
138
        $this->schema = (array)$memory->loadData(static::MEMORY);
139
140
        $this->databases = $databases;
141
        $this->factory = $factory;
142
    }
143
144
    /**
145
     * @param EntityCache $cache
146
     * @return $this
147
     */
148
    public function setCache(EntityCache $cache)
149
    {
150
        $this->cache = $cache;
151
152
        return $this;
153
    }
154
155
    /**
156
     * @return EntityCache
157
     */
158
    public function cache()
159
    {
160
        return $this->cache;
161
    }
162
163
    /**
164
     * When ORM is cloned we are automatically flushing it's cache and creating new isolated area.
165
     * Basically we have cache enabled per selection.
166
     *
167
     * @see RecordSelector::getIterator()
168
     */
169
    public function __clone()
170
    {
171
        $this->cache = clone $this->cache;
172
173
        if (!$this->cache->isEnabled()) {
174
            $this->logger()->warning("ORM are cloned with disabled state.");
175
        }
176
    }
177
178
    /**
179
     * Get database by it's name from DatabaseManager associated with ORM component.
180
     *
181
     * @param string $database
182
     * @return Database
183
     */
184
    public function database($database)
185
    {
186
        return $this->databases->database($database);
187
    }
188
189
    /**
190
     * Construct instance of Record or receive it from cache (if enabled). Only records with
191
     * declared primary key can be cached.
192
     *
193
     * @param string $class Record class name.
194
     * @param array  $data
195
     * @param bool   $cache Add record to entity cache if enabled.
196
     * @return RecordInterface
197
     */
198
    public function record($class, array $data = [], $cache = true)
199
    {
200
        $schema = $this->schema($class);
201
202
        if (!$this->cache->isEnabled() || !$cache) {
203
            //Entity cache is disabled, we can create record right now
204
            return new $class($data, !empty($data), $this, $schema);
205
        }
206
207
        //We have to find unique object criteria (will work for objects with primary key only)
208
        $primaryKey = null;
209
210
        if (
211
            !empty($schema[self::M_PRIMARY_KEY])
212
            && !empty($data[$schema[self::M_PRIMARY_KEY]])
213
        ) {
214
            $primaryKey = $data[$schema[self::M_PRIMARY_KEY]];
215
        }
216
217
        if ($this->cache->has($class, $primaryKey)) {
218
            /**
219
             * @var RecordInterface $entity
220
             */
221
            return $this->cache->get($class, $primaryKey);
222
        }
223
224
        return $this->cache->remember(
225
            new $class($data, !empty($data), $this, $schema)
226
        );
227
    }
228
229
    /**
230
     * Get ORM source for given class.
231
     *
232
     * @param string $class
233
     * @return RecordSource
234
     * @throws ORMException
235
     */
236 View Code Duplication
    public function source($class)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
237
    {
238
        $schema = $this->schema($class);
239
        if (empty($source = $schema[self::M_SOURCE])) {
240
            //Default source
241
            $source = RecordSource::class;
242
        }
243
244
        return new $source($class, $this);
245
    }
246
247
    /**
248
     * Get ORM selector for given class.
249
     *
250
     * @param string $class
251
     * @param Loader $loader
252
     * @return RecordSelector
253
     */
254
    public function selector($class, Loader $loader = null)
255
    {
256
        return new RecordSelector($class, $this, $loader);
257
    }
258
259
    /**
260
     * Get cached schema for specified record by it's name.
261
     *
262
     * @param string $record
263
     * @return array
264
     * @throws ORMException
265
     */
266 View Code Duplication
    public function schema($record)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
267
    {
268
        if (!isset($this->schema[$record])) {
269
            $this->updateSchema();
270
        }
271
272
        if (!isset($this->schema[$record])) {
273
            throw new ORMException("Undefined ORM schema item, unknown record '{$record}'.");
274
        }
275
276
        return $this->schema[$record];
277
    }
278
279
    /**
280
     * Create record relation instance by given relation type, parent and definition (options).
281
     *
282
     * @param int             $type
283
     * @param RecordInterface $parent
284
     * @param array           $definition Relation definition.
285
     * @param array           $data
286
     * @param bool            $loaded
287
     * @return RelationInterface
288
     * @throws ORMException
289
     */
290 View Code Duplication
    public function relation(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
291
        $type,
292
        RecordInterface $parent,
293
        $definition,
294
        $data = null,
295
        $loaded = false
296
    ) {
297
        if (!$this->config->hasRelation($type, 'class')) {
298
            throw new ORMException("Undefined relation type '{$type}'.");
299
        }
300
301
        $class = $this->config->relationClass($type, 'class');
302
303
        //For performance reasons class constructed without container
304
        return new $class($this, $parent, $definition, $data, $loaded);
305
    }
306
307
    /**
308
     * Get instance of relation/selection loader based on relation type and definition.
309
     *
310
     * @param int    $type       Relation type.
311
     * @param string $container  Container related to parent loader.
312
     * @param array  $definition Relation definition.
313
     * @param Loader $parent     Parent loader (if presented).
314
     * @return LoaderInterface
315
     * @throws ORMException
316
     */
317 View Code Duplication
    public function loader($type, $container, array $definition, Loader $parent = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
318
    {
319
        if (!$this->config->hasRelation($type, 'loader')) {
320
            throw new ORMException("Undefined relation loader '{$type}'.");
321
        }
322
323
        $class = $this->config->relationClass($type, 'loader');
324
325
        //For performance reasons class constructed without container
326
        return new $class($this, $container, $definition, $parent);
327
    }
328
329
    /**
330
     * Update ORM records schema, synchronize declared and database schemas and return instance of
331
     * SchemaBuilder.
332
     *
333
     * @param SchemaBuilder $builder    User specified schema builder.
334
     * @param bool          $syncronize Create all required tables and columns
335
     * @return SchemaBuilder
336
     */
337 View Code Duplication
    public function updateSchema(SchemaBuilder $builder = null, $syncronize = false)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
338
    {
339
        if (empty($builder)) {
340
            $builder = $this->schemaBuilder();
341
        }
342
343
        //Create all required tables and columns
344
        if ($syncronize) {
345
            $builder->synchronizeSchema();
346
        }
347
348
        //Getting normalized (cached) version of schema
349
        $this->schema = $builder->normalizeSchema();
350
351
        //Saving
352
        $this->memory->saveData(static::MEMORY, $this->schema);
353
354
        //Let's reinitialize records
355
        DataEntity::resetInitiated();
356
357
        return $builder;
358
    }
359
360
    /**
361
     * Get instance of ORM SchemaBuilder.
362
     *
363
     * @param ClassLocatorInterface $locator
364
     * @return SchemaBuilder
365
     */
366
    public function schemaBuilder(ClassLocatorInterface $locator = null)
367
    {
368
        return $this->factory->make(SchemaBuilder::class, [
369
            'config'  => $this->config,
370
            'orm'     => $this,
371
            'locator' => $locator
372
        ]);
373
    }
374
375
    /**
376
     * Create instance of relation schema based on relation type and given definition (declared in
377
     * record). Resolve using container to support any possible relation type. You can create your
378
     * own relations, loaders and schemas by altering ORM config.
379
     *
380
     * @param mixed         $type
381
     * @param SchemaBuilder $builder
382
     * @param RecordSchema  $record
383
     * @param string        $name
384
     * @param array         $definition
385
     * @return Schemas\RelationInterface
386
     */
387
    public function relationSchema(
388
        $type,
389
        SchemaBuilder $builder,
390
        RecordSchema $record,
391
        $name,
392
        array $definition
393
    ) {
394
        if (!$this->config->hasRelation($type, 'schema')) {
395
            throw new ORMException("Undefined relation schema '{$type}'.");
396
        }
397
398
        //Getting needed relation schema builder
399
        return $this->factory->make(
400
            $this->config->relationClass($type, 'schema'),
401
            compact('builder', 'record', 'name', 'definition')
402
        );
403
    }
404
}
405