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\Annotations\AnnotationRegistry; |
15
|
|
|
use Doctrine\Common\Annotations\Reader; |
16
|
|
|
use Doctrine\Common\Cache\Cache; |
17
|
|
|
use ONGR\ElasticsearchBundle\Annotation\AbstractAnnotation; |
18
|
|
|
use ONGR\ElasticsearchBundle\Annotation\Embedded; |
19
|
|
|
use ONGR\ElasticsearchBundle\Annotation\Id; |
20
|
|
|
use ONGR\ElasticsearchBundle\Annotation\Index; |
21
|
|
|
use ONGR\ElasticsearchBundle\Annotation\NestedType; |
22
|
|
|
use ONGR\ElasticsearchBundle\Annotation\ObjectType; |
23
|
|
|
use ONGR\ElasticsearchBundle\Annotation\PropertiesAwareInterface; |
24
|
|
|
use ONGR\ElasticsearchBundle\Annotation\Property; |
25
|
|
|
use ONGR\ElasticsearchBundle\DependencyInjection\Configuration; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Document parser used for reading document annotations. |
29
|
|
|
*/ |
30
|
|
|
class DocumentParser |
31
|
|
|
{ |
32
|
|
|
const OBJ_CACHED_FIELDS = 'ongr.obj_fields'; |
33
|
|
|
const EMBEDDED_CACHED_FIELDS = 'ongr.embedded_fields'; |
34
|
|
|
const ARRAY_CACHED_FIELDS = 'ongr.array_fields'; |
35
|
|
|
|
36
|
|
|
private $reader; |
37
|
|
|
private $properties = []; |
38
|
|
|
private $analysisConfig = []; |
39
|
|
|
private $cache; |
40
|
|
|
|
41
|
|
|
public function __construct(Reader $reader, Cache $cache, array $analysisConfig = []) |
42
|
|
|
{ |
43
|
|
|
$this->reader = $reader; |
44
|
|
|
$this->cache = $cache; |
45
|
|
|
$this->analysisConfig = $analysisConfig; |
46
|
|
|
|
47
|
|
|
#Fix for annotations loader until doctrine/annotations 2.0 will be released with the full autoload support. |
48
|
|
|
AnnotationRegistry::registerLoader('class_exists'); |
|
|
|
|
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
public function getIndexAliasName(\ReflectionClass $class): string |
52
|
|
|
{ |
53
|
|
|
/** @var Index $document */ |
54
|
|
|
$document = $this->reader->getClassAnnotation($class, Index::class); |
55
|
|
|
|
56
|
|
|
return $document->alias ?? Caser::snake($class->getShortName()); |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
public function isDefaultIndex(\ReflectionClass $class): bool |
60
|
|
|
{ |
61
|
|
|
/** @var Index $document */ |
62
|
|
|
$document = $this->reader->getClassAnnotation($class, Index::class); |
63
|
|
|
|
64
|
|
|
return $document->default; |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
public function getIndexAnnotation(\ReflectionClass $class) |
68
|
|
|
{ |
69
|
|
|
/** @var Index $document */ |
70
|
|
|
$document = $this->reader->getClassAnnotation($class, Index::class); |
71
|
|
|
|
72
|
|
|
return $document; |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* @deprecated will be deleted in v7. Types are deleted from elasticsearch. |
77
|
|
|
*/ |
78
|
|
|
public function getTypeName(\ReflectionClass $class): string |
79
|
|
|
{ |
80
|
|
|
/** @var Index $document */ |
81
|
|
|
$document = $this->reader->getClassAnnotation($class, Index::class); |
82
|
|
|
|
83
|
|
|
return $document->typeName ?? '_doc'; |
|
|
|
|
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
public function getIndexMetadata(\ReflectionClass $class): array |
87
|
|
|
{ |
88
|
|
|
if ($class->isTrait()) { |
89
|
|
|
return []; |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** @var Index $document */ |
93
|
|
|
$document = $this->reader->getClassAnnotation($class, Index::class); |
94
|
|
|
|
95
|
|
|
if ($document === null) { |
96
|
|
|
return []; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
$settings = $document->getSettings(); |
100
|
|
|
$settings['analysis'] = $this->getAnalysisConfig($class); |
101
|
|
|
|
102
|
|
|
return array_filter(array_map('array_filter', [ |
103
|
|
|
'settings' => $settings, |
104
|
|
|
'mappings' => [ |
105
|
|
|
$this->getTypeName($class) => [ |
|
|
|
|
106
|
|
|
'properties' => array_filter($this->getClassMetadata($class)) |
107
|
|
|
] |
108
|
|
|
] |
109
|
|
|
])); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
public function getDocumentNamespace(string $indexAlias): ?string |
113
|
|
|
{ |
114
|
|
|
if ($this->cache->contains(Configuration::ONGR_INDEXES)) { |
115
|
|
|
$indexes = $this->cache->fetch(Configuration::ONGR_INDEXES); |
116
|
|
|
|
117
|
|
|
if (isset($indexes[$indexAlias])) { |
118
|
|
|
return $indexes[$indexAlias]; |
119
|
|
|
} |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
return null; |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
public function getParsedDocument(\ReflectionClass $class): Index |
126
|
|
|
{ |
127
|
|
|
/** @var Index $document */ |
128
|
|
|
$document = $this->reader->getClassAnnotation($class, Index::class); |
129
|
|
|
|
130
|
|
|
return $document; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
private function getClassMetadata(\ReflectionClass $class): array |
134
|
|
|
{ |
135
|
|
|
$mapping = []; |
136
|
|
|
$objFields = null; |
137
|
|
|
$arrayFields = null; |
138
|
|
|
$embeddedFields = null; |
139
|
|
|
|
140
|
|
|
/** @var \ReflectionProperty $property */ |
141
|
|
|
foreach ($this->getDocumentPropertiesReflection($class) as $name => $property) { |
142
|
|
|
$annotations = $this->reader->getPropertyAnnotations($property); |
143
|
|
|
|
144
|
|
|
/** @var AbstractAnnotation $annotation */ |
145
|
|
|
foreach ($annotations as $annotation) { |
146
|
|
|
if (!$annotation instanceof PropertiesAwareInterface) { |
147
|
|
|
continue; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
$fieldMapping = $annotation->getSettings(); |
151
|
|
|
|
152
|
|
|
if ($annotation instanceof Property) { |
153
|
|
|
$fieldMapping['type'] = $annotation->type; |
154
|
|
|
if ($annotation->fields) { |
155
|
|
|
$fieldMapping['fields'] = $annotation->fields; |
156
|
|
|
} |
157
|
|
|
$fieldMapping['analyzer'] = $annotation->analyzer; |
158
|
|
|
$fieldMapping['search_analyzer'] = $annotation->searchAnalyzer; |
159
|
|
|
$fieldMapping['search_quote_analyzer'] = $annotation->searchQuoteAnalyzer; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
if ($annotation instanceof Embedded) { |
163
|
|
|
$embeddedClass = new \ReflectionClass($annotation->class); |
164
|
|
|
$fieldMapping['type'] = $this->getObjectMappingType($embeddedClass); |
165
|
|
|
$fieldMapping['properties'] = $this->getClassMetadata($embeddedClass); |
166
|
|
|
$embeddedFields[$name] = $annotation->class; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
$mapping[$annotation->getName() ?? Caser::snake($name)] = array_filter($fieldMapping); |
170
|
|
|
$objFields[$name] = $annotation->getName() ?? Caser::snake($name); |
171
|
|
|
$arrayFields[$annotation->getName() ?? Caser::snake($name)] = $name; |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
//Embeded fields are option compared to the array or object mapping. |
176
|
|
|
if ($embeddedFields) { |
177
|
|
|
$cacheItem = $this->cache->fetch(self::EMBEDDED_CACHED_FIELDS) ?? []; |
178
|
|
|
$cacheItem[$class->getName()] = $embeddedFields; |
179
|
|
|
$t = $this->cache->save(self::EMBEDDED_CACHED_FIELDS, $cacheItem); |
|
|
|
|
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
$cacheItem = $this->cache->fetch(self::ARRAY_CACHED_FIELDS) ?? []; |
183
|
|
|
$cacheItem[$class->getName()] = $arrayFields; |
184
|
|
|
$this->cache->save(self::ARRAY_CACHED_FIELDS, $cacheItem); |
185
|
|
|
|
186
|
|
|
$cacheItem = $this->cache->fetch(self::OBJ_CACHED_FIELDS) ?? []; |
187
|
|
|
$cacheItem[$class->getName()] = $objFields; |
188
|
|
|
$this->cache->save(self::OBJ_CACHED_FIELDS, $cacheItem); |
189
|
|
|
|
190
|
|
|
return $mapping; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
public function getPropertyMetadata(\ReflectionClass $class, bool $subClass = false): array |
194
|
|
|
{ |
195
|
|
|
if ($class->isTrait() || (!$this->reader->getClassAnnotation($class, Index::class) && !$subClass)) { |
196
|
|
|
return []; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
$metadata = []; |
200
|
|
|
|
201
|
|
|
/** @var \ReflectionProperty $property */ |
202
|
|
|
foreach ($this->getDocumentPropertiesReflection($class) as $name => $property) { |
203
|
|
|
/** @var AbstractAnnotation $annotation */ |
204
|
|
|
foreach ($this->reader->getPropertyAnnotations($property) as $annotation) { |
205
|
|
|
if (!$annotation instanceof PropertiesAwareInterface) { |
206
|
|
|
continue; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
$propertyMetadata = [ |
210
|
|
|
'identifier' => false, |
211
|
|
|
'class' => null, |
212
|
|
|
'embeded' => false, |
213
|
|
|
'type' => null, |
214
|
|
|
'public' => $property->isPublic(), |
215
|
|
|
'getter' => null, |
216
|
|
|
'setter' => null, |
217
|
|
|
'sub_properties' => [] |
218
|
|
|
]; |
219
|
|
|
|
220
|
|
|
$name = $property->getName(); |
221
|
|
|
$propertyMetadata['name'] = $name; |
222
|
|
|
|
223
|
|
|
if (!$propertyMetadata['public']) { |
224
|
|
|
$propertyMetadata['getter'] = $this->guessGetter($class, $name); |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
if ($annotation instanceof Id) { |
228
|
|
|
$propertyMetadata['identifier'] = true; |
229
|
|
|
} else { |
230
|
|
|
if (!$propertyMetadata['public']) { |
231
|
|
|
$propertyMetadata['setter'] = $this->guessSetter($class, $name); |
232
|
|
|
} |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
if ($annotation instanceof Property) { |
236
|
|
|
// we need the type (and possibly settings?) in Converter::denormalize() |
237
|
|
|
$propertyMetadata['type'] = $annotation->type; |
238
|
|
|
$propertyMetadata['settings'] = $annotation->settings; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
if ($annotation instanceof Embedded) { |
242
|
|
|
$propertyMetadata['embeded'] = true; |
243
|
|
|
$propertyMetadata['class'] = $annotation->class; |
244
|
|
|
$propertyMetadata['sub_properties'] = $this->getPropertyMetadata( |
245
|
|
|
new \ReflectionClass($annotation->class), |
246
|
|
|
true |
247
|
|
|
); |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
$metadata[$annotation->getName() ?? Caser::snake($name)] = $propertyMetadata; |
251
|
|
|
} |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
return $metadata; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
public function getAnalysisConfig(\ReflectionClass $class): array |
258
|
|
|
{ |
259
|
|
|
$config = []; |
260
|
|
|
$mapping = $this->getClassMetadata($class); |
261
|
|
|
|
262
|
|
|
//Think how to remove these array merge |
263
|
|
|
$analyzers = $this->getListFromArrayByKey('analyzer', $mapping); |
264
|
|
|
$analyzers = array_merge($analyzers, $this->getListFromArrayByKey('search_analyzer', $mapping)); |
265
|
|
|
$analyzers = array_merge($analyzers, $this->getListFromArrayByKey('search_quote_analyzer', $mapping)); |
266
|
|
|
|
267
|
|
|
foreach ($analyzers as $analyzer) { |
268
|
|
|
if (isset($this->analysisConfig['analyzer'][$analyzer])) { |
269
|
|
|
$config['analyzer'][$analyzer] = $this->analysisConfig['analyzer'][$analyzer]; |
270
|
|
|
} |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
$normalizers = $this->getListFromArrayByKey('normalizer', $mapping); |
274
|
|
|
foreach ($normalizers as $normalizer) { |
275
|
|
|
if (isset($this->analysisConfig['normalizer'][$normalizer])) { |
276
|
|
|
$config['normalizer'][$normalizer] = $this->analysisConfig['normalizer'][$normalizer]; |
277
|
|
|
} |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
foreach (['tokenizer', 'filter', 'char_filter'] as $type) { |
281
|
|
|
$list = $this->getListFromArrayByKey($type, $config); |
282
|
|
|
|
283
|
|
|
foreach ($list as $listItem) { |
284
|
|
|
if (isset($this->analysisConfig[$type][$listItem])) { |
285
|
|
|
$config[$type][$listItem] = $this->analysisConfig[$type][$listItem]; |
286
|
|
|
} |
287
|
|
|
} |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
return $config; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
protected function guessGetter(\ReflectionClass $class, $name): string |
294
|
|
|
{ |
295
|
|
|
if ($class->hasMethod($name)) { |
296
|
|
|
return $name; |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
if ($class->hasMethod('get' . ucfirst($name))) { |
300
|
|
|
return 'get' . ucfirst($name); |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
if ($class->hasMethod('is' . ucfirst($name))) { |
304
|
|
|
return 'is' . ucfirst($name); |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
// if there are underscores in the name convert them to CamelCase |
308
|
|
|
if (strpos($name, '_')) { |
309
|
|
|
$name = Caser::camel($name); |
310
|
|
|
if ($class->hasMethod('get' . ucfirst($name))) { |
311
|
|
|
return 'get' . $name; |
312
|
|
|
} |
313
|
|
|
if ($class->hasMethod('is' . ucfirst($name))) { |
314
|
|
|
return 'is' . $name; |
315
|
|
|
} |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
throw new \Exception("Could not determine a getter for `$name` of class `{$class->getNamespaceName()}`"); |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
protected function guessSetter(\ReflectionClass $class, $name): string |
322
|
|
|
{ |
323
|
|
|
if ($class->hasMethod('set' . ucfirst($name))) { |
324
|
|
|
return 'set' . ucfirst($name); |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
// if there are underscores in the name convert them to CamelCase |
328
|
|
|
if (strpos($name, '_')) { |
329
|
|
|
$name = Caser::camel($name); |
330
|
|
|
if ($class->hasMethod('set' . ucfirst($name))) { |
331
|
|
|
return 'set' . $name; |
332
|
|
|
} |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
throw new \Exception("Could not determine a setter for `$name` of class `{$class->getNamespaceName()}`"); |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
private function getListFromArrayByKey(string $searchKey, array $array): array |
339
|
|
|
{ |
340
|
|
|
$list = []; |
341
|
|
|
|
342
|
|
|
foreach (new \RecursiveIteratorIterator( |
343
|
|
|
new \RecursiveArrayIterator($array), |
344
|
|
|
\RecursiveIteratorIterator::SELF_FIRST |
345
|
|
|
) as $key => $value) { |
346
|
|
|
if ($key === $searchKey) { |
347
|
|
|
if (is_array($value)) { |
348
|
|
|
$list = array_merge($list, $value); |
349
|
|
|
} else { |
350
|
|
|
$list[] = $value; |
351
|
|
|
} |
352
|
|
|
} |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
return array_unique($list); |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
private function getObjectMappingType(\ReflectionClass $class): string |
359
|
|
|
{ |
360
|
|
|
switch (true) { |
361
|
|
|
case $this->reader->getClassAnnotation($class, ObjectType::class): |
362
|
|
|
$type = ObjectType::TYPE; |
363
|
|
|
break; |
364
|
|
|
case $this->reader->getClassAnnotation($class, NestedType::class): |
365
|
|
|
$type = NestedType::TYPE; |
366
|
|
|
break; |
367
|
|
|
default: |
368
|
|
|
throw new \LogicException( |
369
|
|
|
sprintf( |
370
|
|
|
'%s must be used @ObjectType or @NestedType as embeddable object.', |
371
|
|
|
$class->getName() |
372
|
|
|
) |
373
|
|
|
); |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
return $type; |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
private function getDocumentPropertiesReflection(\ReflectionClass $class): array |
380
|
|
|
{ |
381
|
|
|
if (in_array($class->getName(), $this->properties)) { |
382
|
|
|
return $this->properties[$class->getName()]; |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
$properties = []; |
386
|
|
|
|
387
|
|
|
foreach ($class->getProperties() as $property) { |
388
|
|
|
if (!in_array($property->getName(), $properties)) { |
389
|
|
|
$properties[$property->getName()] = $property; |
390
|
|
|
} |
391
|
|
|
} |
392
|
|
|
|
393
|
|
|
$parentReflection = $class->getParentClass(); |
394
|
|
|
if ($parentReflection !== false) { |
395
|
|
|
$properties = array_merge( |
396
|
|
|
$properties, |
397
|
|
|
array_diff_key($this->getDocumentPropertiesReflection($parentReflection), $properties) |
398
|
|
|
); |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
$this->properties[$class->getName()] = $properties; |
402
|
|
|
|
403
|
|
|
return $properties; |
404
|
|
|
} |
405
|
|
|
} |
406
|
|
|
|
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.