Completed
Push — master ( 3d8a14...cb8d49 )
by Anton
02:04
created

ODM::buildSchema()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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