Completed
Pull Request — 5.0 (#724)
by Simonas
04:21
created

MetadataCollector::getMapping()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
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\Exception\DocumentParserException;
16
use ONGR\ElasticsearchBundle\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
     * Bundles mappings local cache container.
45
     *
46
     * @var mixed
47
     */
48
//    private $mappings = [];
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
49
50
    /**
51
     * @param DocumentFinder $finder For finding documents.
52
     * @param DocumentParser $parser For reading document annotations.
53
     * @param CacheProvider  $cache  Cache provider to store the meta data for later use.
54
     */
55
    public function __construct($finder, $parser, $cache = null)
56
    {
57
        $this->finder = $finder;
58
        $this->parser = $parser;
59
        $this->cache = $cache;
60
    }
61
62
    /**
63
     * Enables metadata caching.
64
     *
65
     * @param bool $enableCache
66
     */
67
    public function setEnableCache($enableCache)
68
    {
69
        $this->enableCache = $enableCache;
70
    }
71
72
//    public function fetchMappingFromCache($name)
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
73
//    {
74
//        if ($this->enableCache) {
75
//            $this->mappings[$name] = $this->cache->fetch($name);
76
//        }
77
//
78
//        if (isset($this->mappings[$name])) {
79
//            return $this->mappings[$name];
80
//        }
81
//
82
//        return null;
83
//    }
84
//
85
//    public function saveMappingToCache($name, $value)
86
//    {
87
//        if ($this->enableCache) {
88
//            $this->cache->save($name, $value);
89
//        }
90
//
91
//        $this->mappings[$name] = $value;
92
//    }
93
94
    /**
95
     * Fetches bundles mapping from documents.
96
     *
97
     * @param string[] $bundles Elasticsearch manager config. You can get bundles list from 'mappings' node.
98
     * @return array
99
     */
100
    public function getMappings(array $bundles)
101
    {
102
        $output = [];
103
        foreach ($bundles as $name => $bundleConfig) {
104
            // Backward compatibility hack for support.
105
            if (!is_array($bundleConfig)) {
106
                $name = $bundleConfig;
107
                $bundleConfig = [];
108
            }
109
            $mappings = $this->getBundleMapping($name, $bundleConfig);
110
111
            $alreadyDefinedTypes = array_intersect_key($mappings, $output);
112
            if (count($alreadyDefinedTypes)) {
113
                throw new \LogicException(
114
                    implode(',', array_keys($alreadyDefinedTypes)) .
115
                    ' type(s) already defined in other document, you can use the same ' .
116
                    'type only once in a manager definition.'
117
                );
118
            }
119
120
            $output = array_merge($output, $mappings);
121
        }
122
123
        return $output;
124
    }
125
126
    /**
127
     * Searches for documents in the bundle and tries to read them.
128
     *
129
     * @param string $name
130
     * @param array $config Bundle configuration
131
     *
132
     * @return array Empty array on containing zero documents.
133
     */
134
    public function getBundleMapping($name, $config = [])
135
    {
136
        if (!is_string($name)) {
137
            throw new \LogicException('getBundleMapping() in the Metadata collector expects a string argument only!');
138
        }
139
140
        $cacheName =  'ongr.metadata.bundle.' . md5($name.serialize($config));
141
142
        $this->enableCache && $mappings = $this->cache->contains($cacheName);
143
144
        if (isset($mappings)) {
145
            return $mappings;
146
        }
147
148
        $mappings = [];
149
        $documentDir = isset($config['document_dir']) ? $config['document_dir'] : 'Document';
150
151
        // Handle the case when single document mapping requested
152
        // Usage od ":" in name is deprecated. This if is only for BC.
153
        if (strpos($name, ':') !== false) {
154
            list($bundle, $documentClass) = explode(':', $name);
155
            $documents = $this->finder->getBundleDocumentClasses($bundle);
156
            $documents = in_array($documentClass, $documents) ? [$documentClass] : [];
157
        } else {
158
            $documents = $this->finder->getBundleDocumentClasses($name, $documentDir);
159
            $bundle = $name;
160
        }
161
162
        $bundleNamespace = $this->finder->getBundleClass($bundle);
163
        $bundleNamespace = substr($bundleNamespace, 0, strrpos($bundleNamespace, '\\'));
164
165
        if (!count($documents)) {
166
            return [];
167
        }
168
169
        // Loop through documents found in bundle.
170
        foreach ($documents as $document) {
171
            $documentReflection = new \ReflectionClass(
172
                $bundleNamespace .
173
                '\\' . DocumentFinder::DOCUMENT_DIR .
174
                '\\' . $document
175
            );
176
177
            try {
178
                $documentMapping = $this->getDocumentReflectionMapping($documentReflection);
179
            } catch (MissingDocumentAnnotationException $exception) {
180
                // Not a document, just ignore
181
                continue;
182
            }
183
184
            if (!array_key_exists($documentMapping['type'], $mappings)) {
185
                $documentMapping['bundle'] = $bundle;
186
                $mappings = array_merge($mappings, [$documentMapping['type'] => $documentMapping]);
187
            } else {
188
                throw new \LogicException(
189
                    $bundle . ' has 2 same type names defined in the documents. ' .
190
                    'Type names must be unique!'
191
                );
192
            }
193
        }
194
195
        $this->enableCache && $this->cache->save($cacheName, $mappings);
196
197
        return $mappings;
198
    }
199
200
    /**
201
     * @param array $manager
202
     *
203
     * @return array
204
     */
205
    public function getManagerTypes($manager)
206
    {
207
        $mapping = $this->getMappings($manager['mappings']);
208
209
        return array_keys($mapping);
210
    }
211
212
    /**
213
     * Resolves Elasticsearch type by document class.
214
     *
215
     * @param string $className FQCN or string in AppBundle:Document format
216
     *
217
     * @return string
218
     */
219
    public function getDocumentType($className)
220
    {
221
        $mapping = $this->getMapping($className);
222
223
        return $mapping['type'];
224
    }
225
226
    /**
227
     * Retrieves prepared mapping to sent to the elasticsearch client.
228
     *
229
     * @param array $bundles Manager config.
230
     *
231
     * @return array|null
232
     */
233
    public function getClientMapping(array $bundles)
234
    {
235
        /** @var array $typesMapping Array of filtered mappings for the elasticsearch client*/
236
        $typesMapping = null;
237
238
        /** @var array $mappings All mapping info */
239
        $mappings = $this->getMappings($bundles);
240
241
        foreach ($mappings as $type => $mapping) {
242
            if (!empty($mapping['properties'])) {
243
                $typesMapping[$type] = array_filter(
244
                    array_merge(
245
                        ['properties' => $mapping['properties']],
246
                        $mapping['fields']
247
                    ),
248
                    function ($value) {
249
                        return (bool)$value || is_bool($value);
250
                    }
251
                );
252
            }
253
        }
254
255
        return $typesMapping;
256
    }
257
258
    /**
259
     * Prepares analysis node for Elasticsearch client.
260
     *
261
     * @param array $bundles
262
     * @param array $analysisConfig
263
     *
264
     * @return array
265
     */
266
    public function getClientAnalysis(array $bundles, $analysisConfig = [])
267
    {
268
        $cacheName = 'ongr.metadata.analysis.'.md5(serialize($bundles));
269
        $typesAnalysis = $this->cache->fetch($cacheName);
270
271
        if ($typesAnalysis) {
272
            return $typesAnalysis;
273
        }
274
275
        $typesAnalysis = [
276
            'analyzer' => [],
277
            'filter' => [],
278
            'tokenizer' => [],
279
            'char_filter' => [],
280
        ];
281
282
        /** @var array $mappings All mapping info */
283
        $mappings = $this->getMappings($bundles);
284
285
        foreach ($mappings as $type => $metadata) {
286
            foreach ($metadata['analyzers'] as $analyzerName) {
287
                if (isset($analysisConfig['analyzer'][$analyzerName])) {
288
                    $analyzer = $analysisConfig['analyzer'][$analyzerName];
289
                    $typesAnalysis['analyzer'][$analyzerName] = $analyzer;
290
                    $typesAnalysis['filter'] = $this->getAnalysisNodeConfiguration(
291
                        'filter',
292
                        $analyzer,
293
                        $analysisConfig,
294
                        $typesAnalysis['filter']
295
                    );
296
                    $typesAnalysis['tokenizer'] = $this->getAnalysisNodeConfiguration(
297
                        'tokenizer',
298
                        $analyzer,
299
                        $analysisConfig,
300
                        $typesAnalysis['tokenizer']
301
                    );
302
                    $typesAnalysis['char_filter'] = $this->getAnalysisNodeConfiguration(
303
                        'char_filter',
304
                        $analyzer,
305
                        $analysisConfig,
306
                        $typesAnalysis['char_filter']
307
                    );
308
                }
309
            }
310
        }
311
312
        $this->cacheBundle($cacheName, $typesAnalysis);
313
314
        return $typesAnalysis;
315
    }
316
317
    /**
318
     * Prepares analysis node content for Elasticsearch client.
319
     *
320
     * @param string $type Node type: filter, tokenizer or char_filter
321
     * @param array $analyzer Analyzer from which used helpers will be extracted.
322
     * @param array $analysisConfig Pre configured analyzers container
323
     * @param array $container Current analysis container where prepared helpers will be appended.
324
     *
325
     * @return array
326
     */
327
    private function getAnalysisNodeConfiguration($type, $analyzer, $analysisConfig, $container = [])
328
    {
329
        if (isset($analyzer[$type])) {
330
            if (is_array($analyzer[$type])) {
331
                foreach ($analyzer[$type] as $filter) {
332
                    if (isset($analysisConfig[$type][$filter])) {
333
                        $container[$filter] = $analysisConfig[$type][$filter];
334
                    }
335
                }
336
            } else {
337
                if (isset($analysisConfig[$type][$analyzer[$type]])) {
338
                    $container[$analyzer[$type]] = $analysisConfig[$type][$analyzer[$type]];
339
                }
340
            }
341
        }
342
        return $container;
343
    }
344
345
    /**
346
     * Gathers annotation data from class.
347
     *
348
     * @param \ReflectionClass $reflectionClass Document reflection class to read mapping from.
349
     *
350
     * @return array
351
     * @throws DocumentParserException
352
     */
353
    private function getDocumentReflectionMapping(\ReflectionClass $reflectionClass)
354
    {
355
        return $this->parser->parse($reflectionClass);
356
    }
357
358
    /**
359
     * Returns single document mapping metadata.
360
     *
361
     * @param string $namespace Document namespace
362
     *
363
     * @return array
364
     * @throws DocumentParserException
365
     */
366
    public function getMapping($namespace)
367
    {
368
        $namespace = $this->getClassName($namespace);
369
370
        if (isset($this->mappings[$namespace])) {
371
            return $this->mappings[$namespace];
0 ignored issues
show
Bug introduced by
The property mappings does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
372
        }
373
374
        $mapping = $this->getDocumentReflectionMapping(new \ReflectionClass($namespace));
375
        $this->cacheBundle($namespace, $mapping);
376
377
        return $mapping;
378
    }
379
380
    /**
381
     * Adds metadata information to the cache storage.
382
     *
383
     * @param string $bundle
384
     * @param array  $mapping
385
     */
386
    private function cacheBundle($bundle, array $mapping)
387
    {
388
        if ($this->enableCache) {
389
            $this->mappings[$bundle] = $mapping;
390
            $this->cache->save('ongr.metadata.mappings', $this->mappings);
391
        }
392
    }
393
394
    /**
395
     * Returns fully qualified class name.
396
     *
397
     * @param string $className
398
     *
399
     * @return string
400
     */
401
    public function getClassName($className)
402
    {
403
        return $this->finder->getNamespace($className);
404
    }
405
}
406