Completed
Pull Request — 5.0 (#720)
by
unknown
02:03
created

MetadataCollector::extractAnalysisFromProperties()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 17
rs 9.2
cc 4
eloc 10
nc 5
nop 2
1
<?php
2
3
/*
4
 * This file is part of the ONGR package.
5
 *
6
 * (c) NFQ Technologies UAB <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace ONGR\ElasticsearchBundle\Mapping;
13
14
use Doctrine\Common\Cache\CacheProvider;
15
use ONGR\ElasticsearchBundle\Mapping\Exception\DocumentParserException;
16
use ONGR\ElasticsearchBundle\Mapping\Exception\MissingDocumentAnnotationException;
17
18
/**
19
 * DocumentParser wrapper for getting bundle documents mapping.
20
 */
21
class MetadataCollector
22
{
23
    /**
24
     * @var DocumentFinder
25
     */
26
    private $finder;
27
28
    /**
29
     * @var DocumentParser
30
     */
31
    private $parser;
32
33
    /**
34
     * @var CacheProvider
35
     */
36
    private $cache = null;
37
38
    /**
39
     * @var bool
40
     */
41
    private $enableCache = false;
42
43
    /**
44
     * @var array
45
     */
46
    private $analysisConfiguration;
47
48
    /**
49
     * Bundles mappings local cache container. Could be stored as the whole bundle or as single document.
50
     * e.g. AcmeDemoBundle, AcmeDemoBundle:Product.
51
     *
52
     * @var mixed
53
     */
54
    private $mappings = [];
55
56
    /**
57
     * @param DocumentFinder $finder For finding documents.
58
     * @param DocumentParser $parser For reading document annotations.
59
     * @param CacheProvider  $cache  Cache provider to store the meta data for later use.
60
     */
61
    public function __construct($finder, $parser, $cache = null)
62
    {
63
        $this->finder = $finder;
64
        $this->parser = $parser;
65
        $this->cache = $cache;
66
67
        if ($this->cache) {
68
            $this->mappings = $this->cache->fetch('ongr.metadata.mappings');
69
        }
70
    }
71
72
    /**
73
     * Enables metadata caching.
74
     *
75
     * @param bool $enableCache
76
     */
77
    public function setEnableCache($enableCache)
78
    {
79
        $this->enableCache = $enableCache;
80
    }
81
82
    /**
83
     * @param array $config
84
     */
85
    public function setAnalysisConfiguration(array $config)
86
    {
87
        $this->analysisConfiguration = $config;
88
    }
89
90
    /**
91
     * Fetches bundles mapping from documents.
92
     *
93
     * @param string[] $bundles Elasticsearch manager config. You can get bundles list from 'mappings' node.
94
     * @return array
95
     */
96
    public function getMappings(array $bundles)
97
    {
98
        $output = [];
99
        foreach ($bundles as $bundle) {
100
            $mappings = $this->getBundleMapping($bundle);
101
102
            $alreadyDefinedTypes = array_intersect_key($mappings, $output);
103
            if (count($alreadyDefinedTypes)) {
104
                throw new \LogicException(
105
                    implode(',', array_keys($alreadyDefinedTypes)) .
106
                    ' type(s) already defined in other document, you can use the same ' .
107
                    'type only once in a manager definition.'
108
                );
109
            }
110
111
            $output = array_merge($output, $mappings);
112
        }
113
114
        return $output;
115
    }
116
117
    /**
118
     * Searches for documents in the bundle and tries to read them.
119
     *
120
     * @param string $name
121
     *
122
     * @return array Empty array on containing zero documents.
123
     */
124
    public function getBundleMapping($name)
125
    {
126
        if (!is_string($name)) {
127
            throw new \LogicException('getBundleMapping() in the Metadata collector expects a string argument only!');
128
        }
129
130
        if (isset($this->mappings[$name])) {
131
            return $this->mappings[$name];
132
        }
133
134
        // Handle the case when single document mapping requested
135
        if (strpos($name, ':') !== false) {
136
            list($bundle, $documentClass) = explode(':', $name);
137
            $documents = $this->finder->getBundleDocumentClasses($bundle);
138
            $documents = in_array($documentClass, $documents) ? [$documentClass] : [];
139
        } else {
140
            $documents = $this->finder->getBundleDocumentClasses($name);
141
            $bundle = $name;
142
        }
143
144
        $mappings = [];
145
        $bundleNamespace = $this->finder->getBundleClass($bundle);
146
        $bundleNamespace = substr($bundleNamespace, 0, strrpos($bundleNamespace, '\\'));
147
148
        if (!count($documents)) {
149
            return [];
150
        }
151
152
        // Loop through documents found in bundle.
153
        foreach ($documents as $document) {
154
            $documentReflection = new \ReflectionClass(
155
                $bundleNamespace .
156
                '\\' . DocumentFinder::DOCUMENT_DIR .
157
                '\\' . $document
158
            );
159
160
            try {
161
                $documentMapping = $this->getDocumentReflectionMapping($documentReflection);
162
            } catch (MissingDocumentAnnotationException $exception) {
163
                // Not a document, just ignore
164
                continue;
165
            }
166
167
            if (!array_key_exists($documentMapping['type'], $mappings)) {
168
                $documentMapping['bundle'] = $bundle;
169
                $mappings = array_merge($mappings, [$documentMapping['type'] => $documentMapping]);
170
            } else {
171
                throw new \LogicException(
172
                    $bundle . ' has 2 same type names defined in the documents. ' .
173
                    'Type names must be unique!'
174
                );
175
            }
176
        }
177
178
        $this->cacheBundle($name, $mappings);
179
180
        return $mappings;
181
    }
182
183
    /**
184
     * @param array $manager
185
     *
186
     * @return array
187
     */
188
    public function getManagerTypes($manager)
189
    {
190
        $mapping = $this->getMappings($manager['mappings']);
191
192
        return array_keys($mapping);
193
    }
194
195
    /**
196
     * Resolves Elasticsearch type by document class.
197
     *
198
     * @param string $className FQCN or string in AppBundle:Document format
199
     *
200
     * @return string
201
     */
202
    public function getDocumentType($className)
203
    {
204
        $mapping = $this->getMapping($className);
205
206
        return $mapping['type'];
207
    }
208
209
    /**
210
     * Retrieves prepared mapping to sent to the elasticsearch client.
211
     *
212
     * @param array $bundles Manager config.
213
     *
214
     * @return array|null
215
     */
216
    public function getClientMapping(array $bundles)
217
    {
218
        /** @var array $typesMapping Array of filtered mappings for the elasticsearch client*/
219
        $typesMapping = null;
220
221
        /** @var array $mappings All mapping info */
222
        $mappings = $this->getMappings($bundles);
223
224
        foreach ($mappings as $type => $mapping) {
225
            if (!empty($mapping['properties'])) {
226
                $typesMapping[$type] = array_filter(
227
                    array_merge(
228
                        ['properties' => $mapping['properties']],
229
                        $mapping['fields']
230
                    ),
231
                    function ($value) {
232
                        return (bool)$value || is_bool($value);
233
                    }
234
                );
235
            }
236
        }
237
238
        return $typesMapping;
239
    }
240
241
    /**
242
     * Gets the analysis configuration from the documents of the specific bundles
243
     *
244
     * @param string $manager Manager name
245
     * @param array  $bundles
246
     *
247
     * @return array
248
     */
249
    public function getManagerAnalysis($manager, array $bundles)
250
    {
251
        if ($this->cache) {
252
            $cacheAnalysis = $this->cache->fetch('ongr.metadata.analysis');
253
254
            if (isset($cacheAnalysis[$manager])) {
255
                return $cacheAnalysis[$manager];
256
            }
257
        }
258
259
        $managerAnalysis = [];
260
        $mappings = $this->getClientMapping($bundles);
261
262
        foreach ($mappings as $type) {
0 ignored issues
show
Bug introduced by
The expression $mappings of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
263
            $this->extractAnalysisFromProperties($type['properties'], $managerAnalysis);
264
        }
265
266
        if ($this->enableCache) {
267
            $cacheAnalysis[$manager] = array_filter($managerAnalysis);
0 ignored issues
show
Bug introduced by
The variable $cacheAnalysis does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
268
            $this->cache->save('ongr.metadata.analysis', $cacheAnalysis);
269
        }
270
271
        return array_filter($managerAnalysis);
272
    }
273
274
    /**
275
     * Gathers annotation data from class.
276
     *
277
     * @param \ReflectionClass $reflectionClass Document reflection class to read mapping from.
278
     *
279
     * @return array
280
     * @throws DocumentParserException
281
     */
282
    private function getDocumentReflectionMapping(\ReflectionClass $reflectionClass)
283
    {
284
        return $this->parser->parse($reflectionClass);
285
    }
286
287
    /**
288
     * Returns single document mapping metadata.
289
     *
290
     * @param string $namespace Document namespace
291
     *
292
     * @return array
293
     * @throws DocumentParserException
294
     */
295
    public function getMapping($namespace)
296
    {
297
        $namespace = $this->getClassName($namespace);
298
299
        if (isset($this->mappings[$namespace])) {
300
            return $this->mappings[$namespace];
301
        }
302
303
        $mapping = $this->getDocumentReflectionMapping(new \ReflectionClass($namespace));
304
        $this->cacheBundle($namespace, $mapping);
305
306
        return $mapping;
307
    }
308
309
    /**
310
     * Adds metadata information to the cache storage.
311
     *
312
     * @param string $bundle
313
     * @param array  $mapping
314
     */
315
    private function cacheBundle($bundle, array $mapping)
316
    {
317
        if ($this->enableCache) {
318
            $this->mappings[$bundle] = $mapping;
319
            $this->cache->save('ongr.metadata.mappings', $this->mappings);
320
        }
321
    }
322
323
    /**
324
     * Returns fully qualified class name.
325
     *
326
     * @param string $className
327
     *
328
     * @return string
329
     */
330
    public function getClassName($className)
331
    {
332
        return $this->finder->getNamespace($className);
333
    }
334
335
    /**
336
     * Extracts analysis configuration from all the documents
337
     *
338
     * @param array $properties      Properties of a type or an object
339
     * @param array $managerAnalysis The data that is being formed for the manager
340
     */
341
    private function extractAnalysisFromProperties($properties, &$managerAnalysis)
342
    {
343
        foreach ($properties as $property) {
344
            if (isset($property['analyzer'])) {
345
                $analyzer = $this->analysisConfiguration['analyzer'][$property['analyzer']];
346
                $managerAnalysis['analyzer'][$property['analyzer']] = $analyzer;
347
348
                $this->extractSubData('filter', $analyzer, $managerAnalysis);
349
                $this->extractSubData('char_filter', $analyzer, $managerAnalysis);
350
                $this->extractSubData('tokenizer', $analyzer, $managerAnalysis);
351
            }
352
353
            if (isset($property['properties'])) {
354
                $this->extractAnalysisFromProperties($property['properties'], $managerAnalysis);
355
            }
356
        }
357
    }
358
359
    /**
360
     * Extracts tokenizers and filters from analysis configuration
361
     *
362
     * @param string $type     Either filter or tokenizer
363
     * @param array  $analyzer The current analyzer
364
     * @param array  $data     The data that is being formed for the manager
365
     */
366
    private function extractSubData($type, $analyzer, &$data)
367
    {
368
        if (!isset($analyzer[$type])) {
369
            return;
370
        }
371
372
        if (is_array($analyzer[$type])) {
373
            foreach ($analyzer[$type] as $name) {
374 View Code Duplication
                if (isset($this->analysisConfiguration[$type][$name])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
375
                    $data[$type][$name] = $this->analysisConfiguration[$type][$name];
376
                }
377
            }
378
        } else {
379
            $name = $analyzer[$type];
380
381 View Code Duplication
            if (isset($this->analysisConfiguration[$type][$name])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
382
                $data[$type][$name] = $this->analysisConfiguration[$type][$name];
383
            }
384
        }
385
    }
386
}
387