Completed
Branch feature/pre-split (548a16)
by Anton
03:55
created

ODM::getFactory()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Spiral Framework, Core Components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\ODM;
8
9
use Interop\Container\ContainerInterface;
10
use MongoDB\BSON\ObjectID;
11
use MongoDB\Collection;
12
use Spiral\Core\Component;
13
use Spiral\Core\Container;
14
use Spiral\Core\Container\SingletonInterface;
15
use Spiral\Core\FactoryInterface;
16
use Spiral\Core\MemoryInterface;
17
use Spiral\Core\NullMemory;
18
use Spiral\ODM\Entities\DocumentSelector;
19
use Spiral\ODM\Entities\DocumentSource;
20
use Spiral\ODM\Exceptions\ODMException;
21
use Spiral\ODM\Exceptions\SchemaException;
22
use Spiral\ODM\Schemas\LocatorInterface;
23
use Spiral\ODM\Schemas\NullLocator;
24
use Spiral\ODM\Schemas\SchemaBuilder;
25
26
/**
27
 * Provides supporting functionality for ODM classes such as selectors, instantiators and schema
28
 * builders.
29
 *
30
 * @todo add ODM strict mode which must thrown an exception in AbstractArray and DocumentCompositor
31
 * @todo when multiple atomic operations applied to a field instead of forcing $set command.
32
 */
