Passed
Push — master ( a02f46...bb45b5 )
by Paul
04:32
created

IndexMapping::getIndexAlias()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 24
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5.1576

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 24
ccs 7
cts 12
cp 0.5833
rs 9.9
c 0
b 0
f 0
cc 4
nc 4
nop 1
crap 5.1576
1
<?php
2
3
declare(strict_types=1);
4
5
namespace CCT\Component\ODMElasticsearch\Repository;
6
7
use CCT\Component\ODMElasticsearch\Metadata\ClassMetadata;
8
use CCT\Component\ODMElasticsearch\Metadata\PropertyMetadataInterface;
9
use CCT\Component\ODMElasticsearch\Repository\Exception\NoMetadataConfigException;
10
use Elastica\Client;
11
use Elastica\Type\Mapping;
12
use Metadata\ClassHierarchyMetadata;
13
use Metadata\MetadataFactory;
14
15
class IndexMapping implements IndexMappingInterface
16
{
17
    /**
18
     * Elastic search type. To be removed in future versions of elastic search
19
     *
20
     * @see https://www.elastic.co/guide/en/elasticsearch/reference/master/removal-of-types.html
21
     * @var string
22
     */
23
    protected $type = 'record';
24
25
    /**
26
     * @var Client
27
     */
28
    protected $client;
29
30
    /**
31
     * @var MetadataFactory
32
     */
33
    protected $metadataFactory;
34
35
    /**
36
     * @var bool
37
     */
38
    protected $testEnvironment;
39
40
    /**
41
     * IndexMapping constructor.
42
     *
43
     * @param Client $client
44
     * @param MetadataFactory $metadataFactory
45
     * @param bool $testEnvironment
46
     */
47 9
    public function __construct(Client $client, MetadataFactory $metadataFactory, bool $testEnvironment = false)
48
    {
49 9
        $this->client = $client;
50 9
        $this->metadataFactory = $metadataFactory;
51 9
        $this->testEnvironment = $testEnvironment;
52 9
    }
53
54
    /**
55
     * Get Elastica Index for specific entity. If index does not exists, it creates one.
56
     * If mapping is change then its updated
57
     *
58
     * @param string $entityName
59
     *
60
     * @return \CCT\Component\ODMElasticsearch\Repository\Index
61
     * @throws \Exception
62
     */
63 5
    public function getIndex(string $entityName): Index
64
    {
65
        /** @var ClassHierarchyMetadata $classMetadata */
66 5
        $metadata = $this->getMetaData($entityName);
67
68
        /** @var ClassMetadata $classMetadata */
69 4
        $classMetadata = $metadata->getRootClassMetadata();
0 ignored issues
show
Bug introduced by
The method getRootClassMetadata() does not exist on Metadata\MergeableClassMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

69
        /** @scrutinizer ignore-call */ 
70
        $classMetadata = $metadata->getRootClassMetadata();

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...
70
71 4
        if (null === $classMetadata->getIndex()) {
72 1
            throw new NoMetadataConfigException(
73 1
                sprintf('Metadata is missing index configuration for "%s"', $entityName)
74
            );
75
        }
76 3
        $indexConfig = $classMetadata->getIndex();
77
78 3
        if (true === $this->testEnvironment) {
79
            $indexConfig['name'] .= '_test';
80
        }
81
82 3
        $index = new Index($this->client, $indexConfig['name']);
83 3
        $mappingConfig = $this->extractMappingConfig($metadata);
0 ignored issues
show
Bug introduced by
$metadata of type Metadata\MergeableClassMetadata is incompatible with the type Metadata\ClassHierarchyMetadata expected by parameter $classMetadata of CCT\Component\ODMElastic...:extractMappingConfig(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

83
        $mappingConfig = $this->extractMappingConfig(/** @scrutinizer ignore-type */ $metadata);
Loading history...
84
85 3
        if (false === $index->exists()) {
86
            $this->createIndex($index, $indexConfig);
87
            $this->defineMapping($index, $mappingConfig);
88
89
            return $index;
90
        }
91
92 3
        $mappingDiff = $this->getMappingDifference($index, $mappingConfig);
93
94
        //If diff the update mapping
95 3
        if (\count($mappingDiff) > 0) {
96
            throw new \RuntimeException('System does not yet support dynamic updates to index mapping yet!');
97
        }
98
99 3
        return $index;
100
    }
101
102
    /**
103
     * @param string $entityName
104
     *
105
     * @return Index|null
106
     */
107 2
    public function getIndexAlias(string $entityName): ?Index
108
    {
109 2
        $metadata = $this->getMetaData($entityName);
110
111
        /** @var ClassMetadata $classMetadata */
112 2
        $classMetadata = $metadata->getRootClassMetadata();
113
114 2
        if (null === $classMetadata->getIndex()) {
115
            throw new NoMetadataConfigException(
116
                sprintf('Metadata is missing index configuration for "%s"', $entityName)
117
            );
118
        }
119
120 2
        $indexConfig = $classMetadata->getIndex();
121
122 2
        if (false === isset($indexConfig['alias'])) {
123 2
            return null;
124
        }
125
126
        if (true === $this->testEnvironment) {
127
            $indexConfig['alias'] .= '_test';
128
        }
129
130
        return new Index($this->client, $indexConfig['alias']);
131
    }
132
133
    /**
134
     * Creates the mapping on elastic search
135
     *
136
     * @param Index $index
137
     * @param array $mappingConfig
138
     *
139
     * @throws \Exception
140
     */
141
    protected function defineMapping(Index $index, array $mappingConfig): void
142
    {
143
        //Create a type
144
        $type = $index->getType();
145
146
        // Define mapping
147
        $mapping = new Mapping();
148
        $mapping->setType($type);
149
150
        $mapping->setProperties($mappingConfig);
151
152
        $response = $mapping->send();
153
154
        if (false === $response->isOk()) {
155
            throw new \RuntimeException($response->getErrorMessage());
156
        }
157
    }
158
159
    /**
160
     * Creates index on elastic search
161
     *
162
     * @param Index $index
163
     * @param array $indexConfig
164
     */
165
    protected function createIndex(Index $index, array $indexConfig): void
166
    {
167
        $response = $index->create($indexConfig['settings']);
168
        if (false === $response->isOk()) {
169
            throw new \RuntimeException($response->getErrorMessage());
170
        }
171
172
        if (false === isset($indexConfig['alias'])) {
173
            return;
174
        }
175
176
        $aliasResponse = $index->addAlias($indexConfig['alias']);
177
178
        if (false === $aliasResponse->isOk()) {
179
            throw new \RuntimeException($aliasResponse->getErrorMessage());
180
        }
181
    }
182
183
    /**
184
     * Gets the differences between local index mapping and elastic search mapping
185
     *
186
     * @param Index $index
187
     * @param array $mappingConfig
188
     *
189
     * @return array
190
     * @throws \Exception
191
     */
192 1
    protected function getMappingDifference(Index $index, array $mappingConfig): array
193
    {
194 1
        $mapping = $index->getMapping();
195
196 1
        $elasticMapping = $mapping[$this->type]['properties'] ?? [];
197
198 1
        return $this->getDifferenceBetweenMultiArray($mappingConfig, $elasticMapping);
199
    }
200
201
    /**
202
     * Extract mapping data for elastic search from class metadata
203
     *
204
     * @param ClassHierarchyMetadata $classMetadata
205
     *
206
     * @return array
207
     */
208 3
    public function extractMappingConfig(ClassHierarchyMetadata $classMetadata): array
209
    {
210 3
        $mappingConfig = [];
211
212 3
        foreach ($classMetadata->getRootClassMetadata()->propertyMetadata as $propertyMetadata) {
213 3
            if (!($propertyMetadata instanceof PropertyMetadataInterface)) {
214
                continue;
215
            }
216
217 3
            $index = $propertyMetadata->getFieldName() ?? $propertyMetadata->name;
0 ignored issues
show
Bug introduced by
Accessing name on the interface CCT\Component\ODMElastic...opertyMetadataInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
218
219 3
            if ('object' === $propertyMetadata->getType()) {
220 1
                $className = $propertyMetadata->getTypeClass();
221
222 1
                $subClassMetadata = $this->metadataFactory->getMetadataForClass($className);
223
224 1
                if (null === $subClassMetadata) {
225
                    throw new NoMetadataConfigException(
226
                        sprintf('No metadata config was found for sub class, "%s"', $className)
227
                    );
228
                }
229 1
                $mappingConfig[$index] = [
230 1
                    'type' => 'object',
231 1
                    'properties' => $this->extractMappingConfig($subClassMetadata)
0 ignored issues
show
Bug introduced by
$subClassMetadata of type Metadata\MergeableClassMetadata is incompatible with the type Metadata\ClassHierarchyMetadata expected by parameter $classMetadata of CCT\Component\ODMElastic...:extractMappingConfig(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

231
                    'properties' => $this->extractMappingConfig(/** @scrutinizer ignore-type */ $subClassMetadata)
Loading history...
232
                ];
233 1
                continue;
234
            }
235
236 3
            if (null === $propertyMetadata->getMapping()) {
237 3
                continue;
238
            }
239
240
            $mappingConfig[$index] = $propertyMetadata->getMapping();
241
        }
242
243 3
        return $mappingConfig;
244
    }
245
246
    /**
247
     * Returns array of differences in a multi dimensional array, otherwise an empty array.
248
     * Does not take into account ordering of indexes.
249
     *
250
     * @param $array1
251
     * @param $array2
252
     *
253
     * @return array
254
     */
255 3
    protected function getDifferenceBetweenMultiArray($array1, $array2): array
256
    {
257 3
        $difference = [];
258
259 3
        foreach ($array1 as $key => $value) {
260 2
            if ($value === 'object') {
261
                continue;
262
            }
263
264 2
            if (!array_key_exists($key, $array2)) {
265 1
                $difference[$key] = $value;
266 1
                continue;
267
            }
268
269 2
            if (\is_array($value)) {
270 2
                $arrayRecursiveDiff = $this->getDifferenceBetweenMultiArray($value, $array2[$key]);
271 2
                if (count($arrayRecursiveDiff)) {
272 2
                    $difference[$key] = $arrayRecursiveDiff;
273
                }
274 2
            } elseif ($value !== $array2[$key]) {
275 2
                $difference[$key] = $value;
276
            }
277
        }
278
279 3
        return $difference;
280
    }
281
282
    /**
283
     * @param string $entityName
284
     *
285
     * @return ClassHierarchyMetadata|\Metadata\MergeableClassMetadata|null
286
     */
287 5
    private function getMetaData(string $entityName)
288
    {
289 5
        $metadata = $this->metadataFactory->getMetadataForClass($entityName);
290
291 5
        if (null === $metadata) {
292 1
            throw new NoMetadataConfigException(
293 1
                sprintf('No metadata config was found for "%s"', $entityName)
294
            );
295
        }
296
297 4
        return $metadata;
298
    }
299
}
300