MongoDbIndexer::createIndex()   B
last analyzed

Complexity

Conditions 7
Paths 5

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
dl 0
loc 32
ccs 0
cts 13
cp 0
rs 8.4746
c 0
b 0
f 0
cc 7
nc 5
nop 3
crap 56
1
<?php
2
/**
3
 * File was created 29.02.2016 17:08
4
 */
5
6
namespace PeekAndPoke\Component\Slumber\Data\MongoDb;
7
8
use MongoDB\Collection;
9
use MongoDB\Driver\Exception;
10
use MongoDB\Model\IndexInfo;
11
use PeekAndPoke\Component\Psi\Psi;
12
use PeekAndPoke\Component\Slumber\Annotation\CompoundIndexDefinition;
13
use PeekAndPoke\Component\Slumber\Annotation\IndexDefinition;
14
use PeekAndPoke\Component\Slumber\Annotation\PropertyStorageIndexMarker;
15
use PeekAndPoke\Component\Slumber\Annotation\Slumber\AsObject;
16
use PeekAndPoke\Component\Slumber\Annotation\Slumber\Store\AsDbReference;
17
use PeekAndPoke\Component\Slumber\Core\LookUp\PropertyMarkedForSlumber;
18
use PeekAndPoke\Component\Slumber\Data\LookUp\PropertyMarkedForIndexing;
19
use Psr\Log\LoggerInterface;
20
21
/**
22
 * @author Karsten J. Gerber <[email protected]>
23
 */