33
class ODM extends Component implements ODMInterface, SingletonInterface
34
{
35
    /**
36
     * Memory section to store ODM schema.
37
     */
38
    const MEMORY = 'odm.schema';
39
40
    /**
41
     * Already created instantiators.
42
     *
43
     * @invisible
44
     * @var InstantiatorInterface[]
45
     */
46
    private $instantiators = [];
47
48
    /**
49
     * ODM schema.
50
     *
51
     * @invisible
52
     * @var array
53
     */
54
    private $schema = [];
55
56
    /**
57
     * @var MongoManager
58
     */
59
    protected $manager;
60
61
    /**
62
     * @var LocatorInterface
63
     */
64
    protected $locator;
65
66
    /**
67
     * @invisible
68
     * @var MemoryInterface
69
     */
70
    protected $memory;
71
72
    /**
73
     * Container defines working scope for all Documents and DocumentEntities.
74
     *
75
     * @var ContainerInterface
76
     */
77
    protected $container;
78
79
    /**
80
     * @param MongoManager       $manager
81
     * @param LocatorInterface   $locator
82
     * @param MemoryInterface    $memory
83
     * @param ContainerInterface $container
84
     */
85
    public function __construct(
86
        MongoManager $manager,
87
        LocatorInterface $locator = null,
88
        MemoryInterface $memory = null,
89
        ContainerInterface $container = null
90
    ) {
91
        $this->manager = $manager;
92
93
        $this->locator = $locator ?? new NullLocator();
94
        $this->memory = $memory ?? new NullMemory();
95
        $this->container = $container ?? new Container();
96
97
        //Loading schema from memory (if any)
98
        $this->schema = $this->loadSchema();
99
    }
100
101
    /**
102
     * Create instance of ORM SchemaBuilder.
103
     *
104
     * @param bool $locate Set to true to automatically locate available documents and document
105
     *                     sources in a project files (based on tokenizer scope).
106
     *
107
     * @return SchemaBuilder
108
     *
109
     * @throws SchemaException
110
     */
111
    public function schemaBuilder(bool $locate = true): SchemaBuilder
112
    {
113
        /**
114
         * @var SchemaBuilder $builder
115
         */
116
        $builder = $this->getFactory()->make(SchemaBuilder::class, ['manager' => $this->manager]);
117
118
        if ($locate) {
119
            foreach ($this->locator->locateSchemas() as $schema) {
120
                $builder->addSchema($schema);
121
            }
122
123
            foreach ($this->locator->locateSources() as $class => $source) {
124
                $builder->addSource($class, $source);
125
            }
126
        }
127
128
        return $builder;
129
    }
130
131
    /**
132
     * Specify behaviour schema for ODM to be used.
133
     *
134
     * @param SchemaBuilder $builder
135
     * @param bool          $remember Set to true to remember packed schema in memory.
136
     */
137
    public function buildSchema(SchemaBuilder $builder, bool $remember = false)
138
    {
139
        $this->schema = $builder->packSchema();
140
141
        if ($remember) {
142
            $this->memory->saveData(static::MEMORY, $this->schema);
143
        }
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149
    public function define(string $class, int $property)
150
    {
151
        if (empty($this->schema)) {
152
            //Update and remember
153
            $this->buildSchema($this->schemaBuilder(), true);
154
        }
155
156
        //Check value
157
        if (!isset($this->schema[$class])) {
158
            throw new ODMException("Undefined ODM schema item '{$class}', make sure schema is updated");
159
        }
160
161
        if (!array_key_exists($property, $this->schema[$class])) {
162
            throw new ODMException("Undefined ODM schema property '{$class}'.'{$property}'");
163
        }
164
165
        return $this->schema[$class][$property];
166
    }
167
168
    /**
169
     * Get source (selection repository) for specific entity class.
170
     *
171
     * @param string $class
172
     *
173
     * @return DocumentSource
174
     */
175
    public function source(string $class): DocumentSource
176
    {
177
        $source = $this->define($class, self::D_SOURCE_CLASS);
178
179
        if (empty($source)) {
180
            //Let's use default source
181
            $source = DocumentSource::class;
182
        }
183
184
        $handles = $source::DOCUMENT;
185
        if (empty($handles)) {
186
            //All sources are linked to primary class (i.e. Admin source => User class), unless specified
187
            //in source directly
188
            $handles = $class;
189
        }
190
191
        return $this->getFactory()->make($source, [
192
            'class' => $handles,
193
            'odm'   => $this
194
        ]);
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200
    public function selector(string $class): DocumentSelector
201
    {
202
        return new DocumentSelector(
203
            $this->collection($class),
204
            $this->define($class, self::D_PRIMARY_CLASS),
205
            $this
206
        );
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212
    public function collection(string $class): Collection
213
    {
214
        return $this->manager->database(
215
            $this->define($class, self::D_DATABASE)
216
        )->selectCollection(
217
            $this->define($class, self::D_COLLECTION)
218
        );
219
    }
220
221
    /**
222
     * {@inheritdoc}
223
     */
224
    public function make(
225
        string $class,
226
        $fields = [],
227
        bool $filter = false
228
    ): CompositableInterface {
229
        return $this->instantiator($class)->make($fields, $filter);
230
    }
231
232
    /**
233
     * Get object responsible for class instantiation.
234
     *
235
     * @param string $class
236
     *
237
     * @return InstantiatorInterface
238
     */
239
    protected function instantiator(string $class): InstantiatorInterface
240
    {
241
        if (isset($this->instantiators[$class])) {
242
            return $this->instantiators[$class];
243
        }
244
245
        //Potential optimization
246
        $instantiator = $this->getFactory()->make(
247
            $this->define($class, self::D_INSTANTIATOR),
248
            [
249
                'class'  => $class,
250
                'odm'    => $this,
251
                'schema' => $this->define($class, self::D_SCHEMA)
252
            ]
253
        );
254
255
        //Constructing instantiator and storing it in cache
256
        return $this->instantiators[$class] = $instantiator;
257
    }
258
259
    /**
260
     * Load packed schema from memory.
261
     *
262
     * @return array
263
     */
264
    protected function loadSchema(): array
265
    {
266
        return (array)$this->memory->loadData(static::MEMORY);
267
    }
268
269
    /**
270
     * Get ODM specific factory.
271
     *
272
     * @return FactoryInterface
273
     */
274
    protected function getFactory(): FactoryInterface
275
    {
276
        if ($this->container instanceof FactoryInterface) {
277
            return $this->container;
278
        }
279
280
        return $this->container->get(FactoryInterface::class);
281
    }
282
283
    /**
284
     * Create valid MongoId (ObjectID now) object based on string or id provided from client side.
285
     *
286
     * @param mixed $mongoID String or MongoId object.
287
     *
288
     * @return ObjectID|null
289
     */
290
    public static function mongoID($mongoID)
291
    {
292
        if (empty($mongoID)) {
293
            return null;
294
        }
295
296
        if (!is_object($mongoID)) {
297
            //Old versions of mongo api does not throws exception on invalid mongo id (1.2.1)
298
            if (!is_string($mongoID) || !preg_match('/[0-9a-f]{24}/', $mongoID)) {
299
                return null;
300
            }
301
302
            try {
303
                $mongoID = new ObjectID($mongoID);
304
            } catch (\Exception $e) {
305
                return null;
306
            }
307
        }
308
309
        return $mongoID;
310
    }
311
}