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 = []; |
|
|
|
|
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) |
|
|
|
|
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]; |
|
|
|
|
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
|
|
|
|
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.