Completed
Push — 5.0 ( 8fcd56...90c505 )
by Simonas
02:21
created

MetadataCollector::getDocumentReflectionMapping()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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