Skip to content

Commit cb1bc37

Browse files
committed
Added the option to add @es\Property and @es\Embedded to methods
1 parent 26addf0 commit cb1bc37

File tree

9 files changed

+351
-45
lines changed

9 files changed

+351
-45
lines changed

Annotation/Embedded.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
/**
1515
* @Annotation
16-
* @Target("PROPERTY")
16+
* @Target({"METHOD","PROPERTY"})
1717
*/
1818
final class Embedded extends AbstractAnnotation implements PropertiesAwareInterface
1919
{

Annotation/Property.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
/**
1515
* @Annotation
16-
* @Target("PROPERTY")
16+
* @Target({"METHOD","PROPERTY"})
1717
*/
1818
final class Property extends AbstractAnnotation implements PropertiesAwareInterface
1919
{

Mapping/Converter.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,14 @@ protected function normalize($document, $metadata = null)
5757
if ($fieldMeta['embeded']) {
5858
if (is_iterable($value)) {
5959
foreach ($value as $item) {
60-
$result[$field][] = $this->normalize($item, $fieldMeta['sub_properties']);
60+
if ($item) {
61+
$result[$field][] = $this->normalize($item, $fieldMeta['sub_properties']);
62+
}
6163
}
6264
} else {
63-
$result[$field] = $this->normalize($value, $fieldMeta['sub_properties']);
65+
if ($value) {
66+
$result[$field] = $this->normalize($value, $fieldMeta['sub_properties']);
67+
}
6468
}
6569
} else {
6670
if ($value instanceof \DateTime) {
@@ -106,7 +110,9 @@ protected function denormalize(array $raw, string $namespace)
106110
$setter = \Closure::bind($setter, $object, $object);
107111
$setter($fieldMeta['name'], $value);
108112
} else {
109-
$object->$setter($value);
113+
if ($setter) {
114+
$object->$setter($value);
115+
}
110116
}
111117
}
112118
}

Mapping/DocumentParser.php

Lines changed: 145 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,16 @@
2222
use ONGR\ElasticsearchBundle\Annotation\ObjectType;
2323
use ONGR\ElasticsearchBundle\Annotation\PropertiesAwareInterface;
2424
use ONGR\ElasticsearchBundle\Annotation\Property;
25-
use ONGR\ElasticsearchBundle\DependencyInjection\Configuration;
2625

2726
/**
2827
* Document parser used for reading document annotations.
2928
*/
3029
class DocumentParser
3130
{
32-
const OBJ_CACHED_FIELDS = 'ongr.obj_fields';
33-
const EMBEDDED_CACHED_FIELDS = 'ongr.embedded_fields';
34-
const ARRAY_CACHED_FIELDS = 'ongr.array_fields';
3531

3632
private $reader;
3733
private $properties = [];
34+
private $methods = [];
3835
private $analysisConfig = [];
3936
private $cache;
4037

@@ -106,36 +103,14 @@ private function getClassMetadata(\ReflectionClass $class): array
106103
/** @var \ReflectionProperty $property */
107104
foreach ($this->getDocumentPropertiesReflection($class) as $name => $property) {
108105
$annotations = $this->reader->getPropertyAnnotations($property);
106+
$this->getPropertyMapping($annotations, $embeddedFields, $name, $mapping, $objFields, $arrayFields);
107+
}
109108

110-
/** @var AbstractAnnotation $annotation */
111-
foreach ($annotations as $annotation) {
112-
if (!$annotation instanceof PropertiesAwareInterface) {
113-
continue;
114-
}
115-
116-
$fieldMapping = $annotation->getSettings();
117-
118-
if ($annotation instanceof Property) {
119-
$fieldMapping['type'] = $annotation->type;
120-
if ($annotation->fields) {
121-
$fieldMapping['fields'] = $annotation->fields;
122-
}
123-
$fieldMapping['analyzer'] = $annotation->analyzer;
124-
$fieldMapping['search_analyzer'] = $annotation->searchAnalyzer;
125-
$fieldMapping['search_quote_analyzer'] = $annotation->searchQuoteAnalyzer;
126-
}
127-
128-
if ($annotation instanceof Embedded) {
129-
$embeddedClass = new \ReflectionClass($annotation->class);
130-
$fieldMapping['type'] = $this->getObjectMappingType($embeddedClass);
131-
$fieldMapping['properties'] = $this->getClassMetadata($embeddedClass);
132-
$embeddedFields[$name] = $annotation->class;
133-
}
134-
135-
$mapping[$annotation->getName() ?? Caser::snake($name)] = array_filter($fieldMapping);
136-
$objFields[$name] = $annotation->getName() ?? Caser::snake($name);
137-
$arrayFields[$annotation->getName() ?? Caser::snake($name)] = $name;
138-
}
109+
/** @var \ReflectionMethod $method */
110+
foreach ($this->getDocumentMethodsReflection($class) as $name => $method) {
111+
$name = $this->guessPropertyNameFromGetter($name);
112+
$annotations = $this->reader->getMethodAnnotations($method);
113+
$this->getPropertyMapping($annotations, $embeddedFields, $name, $mapping, $objFields, $arrayFields);
139114
}
140115

141116
//Embeded fields are option compared to the array or object mapping.
@@ -164,10 +139,10 @@ public function getPropertyMetadata(\ReflectionClass $class, bool $subClass = fa
164139

165140
$metadata = [];
166141

167-
/** @var \ReflectionProperty $property */
168-
foreach ($this->getDocumentPropertiesReflection($class) as $name => $property) {
142+
/** @var \ReflectionProperty $method */
143+
foreach ($this->getDocumentPropertiesReflection($class) as $name => $method) {
169144
/** @var AbstractAnnotation $annotation */
170-
foreach ($this->reader->getPropertyAnnotations($property) as $annotation) {
145+
foreach ($this->reader->getPropertyAnnotations($method) as $annotation) {
171146
if (!$annotation instanceof PropertiesAwareInterface) {
172147
continue;
173148
}
@@ -177,13 +152,13 @@ public function getPropertyMetadata(\ReflectionClass $class, bool $subClass = fa
177152
'class' => null,
178153
'embeded' => false,
179154
'type' => null,
180-
'public' => $property->isPublic(),
155+
'public' => $method->isPublic(),
181156
'getter' => null,
182157
'setter' => null,
183158
'sub_properties' => []
184159
];
185160

186-
$name = $property->getName();
161+
$name = $method->getName();
187162
$propertyMetadata['name'] = $name;
188163

189164
if (!$propertyMetadata['public']) {
@@ -217,6 +192,52 @@ public function getPropertyMetadata(\ReflectionClass $class, bool $subClass = fa
217192
}
218193
}
219194

195+
/** @var \ReflectionProperty $property */
196+
foreach ($this->getDocumentMethodsReflection($class) as $name => $method) {
197+
/** @var AbstractAnnotation $annotation */
198+
foreach ($this->reader->getMethodAnnotations($method) as $annotation) {
199+
if (!$annotation instanceof PropertiesAwareInterface) {
200+
continue;
201+
}
202+
203+
$guessedName = $this->guessPropertyNameFromGetter($name);
204+
$fieldName = $annotation->getName() ?? Caser::snake($guessedName);
205+
206+
$propertyMetadata = [
207+
'identifier' => false,
208+
'class' => null,
209+
'embeded' => false,
210+
'type' => null,
211+
'public' => false,
212+
'getter' => $name,
213+
'setter' => null,
214+
'sub_properties' => [],
215+
'name' => $annotation->getName() ?? $guessedName
216+
];
217+
218+
if ($annotation instanceof Id) {
219+
$propertyMetadata['identifier'] = true;
220+
}
221+
222+
if ($annotation instanceof Property) {
223+
// we need the type (and possibly settings?) in Converter::denormalize()
224+
$propertyMetadata['type'] = $annotation->type;
225+
$propertyMetadata['settings'] = $annotation->settings;
226+
}
227+
228+
if ($annotation instanceof Embedded) {
229+
$propertyMetadata['embeded'] = true;
230+
$propertyMetadata['class'] = $annotation->class;
231+
$propertyMetadata['sub_properties'] = $this->getPropertyMetadata(
232+
new \ReflectionClass($annotation->class),
233+
true
234+
);
235+
}
236+
237+
$metadata[$fieldName] = $propertyMetadata;
238+
}
239+
}
240+
220241
return $metadata;
221242
}
222243

@@ -284,6 +305,15 @@ protected function guessGetter(\ReflectionClass $class, $name): string
284305
throw new \Exception("Could not determine a getter for `$name` of class `{$class->getNamespaceName()}`");
285306
}
286307

308+
protected function guessPropertyNameFromGetter($name): string
309+
{
310+
if (preg_match('/^get([A-Z_0-9].*?)$/', $name, $matches)) {
311+
return lcfirst($matches[1]);
312+
}
313+
314+
return $name;
315+
}
316+
287317
protected function guessSetter(\ReflectionClass $class, $name): string
288318
{
289319
if ($class->hasMethod('set' . ucfirst($name))) {
@@ -356,6 +386,13 @@ private function getDocumentPropertiesReflection(\ReflectionClass $class): array
356386
}
357387
}
358388

389+
foreach ($class->getMethods() as $method) {
390+
391+
if (!in_array($property->getName(), $properties)) {
392+
$properties[$property->getName()] = $property;
393+
}
394+
}
395+
359396
$parentReflection = $class->getParentClass();
360397
if ($parentReflection !== false) {
361398
$properties = array_merge(
@@ -368,4 +405,74 @@ private function getDocumentPropertiesReflection(\ReflectionClass $class): array
368405

369406
return $properties;
370407
}
408+
409+
private function getDocumentMethodsReflection(\ReflectionClass $class): array
410+
{
411+
if (in_array($class->getName(), $this->methods)) {
412+
return $this->methods[$class->getName()];
413+
}
414+
415+
$methods = [];
416+
417+
foreach ($class->getMethods() as $method) {
418+
if (!in_array($method->getName(), $methods)) {
419+
$methods[$method->getName()] = $method;
420+
}
421+
}
422+
423+
$parentReflection = $class->getParentClass();
424+
if ($parentReflection !== false) {
425+
$methods = array_merge(
426+
$methods,
427+
array_diff_key($this->getDocumentMethodsReflection($parentReflection), $methods)
428+
);
429+
}
430+
431+
$this->methods[$class->getName()] = $methods;
432+
433+
return $methods;
434+
}
435+
436+
/**
437+
* @param array $annotations
438+
* @param $embeddedFields
439+
* @param string $name
440+
* @param array $mapping
441+
* @param array $objFields
442+
* @param array $arrayFields
443+
* @throws \ReflectionException
444+
*/
445+
private function getPropertyMapping(array $annotations, &$embeddedFields, string $name, array &$mapping, ?array &$objFields, ?array &$arrayFields): void
446+
{
447+
/** @var AbstractAnnotation $annotation */
448+
foreach ($annotations as $annotation) {
449+
if (!$annotation instanceof PropertiesAwareInterface) {
450+
continue;
451+
}
452+
453+
$fieldMapping = $annotation->getSettings();
454+
455+
if ($annotation instanceof Property) {
456+
$fieldMapping['type'] = $annotation->type;
457+
if ($annotation->fields) {
458+
$fieldMapping['fields'] = $annotation->fields;
459+
}
460+
$fieldMapping['analyzer'] = $annotation->analyzer;
461+
$fieldMapping['search_analyzer'] = $annotation->searchAnalyzer;
462+
$fieldMapping['search_quote_analyzer'] = $annotation->searchQuoteAnalyzer;
463+
}
464+
465+
if ($annotation instanceof Embedded) {
466+
$embeddedClass = new \ReflectionClass($annotation->class);
467+
$fieldMapping['type'] = $this->getObjectMappingType($embeddedClass);
468+
$fieldMapping['properties'] = $this->getClassMetadata($embeddedClass);
469+
$embeddedFields[$name] = $annotation->class;
470+
}
471+
472+
$fieldName = $annotation->getName() ?? Caser::snake($name);
473+
$mapping[$fieldName] = array_filter($fieldMapping);
474+
$objFields[$name] = $fieldName;
475+
$arrayFields[$fieldName] = $name;
476+
}
477+
}
371478
}

Resources/doc/mapping.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,30 @@ nested objects stored separately. This introduces differences when querying and
404404

405405
More information about nested documents if [here](https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html)
406406

407+
### Methods as properties
408+
You can use the annotations `@ES\Property` and `@ES\Embedded` with both properties and methods.
409+
If you use it with a method, the value will be saved in elastic search. Thus it can be used to query for these
410+
values. However, since we assume that there is no setter that belongs to the method, no setter is called, when
411+
you retrieve the document from elastic search.
412+
413+
```php
414+
/**
415+
* The name is `has_description` by default.
416+
* @ES\Property(type="boolean")
417+
*/
418+
public function hasDescription() {
419+
return !empty($this->description);
420+
}
421+
422+
/**
423+
* The name is `some_object` by default.
424+
* @ES\Embedded(class="App\Document\SomeObject")
425+
*/
426+
public function getSomeObject() {
427+
return new SomeObject();
428+
}
429+
```
430+
407431
### Multi field annotations and usage
408432

409433
Within the properties annotation, you can specify the `fields` attribute. It enables you to map several core

Test/AbstractElasticsearchTestCase.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,27 @@ protected function getDataArray(): array
5858
return [];
5959
}
6060

61-
private function populateElastic(IndexService $indexService, array $documents = [])
61+
protected function getDataDocuments(): array
62+
{
63+
return [];
64+
}
65+
66+
private function populateDataArray(IndexService $indexService, array $documents = [])
6267
{
6368
foreach ($documents as $document) {
6469
$indexService->bulk('index', $document);
6570
}
6671
$indexService->commit();
6772
}
6873

74+
private function populateDocument(IndexService $indexService, array $documents = [])
75+
{
76+
foreach ($documents as $document) {
77+
$indexService->persist($document);
78+
}
79+
$indexService->commit();
80+
}
81+
6982
/**
7083
* {@inheritdoc}
7184
*/
@@ -106,8 +119,15 @@ protected function getIndex($namespace, $createIndex = true): IndexService
106119
// Populates elasticsearch index with the data
107120
$data = $this->getDataArray();
108121
if (!empty($data[$namespace])) {
109-
$this->populateElastic($this->indexes[$namespace], $data[$namespace]);
122+
$this->populateDataArray($this->indexes[$namespace], $data[$namespace]);
110123
}
124+
125+
// Populates elasticsearch index with the data
126+
$data = $this->getDataDocuments();
127+
if (!empty($data[$namespace])) {
128+
$this->populateDocument($this->indexes[$namespace], $data[$namespace]);
129+
}
130+
111131
$this->indexes[$namespace]->refresh();
112132
}
113133

0 commit comments

Comments
 (0)