24
class MongoDbIndexer
25
{
26
    /** @var MongoDbEntityConfigReader */
27
    private $lookUp;
28
    /** @var LoggerInterface */
29
    private $logger;
30
31
    /**
32
     * MongoDbIndexer constructor.
33
     *
34
     * @param MongoDbEntityConfigReader $lookUp
35
     * @param LoggerInterface           $logger
36
     */
37
    public function __construct(MongoDbEntityConfigReader $lookUp, LoggerInterface $logger)
38
    {
39
        $this->lookUp = $lookUp;
40
        $this->logger = $logger;
41
    }
42
43
    /**
44
     * @param Collection       $collection
45
     * @param \ReflectionClass $class
46
     *
47
     * @return array
48
     */
49
    public function ensureIndexes(Collection $collection, \ReflectionClass $class)
50
    {
51
        $createdIndexes = $this->ensureIndexesRecursive([], $class, $collection);
52
53
        // delete all indexes that are no longer needed
54
        $indexInfos     = $collection->listIndexes();
55
        $deletedIndexes = [];
56
57
        /** @var IndexInfo $indexInfo */
58
        foreach ($indexInfos as $indexInfo) {
59
60
            $indexInfoName = $indexInfo->getName();
61
62
            if ($indexInfoName !== '_id_' && ! \in_array($indexInfoName, $createdIndexes, true)) {
63
                $collection->dropIndex($indexInfoName);
64
                $deletedIndexes[] = $indexInfoName;
65
            }
66
        }
67
68
        $this->logger->debug(
69
            'Creating indexes for repository ' . $collection->getCollectionName() . ' - ' .
70
            'Ensured indexes: ' . implode(', ', $createdIndexes) . ' - ' .
71
            'Deleted indexes: ' . implode(', ', $deletedIndexes)
72
        );
73
74
        return Psi::it($collection->listIndexes())->toArray();
75
    }
76
77
    /**
78
     * @param string[]         $prefixes
79
     * @param \ReflectionClass $entity
80
     * @param Collection       $collection
81
     *
82
     * @return array
83
     */
84
    private function ensureIndexesRecursive($prefixes, \ReflectionClass $entity, Collection $collection)
85
    {
86
        $entityConfig = $this->lookUp->getEntityConfig($entity);
87
88
        $createdIndexes = array_merge(
89
            $this->createCompoundIndexes($prefixes, $collection, $entityConfig->getCompoundIndexes()),
90
            $this->createPropertyIndexes($prefixes, $collection, $entityConfig->getIndexedProperties())
91
        );
92
93
        // also index child objects
94
        Psi::it($entityConfig->getMarkedProperties())
95
            ->filter(new Psi\IsInstanceOf(PropertyMarkedForSlumber::class))
96
            // we also look into child objects
97
            ->filter(function (PropertyMarkedForSlumber $p) {
98
                return $p->getFirstMarkerOf(AsObject::class) !== null;
99
            })
100
            // but NOT if they are db-references
101
            ->filter(function (PropertyMarkedForSlumber $p) {
102
                return $p->getFirstMarkerOf(AsDbReference::class) === null;
103
            })
104
            ->each(function (PropertyMarkedForSlumber $p) use ($prefixes, $collection, &$createdIndexes) {
105
                /** @var AsObject $asObject */
106
                $asObject = $p->getFirstMarkerOf(AsObject::class);
107
108
                /** @noinspection NullPointerExceptionInspection */
109
                $subCreated = $this->ensureIndexesRecursive(
110
                    array_merge($prefixes, [$p->alias]),
111
                    new \ReflectionClass($asObject->value),
112
                    $collection
113
                );
114
115
                $createdIndexes = array_merge($createdIndexes, $subCreated);
116
            })->toArray();
117
118
        return $createdIndexes;
119
    }
120
121
    /**
122
     * @param string[]                  $prefixes
123
     * @param Collection                $collection
124
     * @param CompoundIndexDefinition[] $compoundIndexes
125
     *
126
     * @return string[] The names of all indexes that where created
127
     */
128
    private function createCompoundIndexes($prefixes, Collection $collection, $compoundIndexes)
129
    {
130
        $createdIndexes = [];
131
132
        foreach ($compoundIndexes as $compoundIndex) {
133
134
            $definition = [];
135
            // append the prefixes to all fields
136
            foreach ($compoundIndex->getDefinition() as $k => $v) {
137
                $definition[$this->buildFieldName($prefixes, $k)] = $v;
138
            }
139
140
            $name    = $this->buildCompoundIndexName($prefixes, $definition);
141
            $options = $this->assembleOptions($name, $compoundIndex);
142
143
            $createdIndexes[] = $this->createIndex($collection, $definition, $options);
144
        }
145
146
        return $createdIndexes;
147
    }
148
149
    /**
150
     * @param string[]                    $prefixes
151
     * @param Collection                  $collection
152
     * @param PropertyMarkedForIndexing[] $indexedProperties
153
     *
154
     * @return string[] The names of all indexes that where created
155
     */
156
    private function createPropertyIndexes($prefixes, Collection $collection, $indexedProperties)
157
    {
158
        $createdIndexes = [];
159
160
        foreach ($indexedProperties as $indexedProperty) {
161
162
            foreach ($indexedProperty->markers as $marker) {
163
164
                $definition = [
165
                    // append the prefixes to the field
166
                    $this->buildFieldName($prefixes, $indexedProperty->propertyName) => $this->mapDirection($marker),
167
                ];
168
                $name       = $this->buildPropertyIndexName($prefixes, $indexedProperty->propertyName, $marker);
169
                $options    = $this->assembleOptions($name, $marker);
170
171
                $createdIndexes[] = $this->createIndex($collection, $definition, $options);
172
            }
173
        }
174
175
        return $createdIndexes;
176
    }
177
178
    /**
179
     * @param Collection $collection
180
     * @param array      $fields
181
     * @param array      $options
182
     *
183
     * @return string The resulting index names
184
     */
185
    private function createIndex(Collection $collection, array $fields, array $options)
186
    {
187
        $debugInfo = 'collection "' . $collection->getCollectionName() . '" for fields: ' . json_encode($fields) . ' - Options: ' . json_encode($options);
188
189
        try {
190
            $collection->createIndex($fields, $options);
191
        } catch (Exception\RuntimeException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\RuntimeException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
192
193
            // Do we have a duplicate key problem?
194
            if ($e->getCode() === 11000 || strpos($e->getMessage(), 'E11000') !== false) {
195
                throw new \RuntimeException('Duplicate key problems on ' . $debugInfo, 0, $e);
196
            }
197
198
            // Have the options changes? So we try to drop the index and then create it again
199
            if ($e->getCode() === 85 || strpos($e->getMessage(), 'already exists with different options') === false) {
200
                // drop index by name
201
                $collection->dropIndex($options['name']);
202
203
                // and try the creation again
204
                try {
205
                    $collection->createIndex($fields, $options);
206
                } catch (Exception\RuntimeException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\RuntimeException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
207
                    throw new \RuntimeException('Cannot create index - even after dropping it - on ' . $debugInfo, 0, $e);
208
                }
209
            }
210
211
            // something else has happened
212
            throw new \RuntimeException('Unknown problem on ' . $debugInfo, 0, $e);
213
        }
214
215
        return $options['name'];
216
    }
217
218
    /**
219
     * @param string          $indexName
220
     * @param IndexDefinition $marker
221
     *
222
     * @return array
223
     */
224
    private function assembleOptions($indexName, IndexDefinition $marker)
225
    {
226
        $options = [
227
            'name'       => (string) $indexName,
228
            'background' => (bool) $marker->isBackground(),
229
            'unique'     => (bool) $marker->isUnique(),
230
            'dropDups'   => (bool) $marker->isDropDups(),
231
            'sparse'     => (bool) $marker->isSparse(),
232
        ];
233
234
        if ($marker->getExpireAfterSeconds() >= 0) {
235
            $options['expireAfterSeconds'] = $marker->getExpireAfterSeconds();
236
        }
237
238
        return $options;
239
    }
240
241
    /**
242
     * @param string[] $prefixes
243
     * @param string   $fieldName
244
     *
245
     * @return string
246
     */
247
    private function buildFieldName($prefixes, $fieldName)
248
    {
249
        if (\count($prefixes) === 0) {
250
            return $fieldName;
251
        }
252
253
        return implode('.', $prefixes) . '.' . $fieldName;
254
    }
255
256
    /**
257
     * @param string[]        $prefixes
258
     * @param string          $propertyName
259
     * @param IndexDefinition $marker
260
     *
261
     * @return string
262
     */
263
    private function buildPropertyIndexName($prefixes, $propertyName, IndexDefinition $marker)
264
    {
265
        // is the name overridden by the user ?
266
        if (! empty($marker->getName())) {
267
            $rest = $marker->getName();
268
        } else {
269
            $rest = $propertyName . '_' . $this->mapDirection($marker);
270
        }
271
272
        if (\count($prefixes) > 0) {
273
            return implode('.', $prefixes) . '.' . $rest;
274
        }
275
276
        return $rest;
277
    }
278
279
    /**
280
     * @param string[] $prefixes
281
     * @param array    $definition
282
     *
283
     * @return string
284
     */
285
    private function buildCompoundIndexName($prefixes, $definition)
286
    {
287
        $parts = [];
288
289
        foreach ($definition as $k => $v) {
290
            $parts[] = ((string) $k) . '_' . ((string) $v);
291
        }
292
293
        $rest = implode('_', $parts);
294
295
        if (\count($prefixes) > 0) {
296
            return implode('.', $prefixes) . '.' . $rest;
297
        }
298
299
        return $rest;
300
    }
301
302
    /**
303
     * @param IndexDefinition $marker
304
     *
305
     * @return int
306
     */
307
    private function mapDirection(IndexDefinition $marker)
308
    {
309
        return $marker->getDirection() === PropertyStorageIndexMarker::ASCENDING ? 1 : -1;
310
    }
311
}
312