diff --git a/DependencyInjection/Compiler/MappingPass.php b/DependencyInjection/Compiler/MappingPass.php index 3f696a9e..dc739659 100644 --- a/DependencyInjection/Compiler/MappingPass.php +++ b/DependencyInjection/Compiler/MappingPass.php @@ -59,6 +59,7 @@ private function handleDirectoryMapping(ContainerBuilder $container, string $dir /** @var DocumentParser $parser */ $parser = $container->get(DocumentParser::class); $indexesOverride = $container->getParameter(Configuration::ONGR_INDEXES_OVERRIDE); + $converterDefinition = $container->getDefinition(Converter::class); foreach ($this->getNamespaces($dir) as $namespace) { $class = new \ReflectionClass($namespace); @@ -71,10 +72,9 @@ private function handleDirectoryMapping(ContainerBuilder $container, string $dir /** @var Index $document */ $document = $parser->getIndexAnnotation($class); - $indexMapping = $parser->getIndexMetadata($class); $indexMetadata = $parser->getIndexMetadata($class); - if (!empty($indexMapping)) { + if (!empty($indexMetadata)) { $indexMetadata['settings'] = array_filter(array_merge_recursive( $indexMetadata['settings'] ?? [], [ @@ -99,14 +99,20 @@ private function handleDirectoryMapping(ContainerBuilder $container, string $dir $indexServiceDefinition = new Definition(IndexService::class, [ $namespace, - $container->getDefinition(Converter::class), + $converterDefinition, $container->getDefinition('event_dispatcher'), - $container->getDefinition('serializer'), $indexSettings, $container->getParameter(Configuration::ONGR_PROFILER_CONFIG) ? $container->getDefinition('ongr.esb.tracer') : null ]); $indexServiceDefinition->setPublic(true); + $converterDefinition->addMethodCall( + 'addClassMetadata', + [ + $namespace, + $parser->getPropertyMetadata($class) + ] + ); $container->setDefinition($namespace, $indexServiceDefinition); $this->indexes[$indexAlias] = $namespace; diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index f73b1bd1..bc2c665b 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -68,6 +68,7 @@ public function getConfigTreeBuilder() ->arrayNode('indexes') ->defaultValue([]) + ->useAttributeAsKey('namespace') ->info( 'In case you want to override index settings defined in the annotation.' . ' e.g. use env variables instead.' diff --git a/Mapping/Converter.php b/Mapping/Converter.php index f347cb5a..6778c0f6 100644 --- a/Mapping/Converter.php +++ b/Mapping/Converter.php @@ -11,34 +11,107 @@ namespace ONGR\ElasticsearchBundle\Mapping; -use ONGR\ElasticsearchBundle\Annotation\NestedType; -use ONGR\ElasticsearchBundle\Annotation\ObjectType; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; -use Symfony\Component\Serializer\SerializerAwareTrait; -use Symfony\Component\Serializer\SerializerInterface; +use ONGR\ElasticsearchBundle\Result\ObjectIterator; /** * This class converts array to document object. */ class Converter { - use SerializerAwareTrait; + private $propertyMetadata = []; - private $documentParser; + public function addClassMetadata(string $class, array $metadata): void + { + $this->propertyMetadata[$class] = $metadata; + } + + public function convertArrayToDocument(string $namespace, array $raw) + { + if (!isset($this->propertyMetadata[$namespace])) { + throw new \Exception("Cannot convert array to object of class `$class`."); + } - public function __construct(DocumentParser $documentParser) + return $this->denormalize($raw, $namespace); + } + + public function convertDocumentToArray($document): array { - $this->documentParser = $documentParser; + $class = get_class($document); + + if (!isset($this->propertyMetadata[$class])) { + throw new \Exception("Cannot convert object of class `$class` to array."); + } + + return $this->normalize($document); } - public function convertArrayToDocument(string $namespace, array $raw, Serializer $serializer) + protected function normalize($document, $metadata = null) { - return $serializer->denormalize($raw, $namespace); + $metadata = $metadata ?? $this->propertyMetadata[get_class($document)]; + $result = []; + + foreach ($metadata as $field => $fieldMeta) { + $getter = $fieldMeta['getter']; + $value = $fieldMeta['public'] ? $document->{$fieldMeta['name']} : $document->$getter(); + + if ($fieldMeta['embeded']) { + if (is_iterable($value)) { + foreach ($value as $item) { + $result[$field][] = $this->normalize($item, $fieldMeta['sub_properties']); + } + } else { + $result[$field] = $this->normalize($value, $fieldMeta['sub_properties']); + } + } else { + if ($value instanceof \DateTime) { + $value = $value->format(\DateTime::ISO8601); + } + $result[$field] = $value; + } + } + + return $result; } - public function convertDocumentToArray($document, Serializer $serializer): array + protected function denormalize(array $raw, string $namespace) { - return $serializer->normalize($document, 'array'); + $metadata = $this->propertyMetadata[$namespace]; + $object = new $namespace(); + + foreach ($raw as $field => $value) { + $fieldMeta = $metadata[$field]; + $setter = $fieldMeta['setter']; + + if ($fieldMeta['embeded']) { + $this->addClassMetadata($fieldMeta['class'], $fieldMeta['sub_properties']); + $iterator = new ObjectIterator($fieldMeta['class'], $value, $this); + + if ($fieldMeta['public']) { + $object->{$fieldMeta['name']} = $iterator; + } else { + $object->$setter($iterator); + } + } else { + if ($fieldMeta['type'] == 'date') { + $value = \DateTime::createFromFormat(\DateTime::ISO8601, $value) ?: null; + } + if ($fieldMeta['public']) { + $object->{$fieldMeta['name']} = $value; + } else { + if ($fieldMeta['identifier']) { + $setter = function ($field, $value) { + $this->$field = $value; + }; + + $setter = \Closure::bind($setter, $object, $object); + $setter($fieldMeta['name'], $value); + } else { + $object->$setter($value); + } + } + } + } + + return $object; } } diff --git a/Mapping/DocumentParser.php b/Mapping/DocumentParser.php index b8b74cdc..b929bce2 100644 --- a/Mapping/DocumentParser.php +++ b/Mapping/DocumentParser.php @@ -16,6 +16,7 @@ use Doctrine\Common\Cache\Cache; use ONGR\ElasticsearchBundle\Annotation\AbstractAnnotation; use ONGR\ElasticsearchBundle\Annotation\Embedded; +use ONGR\ElasticsearchBundle\Annotation\Id; use ONGR\ElasticsearchBundle\Annotation\Index; use ONGR\ElasticsearchBundle\Annotation\NestedType; use ONGR\ElasticsearchBundle\Annotation\ObjectType; @@ -150,6 +151,9 @@ private function getClassMetadata(\ReflectionClass $class): array if ($annotation instanceof Property) { $fieldMapping['type'] = $annotation->type; + if ($annotation->fields) { + $fieldMapping['fields'] = $annotation->fields; + } $fieldMapping['analyzer'] = $annotation->analyzer; $fieldMapping['search_analyzer'] = $annotation->searchAnalyzer; $fieldMapping['search_quote_analyzer'] = $annotation->searchQuoteAnalyzer; @@ -186,6 +190,70 @@ private function getClassMetadata(\ReflectionClass $class): array return $mapping; } + public function getPropertyMetadata(\ReflectionClass $class, bool $subClass = false): array + { + if ($class->isTrait() || (!$this->reader->getClassAnnotation($class, Index::class) && !$subClass)) { + return []; + } + + $metadata = []; + + /** @var \ReflectionProperty $property */ + foreach ($this->getDocumentPropertiesReflection($class) as $name => $property) { + /** @var AbstractAnnotation $annotation */ + foreach ($this->reader->getPropertyAnnotations($property) as $annotation) { + if (!$annotation instanceof PropertiesAwareInterface) { + continue; + } + + $propertyMetadata = [ + 'identifier' => false, + 'class' => null, + 'embeded' => false, + 'type' => null, + 'public' => $property->isPublic(), + 'getter' => null, + 'setter' => null, + 'sub_properties' => [] + ]; + + $name = $property->getName(); + $propertyMetadata['name'] = $name; + + if (!$propertyMetadata['public']) { + $propertyMetadata['getter'] = $this->guessGetter($class, $name); + } + + if ($annotation instanceof Id) { + $propertyMetadata['identifier'] = true; + } else { + if (!$propertyMetadata['public']) { + $propertyMetadata['setter'] = $this->guessSetter($class, $name); + } + } + + if ($annotation instanceof Property) { + // we need the type (and possibly settings?) in Converter::denormalize() + $propertyMetadata['type'] = $annotation->type; + $propertyMetadata['settings'] = $annotation->settings; + } + + if ($annotation instanceof Embedded) { + $propertyMetadata['embeded'] = true; + $propertyMetadata['class'] = $annotation->class; + $propertyMetadata['sub_properties'] = $this->getPropertyMetadata( + new \ReflectionClass($annotation->class), + true + ); + } + + $metadata[$annotation->getName() ?? Caser::snake($name)] = $propertyMetadata; + } + } + + return $metadata; + } + public function getAnalysisConfig(\ReflectionClass $class): array { $config = []; @@ -202,7 +270,14 @@ public function getAnalysisConfig(\ReflectionClass $class): array } } - foreach (['tokenizer', 'filter', 'normalizer', 'char_filter'] as $type) { + $normalizers = $this->getListFromArrayByKey('normalizer', $mapping); + foreach ($normalizers as $normalizer) { + if (isset($this->analysisConfig['normalizer'][$normalizer])) { + $config['normalizer'][$normalizer] = $this->analysisConfig['normalizer'][$normalizer]; + } + } + + foreach (['tokenizer', 'filter', 'char_filter'] as $type) { $list = $this->getListFromArrayByKey($type, $config); foreach ($list as $listItem) { @@ -215,6 +290,51 @@ public function getAnalysisConfig(\ReflectionClass $class): array return $config; } + protected function guessGetter(\ReflectionClass $class, $name): string + { + if ($class->hasMethod($name)) { + return $name; + } + + if ($class->hasMethod('get' . ucfirst($name))) { + return 'get' . ucfirst($name); + } + + if ($class->hasMethod('is' . ucfirst($name))) { + return 'is' . ucfirst($name); + } + + // if there are underscores in the name convert them to CamelCase + if (strpos($name, '_')) { + $name = Caser::camel($name); + if ($class->hasMethod('get' . ucfirst($name))) { + return 'get' . $name; + } + if ($class->hasMethod('is' . ucfirst($name))) { + return 'is' . $name; + } + } + + throw new \Exception("Could not determine a getter for `$name` of class `{$class->getNamespaceName()}`"); + } + + protected function guessSetter(\ReflectionClass $class, $name): string + { + if ($class->hasMethod('set' . ucfirst($name))) { + return 'set' . ucfirst($name); + } + + // if there are underscores in the name convert them to CamelCase + if (strpos($name, '_')) { + $name = Caser::camel($name); + if ($class->hasMethod('set' . ucfirst($name))) { + return 'set' . $name; + } + } + + throw new \Exception("Could not determine a setter for `$name` of class `{$class->getNamespaceName()}`"); + } + private function getListFromArrayByKey(string $searchKey, array $array): array { $list = []; diff --git a/Mapping/NameConverter.php b/Mapping/NameConverter.php deleted file mode 100644 index dfd6b398..00000000 --- a/Mapping/NameConverter.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace ONGR\ElasticsearchBundle\Mapping; - -use Doctrine\Common\Cache\Cache; -use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; - -class NameConverter implements AdvancedNameConverterInterface -{ - private $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } - - public function normalize($propertyName, string $class = null, string $format = null, array $context = []) - { - $fields = $this->cache->fetch(DocumentParser::OBJ_CACHED_FIELDS); - - if (isset($fields[$class])) { - return $fields[$class][$propertyName] ?? $propertyName; - } - - return $propertyName; - } - - public function denormalize($propertyName, string $class = null, string $format = null, array $context = []) - { - $fields = $this->cache->fetch(DocumentParser::ARRAY_CACHED_FIELDS); - - if (isset($fields[$class])) { - return $fields[$class][$propertyName] ?? $propertyName; - } - - return $propertyName; - } -} diff --git a/Mapping/ObjectNormalizer.php b/Mapping/ObjectNormalizer.php deleted file mode 100644 index daac1f05..00000000 --- a/Mapping/ObjectNormalizer.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace ONGR\ElasticsearchBundle\Mapping; - -use Doctrine\Common\Cache\Cache; -use ONGR\ElasticsearchBundle\Result\ObjectIterator; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\Serializer\NameConverter\NameConverterInterface; -use Symfony\Component\Serializer\Normalizer\ObjectNormalizer as SymfonyObjectNormalizer; - -class ObjectNormalizer extends SymfonyObjectNormalizer -{ - private $cache; - private $converter; - - public function __construct( - Cache $cache, - Converter $converter, - NameConverterInterface $nameConverter = null, - PropertyAccessorInterface $propertyAccessor = null - ) { - parent::__construct(null, $nameConverter, $propertyAccessor, null, null, null, []); - - $this->cache = $cache; - $this->converter = $converter; - } - - protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = []) - { - $embeddedFields = $this->cache->fetch(DocumentParser::EMBEDDED_CACHED_FIELDS); - - $class = $embeddedFields[get_class($object)][$attribute] ?? null; - - try { - if ($class && is_array($value)) { - $value = new ObjectIterator($class, $value, $this->converter, $this->serializer); - } - - $this->propertyAccessor->setValue($object, $attribute, $value); - } catch (NoSuchPropertyException $exception) { - // Properties not found are ignored - } - } -} diff --git a/Resources/config/services.yml b/Resources/config/services.yml index b34204a7..6b9ac085 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -21,21 +21,11 @@ services: ONGR\ElasticsearchBundle\Service\ExportService: ~ ONGR\ElasticsearchBundle\Service\ImportService: ~ ONGR\ElasticsearchBundle\Service\IndexSuffixFinder: ~ - - ONGR\ElasticsearchBundle\Mapping\ObjectNormalizer: - arguments: ["@ongr.esb.cache", '@ONGR\ElasticsearchBundle\Mapping\Converter', '@ONGR\ElasticsearchBundle\Mapping\NameConverter'] - tags: - - { name: serializer.normalizer, priority: 100} - - ONGR\ElasticsearchBundle\Mapping\NameConverter: - arguments: ["@ongr.esb.cache"] + ONGR\ElasticsearchBundle\Mapping\Converter: ~ ONGR\ElasticsearchBundle\Mapping\DocumentParser: arguments: ["@ongr.esb.cache_reader", "@ongr.esb.cache", "%ongr.esb.analysis%"] - ONGR\ElasticsearchBundle\Mapping\Converter: - arguments: ['@ONGR\ElasticsearchBundle\Mapping\DocumentParser'] - ONGR\ElasticsearchBundle\Profiler\Handler\CollectionHandler: public: false diff --git a/Result/AbstractResultsIterator.php b/Result/AbstractResultsIterator.php index fc35b851..39061833 100644 --- a/Result/AbstractResultsIterator.php +++ b/Result/AbstractResultsIterator.php @@ -13,7 +13,6 @@ use ONGR\ElasticsearchBundle\Mapping\Converter; use ONGR\ElasticsearchBundle\Service\IndexService; -use Symfony\Component\Serializer\Serializer; abstract class AbstractResultsIterator implements \Countable, \Iterator { @@ -30,13 +29,11 @@ abstract class AbstractResultsIterator implements \Countable, \Iterator //Used to count scroll iteration. private $key = 0; - protected $serializer; public function __construct( array $rawData, IndexService $index, Converter $converter = null, - Serializer $serializer = null, array $scroll = [] ) { $this->raw = $rawData; @@ -58,7 +55,6 @@ public function __construct( if (isset($rawData['hits']['total'])) { $this->count = $rawData['hits']['total']; } - $this->serializer = $serializer; } public function __destruct() diff --git a/Result/DocumentIterator.php b/Result/DocumentIterator.php index e680fb19..7665fe9f 100644 --- a/Result/DocumentIterator.php +++ b/Result/DocumentIterator.php @@ -41,10 +41,6 @@ protected function convertDocument(array $raw) $data = $raw['_source'] ?? $raw['_fields'] ?? null; $data['_id'] = $raw['_id'] ?? null; - return $this->getConverter()->convertArrayToDocument( - $this->getIndex()->getNamespace(), - array_filter($data), - $this->serializer - ); + return $this->getConverter()->convertArrayToDocument($this->getIndex()->getNamespace(), array_filter($data)); } } diff --git a/Result/ObjectIterator.php b/Result/ObjectIterator.php index ab239c95..502dcca9 100644 --- a/Result/ObjectIterator.php +++ b/Result/ObjectIterator.php @@ -14,7 +14,6 @@ use Doctrine\Common\Collections\AbstractLazyCollection; use Doctrine\Common\Collections\ArrayCollection; use ONGR\ElasticsearchBundle\Mapping\Converter; -use Symfony\Component\Serializer\Serializer; /** * This is for embedded ObjectType's or NestedType's iterator implemented with a lazy loading. @@ -24,23 +23,17 @@ class ObjectIterator extends AbstractLazyCollection private $converter; protected $collection; private $namespace; - private $serializer; - public function __construct(string $namespace, array $array, Converter $converter, Serializer $serializer) + public function __construct(string $namespace, array $array, Converter $converter) { $this->converter = $converter; $this->collection = new ArrayCollection($array); $this->namespace = $namespace; - $this->serializer = $serializer; } protected function convertDocument(array $data) { - return $this->converter->convertArrayToDocument( - $this->namespace, - $data, - $this->serializer - ); + return $this->converter->convertArrayToDocument($this->namespace, $data); } protected function doInitialize() diff --git a/Service/ExportService.php b/Service/ExportService.php index 6cfafdef..5b927549 100644 --- a/Service/ExportService.php +++ b/Service/ExportService.php @@ -43,7 +43,6 @@ public function exportIndex( $searchResults, $index, null, - null, [ 'duration' => '2m', '_scroll_id' => $searchResults['_scroll_id'], @@ -66,7 +65,6 @@ public function exportIndex( $filename = str_replace('.json', '', $filename); $writer = $this->getWriter($this->getFilePath($filename.'.json'), $metadata['count']); - $file = []; foreach ($results as $data) { if ($counter >= $maxLinesInFile) { $writer->finalize(); @@ -83,7 +81,6 @@ public function exportIndex( $doc = array_intersect_key($data, array_flip(['_id', '_type', '_source'])); $writer->push($doc); - $file[] = $doc; $progress->advance(); $counter++; } diff --git a/Service/IndexService.php b/Service/IndexService.php index e6efc973..a9cc924f 100644 --- a/Service/IndexService.php +++ b/Service/IndexService.php @@ -28,7 +28,6 @@ use ONGR\ElasticsearchDSL\Sort\FieldSort; use ONGR\ElasticsearchBundle\Result\DocumentIterator; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\Serializer\Serializer; class IndexService { @@ -41,21 +40,18 @@ class IndexService private $bulkCommitSize = 100; private $bulkQueries = []; private $indexSettings = []; - private $serializer; private $tracer; public function __construct( string $namespace, Converter $converter, EventDispatcherInterface $eventDispatcher, - Serializer $serializer, IndexSettings $indexSettings, $tracer = null ) { $this->namespace = $namespace; $this->converter = $converter; $this->eventDispatcher = $eventDispatcher; - $this->serializer = $serializer; $this->indexSettings = $indexSettings; $this->tracer = $tracer; $this->getClient(); @@ -207,7 +203,7 @@ public function find($id, $params = []) $result['_source']['_id'] = $result['_id']; - return $this->converter->convertArrayToDocument($this->namespace, $result['_source'], $this->serializer); + return $this->converter->convertArrayToDocument($this->namespace, $result['_source']); } public function findByIds(array $ids): DocumentIterator @@ -286,7 +282,6 @@ public function findDocuments(Search $search): DocumentIterator $results, $this, $this->converter, - $this->serializer, $this->getScrollConfiguration($results, $search->getScroll()) ); } @@ -299,7 +294,6 @@ public function findArray(Search $search): ArrayIterator $results, $this, $this->converter, - $this->serializer, $this->getScrollConfiguration($results, $search->getScroll()) ); } @@ -312,7 +306,6 @@ public function findRaw(Search $search): RawIterator $results, $this, $this->converter, - $this->serializer, $this->getScrollConfiguration($results, $search->getScroll()) ); } @@ -404,7 +397,6 @@ public function search(array $query, array $params = []): array public function bulk(string $operation, array $data = [], $autoCommit = true): array { $bulkParams = [ - '_index' => $this->getIndexName(), '_type' => $this->getTypeName(), '_id' => $data['_id'] ?? null, ]; @@ -439,7 +431,10 @@ public function bulk(string $operation, array $data = [], $autoCommit = true): a */ public function persist($document): void { - $documentArray = array_filter($this->converter->convertDocumentToArray($document, $this->serializer)); + $documentArray = array_filter($this->converter->convertDocumentToArray($document), function ($val) { + // remove unset properties but keep other falsy values + return !($val === null); + }); $this->bulk('index', $documentArray); } diff --git a/Tests/Functional/Result/PersistObjectsTest.php b/Tests/Functional/Result/PersistObjectsTest.php index bd3d633c..2a4716cb 100644 --- a/Tests/Functional/Result/PersistObjectsTest.php +++ b/Tests/Functional/Result/PersistObjectsTest.php @@ -12,6 +12,7 @@ namespace ONGR\ElasticsearchBundle\Tests\Functional\Result; use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; +use ONGR\App\Document\IndexWithFieldsDataDocument; use ONGR\App\Document\CollectionNested; use ONGR\App\Document\DummyDocument; @@ -38,10 +39,27 @@ public function testPersistObject() $nested->value = 'delta'; $document->getNestedCollection()->add($nested); + $document->setDatetimefield(new \DateTime('2010-01-01 10:10:56')); $index->persist($document); $index->commit(); $document = $index->find(5); $this->assertEquals('bar bar', $document->title); + $this->assertInstanceOf(\DateTimeInterface::class, $document->getDatetimefield()); + $this->assertEquals('2010-01-01', $document->getDatetimefield()->format('Y-m-d')); + } + + public function testAddingValuesToPrivateIdsWithoutSetters() + { + $index = $this->getIndex(IndexWithFieldsDataDocument::class); + + $document = new IndexWithFieldsDataDocument(); + $document->title = 'acme'; + + $index->persist($document); + $index->commit(); + + $document = $index->findOneBy(['private' => 'acme']); + $this->assertNotNull($document->getId()); } } diff --git a/Tests/Unit/Mapping/DocumentParserTest.php b/Tests/Unit/Mapping/DocumentParserTest.php index 71de8db6..d6d6d501 100644 --- a/Tests/Unit/Mapping/DocumentParserTest.php +++ b/Tests/Unit/Mapping/DocumentParserTest.php @@ -13,9 +13,12 @@ use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Cache\Cache; +use ONGR\App\Document\DummyDocument; +use ONGR\App\Document\TestDocument; use ONGR\App\Entity\DummyDocumentInTheEntityDirectory; use ONGR\ElasticsearchBundle\Mapping\DocumentParser; use PHPUnit\Framework\TestCase; +use Symfony\Component\Yaml\Yaml; class DocumentParserTest extends TestCase { @@ -23,7 +26,6 @@ public function testDocumentParsing() { $parser = new DocumentParser(new AnnotationReader(), $this->createMock(Cache::class)); - ; $indexMetadata = $parser->getIndexMetadata(new \ReflectionClass(DummyDocumentInTheEntityDirectory::class)); @@ -31,9 +33,9 @@ public function testDocumentParsing() 'mappings' => [ '_doc' => [ 'properties' => [ - 'keyword_field' => [ + 'keyword_field' => [ 'type' => 'keyword', - ] + ] ] ] ] @@ -41,4 +43,98 @@ public function testDocumentParsing() $this->assertEquals($expected, $indexMetadata); } + + public function testParsingWithMultiFieldsMapping() + { + $parser = new DocumentParser(new AnnotationReader(), $this->createMock(Cache::class)); + + $indexMetadata = $parser->getIndexMetadata(new \ReflectionClass(TestDocument::class)); + + // Mapping definition for field "title" should be there + $this->assertNotEmpty($indexMetadata['mappings']['_doc']['properties']['title']); + $title_field_def = $indexMetadata['mappings']['_doc']['properties']['title']; + + // title should have `fields` sub-array + $this->assertArrayHasKey('fields', $title_field_def); + + // `fields` should look like so: + $expected = [ + 'raw' => ['type' => 'keyword'], + 'increment' => ['type' => 'text', 'analyzer' => 'incrementalAnalyzer'], + 'sorting' => ['type' => 'keyword', 'normalizer' => 'lowercase_normalizer'] + ]; + + $this->assertEquals($expected, $title_field_def['fields']); + } + + public function testGetAnalysisConfig() + { + // Global analysis settings used for this test, usually set in the bundle configuration + // sets custom analyzer, filter, and normalizer + $config_analysis = [ + 'analyzer' => [ + 'incrementalAnalyzer' => [ + 'type' => 'custom', + 'tokenizer' => 'standard', + 'filter' => [ + 0 => 'lowercase', + 1 => 'edge_ngram_filter', + ], + ], + 'unusedAnalyzer' => [ + 'type' => 'custom', + 'tokenizer' => 'standard' + ] + ], + 'filter' => [ + 'edge_ngram_filter' => [ + 'type' => 'edge_ngram', + 'min_gram' => 1, + 'max_gram' => 20, + ], + ], + 'normalizer' => [ + 'lowercase_normalizer' => [ + 'type' => 'custom', + 'filter' => ['lowercase'] + ], + 'unused_normalizer' => [ + 'type' => 'custom' + ] + ] + ]; + + $parser = new DocumentParser(new AnnotationReader(), $this->createMock(Cache::class), $config_analysis); + $analysis = $parser->getAnalysisConfig(new \ReflectionClass(TestDocument::class)); + + $expected = [ + 'analyzer' => [ + 'incrementalAnalyzer' => [ + 'type' => 'custom', + 'tokenizer' => 'standard', + 'filter' => [ + 0 => 'lowercase', + 1 => 'edge_ngram_filter', + ], + ], + ], + // 'unusedAnalyzer' must not be there because it is not used + 'filter' => [ + 'edge_ngram_filter' => [ + 'type' => 'edge_ngram', + 'min_gram' => 1, + 'max_gram' => 20, + ], + ], + 'normalizer' => [ + 'lowercase_normalizer' => [ + 'type' => 'custom', + 'filter' => ['lowercase'] + ] + // 'unused_normalizer' must not be there + ] + ]; + + $this->assertEquals($expected, $analysis); + } } diff --git a/Tests/Unit/Result/AbstractResultsIteratorTest.php b/Tests/Unit/Result/AbstractResultsIteratorTest.php index 5c7e6e4a..2fe363ff 100644 --- a/Tests/Unit/Result/AbstractResultsIteratorTest.php +++ b/Tests/Unit/Result/AbstractResultsIteratorTest.php @@ -34,7 +34,7 @@ public function testClearScroll() $index->expects($this->once())->method('clearScroll')->with('foo'); $scroll = ['_scroll_id' => 'foo', 'duration' => '5m']; - $iterator = new RawIterator($rawData, $index, null, null, $scroll); + $iterator = new RawIterator($rawData, $index, null, $scroll); // Trigger destructor call unset($iterator); diff --git a/Tests/Unit/Result/DocumentIteratorTest.php b/Tests/Unit/Result/DocumentIteratorTest.php index f7b9d2d2..f19fde65 100644 --- a/Tests/Unit/Result/DocumentIteratorTest.php +++ b/Tests/Unit/Result/DocumentIteratorTest.php @@ -69,7 +69,7 @@ public function testResultConvert() $index->expects($this->any())->method('getNamespace')->willReturn(DummyDocument::class); - $iterator = new DocumentIterator($rawData, $index, $converter, $this->createMock(Serializer::class)); + $iterator = new DocumentIterator($rawData, $index, $converter); $this->assertEquals($document, $iterator->first()); } } diff --git a/Tests/app/src/Document/DummyDocument.php b/Tests/app/src/Document/DummyDocument.php index 111559d3..5fa0ecec 100644 --- a/Tests/app/src/Document/DummyDocument.php +++ b/Tests/app/src/Document/DummyDocument.php @@ -39,11 +39,9 @@ class DummyDocument * @ES\Property( * type="text", * name="title", - * settings={ - * "fields"={ - * "raw"={"type"="keyword"}, - * "increment"={"type"="text", "analyzer"="incrementalAnalyzer"} - * } + * fields={ + * "raw"={"type"="keyword"}, + * "increment"={"type"="text", "analyzer"="incrementalAnalyzer"} * } * ) */ @@ -69,6 +67,12 @@ class DummyDocument */ private $objectCollection; + /** + * @var \DateTimeInterface + * @ES\Property(type="date") + */ + private $datetimefield; + public function __construct() { $this->nestedCollection = new ArrayCollection(); @@ -107,4 +111,15 @@ public function setObjectCollection($objectCollection) $this->objectCollection = $objectCollection; return $this; } + + public function getDatetimefield(): ?\DateTimeInterface + { + return $this->datetimefield; + } + + public function setDatetimefield(\DateTimeInterface $datetimefield): void + { + $this->datetimefield = $datetimefield; + } + } diff --git a/Tests/app/src/Document/IndexWithFieldsDataDocument.php b/Tests/app/src/Document/IndexWithFieldsDataDocument.php index f31fe6ba..5b0279c2 100644 --- a/Tests/app/src/Document/IndexWithFieldsDataDocument.php +++ b/Tests/app/src/Document/IndexWithFieldsDataDocument.php @@ -26,10 +26,18 @@ class IndexWithFieldsDataDocument /** * @ES\Id() */ - public $id; + private $id; /** * @ES\Property(type="text", name="private", settings={"fielddata"=true}) */ public $title; + + /** + * @return mixed + */ + public function getId() + { + return $this->id; + } } diff --git a/Tests/app/src/Document/TestDocument.php b/Tests/app/src/Document/TestDocument.php new file mode 100644 index 00000000..9518ed79 --- /dev/null +++ b/Tests/app/src/Document/TestDocument.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\App\Document; + +use Doctrine\Common\Collections\ArrayCollection; +use ONGR\ElasticsearchBundle\Annotation as ES; + +/** + * test document for unit testing of DocumentParser class + * + * @ES\Index(alias="testdocument") + */ +class TestDocument +{ + // This con't is only as a helper. + CONST INDEX_NAME = 'testdocument'; + + /** + * @ES\Id() + */ + public $id; + + /** + * @ES\Property( + * type="text", + * name="title", + * fields={ + * "raw"={"type"="keyword"}, + * "increment"={"type"="text", "analyzer"="incrementalAnalyzer"}, + * "sorting"={"type"="keyword", "normalizer"="lowercase_normalizer"} + * } + * ) + */ + public $title; +} diff --git a/composer.json b/composer.json index 8526765d..988b1e0a 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,6 @@ "symfony/dependency-injection": "^3.4|^4.1", "symfony/console": "^3.4|^4.1", "symfony/stopwatch": "^3.4|^4.1", - "symfony/templating": "^3.4|^4.1", "symfony/finder": "^3.4|^4.1", "symfony/serializer": "^3.4|^4.1", "symfony/cache": "^3.4|^4.1", @@ -56,7 +55,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "6.0-dev" + "dev-master": "6.1-dev" } } }