diff --git a/DTO/IndicesAndTypesMetadataCollection.php b/DTO/IndicesAndTypesMetadataCollection.php deleted file mode 100644 index 455caf5..0000000 --- a/DTO/IndicesAndTypesMetadataCollection.php +++ /dev/null @@ -1,53 +0,0 @@ - => [ - * => DocumentMetadata - * ... - * ] - * ... - * - * @var array - */ - private $metadata = []; - - /** - * Sets the metadata for a combination of a real/physical index name and a type - * - * @param DocumentMetadata $documentMetadata - * @param string $realIndex The physical index name - */ - public function setTypeMetadata(DocumentMetadata $documentMetadata, $realIndex = null) - { - $this->metadata[$realIndex ?: '*'][$documentMetadata->getType()] = $documentMetadata; - } - - /** - * Returns the metadata for a combination of a physical index name and a type - * - * @param string $type The Elasticsearch type - * @param string $realIndex The physical index name - * @return DocumentMetadata - */ - public function getTypeMetadata($type, $realIndex) - { - if (isset($this->metadata[$realIndex][$type])) { - return $this->metadata[$realIndex][$type]; - } elseif (isset($this->metadata['*'][$type])) { - return $this->metadata['*'][$type]; - } else { - throw new \InvalidArgumentException(sprintf('No metadata available for type "%s" in index "%s"', $type, $realIndex)); - } - } -} diff --git a/DTO/TypesToDocumentClasses.php b/DTO/TypesToDocumentClasses.php new file mode 100644 index 0000000..e21d31f --- /dev/null +++ b/DTO/TypesToDocumentClasses.php @@ -0,0 +1,52 @@ + => [ + * => + * ... + * ] + * ... + * + * @var array + */ + private $documentClasses = []; + + /** + * Set the document class for a combination of a physical index name and a type + * When all the needed types are unique across indices, `null` can be passed for $index + * + * @param string $index The name of the physical index in Elasticsearch + * @param string $type The name of the type in Elasticsearch + * @param string $documentClass The document class in short notation + */ + public function set($index, $type, $documentClass) + { + $this->documentClasses[$index ?: '*'][$type] = $documentClass; + } + + /** + * Get the document class for a combination of a physical index name and a type + * + * @param string $index The name of the physical index in Elasticsearch + * @param string $type The name of the type in Elasticsearch + * @return string + */ + public function get($index, $type) + { + if (isset($this->documentClasses[$index][$type])) { + return $this->documentClasses[$index][$type]; + } elseif (isset($this->documentClasses['*'][$type])) { + return $this->documentClasses['*'][$type]; + } else { + throw new \InvalidArgumentException(sprintf('Document class for type "%s" in index "%s" is not set', $type, $index)); + } + } +} diff --git a/Document/AbstractDocument.php b/Document/AbstractDocument.php index 34f919f..80da7f7 100644 --- a/Document/AbstractDocument.php +++ b/Document/AbstractDocument.php @@ -11,135 +11,38 @@ abstract class AbstractDocument implements DocumentInterface { /** * @var string + * + * @ES\Property(type="string", name="_id") */ - private $id; + public $id; /** * @var string + * + * @ES\Property(type="float", name="_score") */ - private $score; + public $score; /** * @var string + * + * @ES\Property(type="string", name="_parent") */ - private $parent; + public $parent; /** * @var string + * + * @ES\Property(type="string", name="_ttl") */ - private $ttl; + public $ttl; /** * When document is cloned id is set to null. */ public function __clone() { - $this->setId(null); + $this->id = null; } - /** - * Sets document unique id. - * - * @param string $documentId - * - * @return $this - */ - public function setId($documentId) - { - $this->id = $documentId; - - return $this; - } - - /** - * Returns document id. - * - * @return string - */ - public function getId() - { - return $this->id; - } - - /** - * Sets document score. - * - * @param string $documentScore - * - * @return $this - */ - public function setScore($documentScore) - { - $this->score = $documentScore; - - return $this; - } - - /** - * Gets document score. - * - * @return string - */ - public function getScore() - { - return $this->score; - } - - /** - * Sets parent document id. - * - * @param string $parent - * - * @return $this - */ - public function setParent($parent) - { - $this->parent = $parent; - - return $this; - } - - /** - * Returns parent document id. - * - * @return null|string - */ - public function getParent() - { - return $this->parent; - } - - /** - * Checks if document has a parent. - * - * @return bool - */ - public function hasParent() - { - return $this->parent !== null; - } - - /** - * Sets time to live timestamp. - * - * @param string $ttl - * - * @return $this - */ - public function setTtl($ttl) - { - $this->ttl = $ttl; - - return $this; - } - - /** - * Returns time to live value. - * - * @return int - */ - public function getTtl() - { - return $this->ttl; - } } diff --git a/Document/DocumentInterface.php b/Document/DocumentInterface.php index 167a727..e5a0a01 100644 --- a/Document/DocumentInterface.php +++ b/Document/DocumentInterface.php @@ -7,74 +7,4 @@ */ interface DocumentInterface { - /** - * Sets document unique id. - * - * @param string $documentId - * - * @return $this - */ - public function setId($documentId); - - /** - * Returns document id. - * - * @return string - */ - public function getId(); - - /** - * Sets document score. - * - * @param string $documentScore - * - * @return $this - */ - public function setScore($documentScore); - - /** - * Gets document score. - * - * @return string - */ - public function getScore(); - - /** - * Sets parent document id. - * - * @param string $parent - * - * @return $this - */ - public function setParent($parent); - - /** - * Returns parent document id. - * - * @return null|string - */ - public function getParent(); - - /** - * Checks if document has a parent. - * - * @return bool - */ - public function hasParent(); - - /** - * Sets time to live timestamp. - * - * @param int $ttl - * - * @return $this - */ - public function setTtl($ttl); - - /** - * Returns time to live value. - * - * @return int - */ - public function getTtl(); } diff --git a/Document/Repository/Repository.php b/Document/Repository/Repository.php index 6eab5c7..51dbc45 100644 --- a/Document/Repository/Repository.php +++ b/Document/Repository/Repository.php @@ -2,10 +2,8 @@ namespace Sineflow\ElasticsearchBundle\Document\Repository; -use Elasticsearch\Common\Exceptions\Missing404Exception; use Sineflow\ElasticsearchBundle\Document\DocumentInterface; use Sineflow\ElasticsearchBundle\Finder\Finder; -use Sineflow\ElasticsearchBundle\Result\Converter; use Sineflow\ElasticsearchBundle\Manager\IndexManager; use Sineflow\ElasticsearchBundle\Mapping\DocumentMetadata; diff --git a/Finder/Finder.php b/Finder/Finder.php index 36649d1..be43968 100644 --- a/Finder/Finder.php +++ b/Finder/Finder.php @@ -3,12 +3,12 @@ namespace Sineflow\ElasticsearchBundle\Finder; use Elasticsearch\Common\Exceptions\Missing404Exception; +use Sineflow\ElasticsearchBundle\DTO\TypesToDocumentClasses; use Sineflow\ElasticsearchBundle\Manager\ConnectionManager; use Sineflow\ElasticsearchBundle\Manager\IndexManagerRegistry; use Sineflow\ElasticsearchBundle\Mapping\DocumentMetadataCollector; -use Sineflow\ElasticsearchBundle\DTO\IndicesAndTypesMetadataCollection; use Sineflow\ElasticsearchBundle\Paginator\KnpPaginatorAdapter; -use Sineflow\ElasticsearchBundle\Result\Converter; +use Sineflow\ElasticsearchBundle\Result\DocumentConverter; use Sineflow\ElasticsearchBundle\Result\DocumentIterator; /** @@ -34,6 +34,11 @@ class Finder */ private $indexManagerRegistry; + /** + * @var DocumentConverter + */ + private $documentConverter; + /** * @var string */ @@ -43,12 +48,18 @@ class Finder * Finder constructor. * @param DocumentMetadataCollector $documentMetadataCollector * @param IndexManagerRegistry $indexManagerRegistry + * @param DocumentConverter $documentConverter * @param string $languageSeparator */ - public function __construct(DocumentMetadataCollector $documentMetadataCollector, IndexManagerRegistry $indexManagerRegistry, $languageSeparator) + public function __construct( + DocumentMetadataCollector $documentMetadataCollector, + IndexManagerRegistry $indexManagerRegistry, + DocumentConverter $documentConverter, + $languageSeparator) { $this->documentMetadataCollector = $documentMetadataCollector; $this->indexManagerRegistry = $indexManagerRegistry; + $this->documentConverter = $documentConverter; $this->languageSeparator = $languageSeparator; } @@ -82,7 +93,7 @@ public function get($documentClass, $id, $resultType = self::RESULTS_OBJECT) switch ($resultType & self::BITMASK_RESULT_TYPES) { case self::RESULTS_OBJECT: - return (new Converter($documentMetadata, $this->languageSeparator))->convertToDocument($raw); + return $this->documentConverter->convertToDocument($raw, $documentClass); case self::RESULTS_ARRAY: return $this->convertToNormalizedArray($raw); case self::RESULTS_RAW: @@ -188,76 +199,52 @@ public function getTargetIndicesAndTypes(array $documentClasses) * Returns a mapping of live indices and types to the document classes in short notation that represent them * * @param string[] $documentClasses - * @return IndicesAndTypesMetadataCollection + * @return TypesToDocumentClasses */ - private function getIndicesAndTypesMetadataCollection(array $documentClasses) + private function getTypesToDocumentClasses(array $documentClasses) { - $allDocumentClassToIndexMappings = $this->documentMetadataCollector->getDocumentClassesIndices(); - $documentClassToIndexMap = array_intersect_key($allDocumentClassToIndexMappings, array_flip($documentClasses)); - $typesMetadataCollection = new IndicesAndTypesMetadataCollection(); + $typesToDocumentClasses = new TypesToDocumentClasses(); + + $documentClassToIndexMap = $this->documentMetadataCollector->getDocumentClassesIndices($documentClasses); + $documentClassToTypeMap = $this->documentMetadataCollector->getDocumentClassesTypes($documentClasses); $getLiveIndices = false; - $classToTypeMap = $this->documentMetadataCollector->getClassToTypeMap($documentClasses); // If there are duplicate type names across the indices we're querying - if (count($classToTypeMap) > count(array_unique($classToTypeMap))) { + if (count($documentClassToTypeMap) > count(array_unique($documentClassToTypeMap))) { // We'll need to get the live index name for each type, so we can correctly map the results to the appropriate objects $getLiveIndices = true; } foreach ($documentClassToIndexMap as $documentClass => $indexManagerName) { - // Build mappings of indices and types to metadata, for the Converter + // Build mappings of indices and types to document class names, for the Converter $liveIndex = $getLiveIndices ? $this->indexManagerRegistry->get($indexManagerName)->getLiveIndex() : null; - $documentMetadata = $this->documentMetadataCollector->getDocumentMetadata($documentClass); - $typesMetadataCollection->setTypeMetadata($documentMetadata, $liveIndex); + $typesToDocumentClasses->set($liveIndex, $documentClassToTypeMap[$documentClass], $documentClass); } - return $typesMetadataCollection; + return $typesToDocumentClasses; } + private function parseResult($raw, $resultsType, array $documentClasses = null) { switch ($resultsType & self::BITMASK_RESULT_TYPES) { case self::RESULTS_OBJECT: - // TODO: add the DocumentScanIterator -// if (isset($raw['_scroll_id'])) { -// $iterator = new DocumentScanIterator( -// $raw, -// $this->getManager()->getTypesMapping(), -// $this->getManager()->getBundlesMapping() -// ); -// $iterator -// ->setRepository($this) -// ->setScrollDuration($scrollDuration) -// ->setScrollId($raw['_scroll_id']); -// -// return $iterator; -// } - if (empty($documentClasses)) { throw new \InvalidArgumentException('$documentClasses must be specified when retrieving results as objects'); } return new DocumentIterator( $raw, - $this->getIndicesAndTypesMetadataCollection($documentClasses), - $this->languageSeparator + $this->documentConverter, + $this->getTypesToDocumentClasses($documentClasses) ); + case self::RESULTS_ARRAY: return $this->convertToNormalizedArray($raw); + case self::RESULTS_RAW: return $raw; -// case self::RESULTS_RAW_ITERATOR: -// if (isset($raw['_scroll_id'])) { -// $iterator = new RawResultScanIterator($raw); -// $iterator -// ->setRepository($this) -// ->setScrollDuration($scrollDuration) -// ->setScrollId($raw['_scroll_id']); -// -// return $iterator; -// } -// -// return new RawResultIterator($raw); + default: throw new \InvalidArgumentException('Wrong results type selected'); } diff --git a/Manager/IndexManager.php b/Manager/IndexManager.php index 3e4c9fc..d1d31c6 100644 --- a/Manager/IndexManager.php +++ b/Manager/IndexManager.php @@ -14,7 +14,7 @@ use Sineflow\ElasticsearchBundle\Finder\Finder; use Sineflow\ElasticsearchBundle\Mapping\DocumentMetadata; use Sineflow\ElasticsearchBundle\Mapping\DocumentMetadataCollector; -use Sineflow\ElasticsearchBundle\Result\Converter; +use Sineflow\ElasticsearchBundle\Result\DocumentConverter; /** * Manager class. @@ -47,14 +47,14 @@ class IndexManager private $finder; /** - * @var array + * @var DocumentConverter */ - private $indexSettings; + private $documentConverter; /** - * @var Converter[] + * @var array */ - private $convertersCache; + private $indexSettings; /** * @var RepositoryInterface[] @@ -87,6 +87,7 @@ class IndexManager * @param DocumentMetadataCollector $metadataCollector * @param ProviderRegistry $providerRegistry * @param Finder $finder + * @param DocumentConverter $documentConverter * @param array $indexSettings * @param string $languageSeparator */ @@ -96,6 +97,7 @@ public function __construct( DocumentMetadataCollector $metadataCollector, ProviderRegistry $providerRegistry, Finder $finder, + DocumentConverter $documentConverter, array $indexSettings, $languageSeparator) { @@ -104,6 +106,7 @@ public function __construct( $this->metadataCollector = $metadataCollector; $this->providerRegistry = $providerRegistry; $this->finder = $finder; + $this->documentConverter = $documentConverter; $this->indexSettings = $indexSettings; $this->languageSeparator = $languageSeparator; @@ -534,13 +537,8 @@ public function reindex($documentClass, $id) */ public function persist(DocumentInterface $document) { - $documentClass = get_class($document); - $documentMetadata = $this->metadataCollector->getDocumentMetadata($documentClass); - $converter = isset($this->convertersCache[$documentClass]) ? - $this->convertersCache[$documentClass] : new Converter($documentMetadata, $this->languageSeparator); - - $documentArray = $converter->convertToArray($document); - + $documentMetadata = $this->metadataCollector->getDocumentMetadata(get_class($document)); + $documentArray = $this->documentConverter->convertToArray($document); $this->persistRaw($documentMetadata->getType(), $documentArray); } diff --git a/Manager/IndexManagerFactory.php b/Manager/IndexManagerFactory.php index 5c91604..4d04b1f 100644 --- a/Manager/IndexManagerFactory.php +++ b/Manager/IndexManagerFactory.php @@ -5,6 +5,7 @@ use Sineflow\ElasticsearchBundle\Document\Provider\ProviderRegistry; use Sineflow\ElasticsearchBundle\Finder\Finder; use Sineflow\ElasticsearchBundle\Mapping\DocumentMetadataCollector; +use Sineflow\ElasticsearchBundle\Result\DocumentConverter; /** * Factory for index manager services @@ -26,6 +27,11 @@ class IndexManagerFactory */ private $finder; + /** + * @var DocumentConverter + */ + private $documentConverter; + /** * @var string The separator string between property names and language codes for ML properties */ @@ -40,17 +46,20 @@ class IndexManagerFactory * @param DocumentMetadataCollector $metadataCollector * @param ProviderRegistry $providerRegistry * @param Finder $finder + * @param DocumentConverter $documentConverter * @param string $languageSeparator */ public function __construct( DocumentMetadataCollector $metadataCollector, ProviderRegistry $providerRegistry, Finder $finder, + DocumentConverter $documentConverter, $languageSeparator) { $this->metadataCollector = $metadataCollector; $this->providerRegistry = $providerRegistry; $this->finder = $finder; + $this->documentConverter = $documentConverter; $this->languageSeparator = $languageSeparator; } @@ -71,6 +80,7 @@ public function createManager( $this->metadataCollector, $this->providerRegistry, $this->finder, + $this->documentConverter, $this->getIndexParams($managerName, $indexSettings), $this->languageSeparator ); diff --git a/Mapping/DocumentMetadata.php b/Mapping/DocumentMetadata.php index de5971d..15c4fb3 100644 --- a/Mapping/DocumentMetadata.php +++ b/Mapping/DocumentMetadata.php @@ -9,6 +9,9 @@ */ class DocumentMetadata { + const PROPERTY_ACCESS_PUBLIC = 1; + const PROPERTY_ACCESS_PRIVATE = 2; + /** * @var array */ diff --git a/Mapping/DocumentMetadataCollector.php b/Mapping/DocumentMetadataCollector.php index efa0095..a28be61 100644 --- a/Mapping/DocumentMetadataCollector.php +++ b/Mapping/DocumentMetadataCollector.php @@ -131,7 +131,8 @@ private function fetchDocumentsMetadata() } /** - * Returns metadata for the specified document class short name (e.g AppBundle:Product) + * Returns metadata for the specified document class name. + * Class can also be specified in short notation (e.g AppBundle:Product) * * @param string $documentClass * @return DocumentMetadata @@ -173,7 +174,7 @@ public function getDocumentsMetadataForIndex($indexManagerName) * @param array $documentClasses Only return those classes if specified * @return array */ - public function getClassToTypeMap(array $documentClasses = []) + public function getDocumentClassesTypes(array $documentClasses = []) { $result = []; foreach ($this->metadata as $index => $documentsMetadata) { @@ -191,19 +192,24 @@ public function getClassToTypeMap(array $documentClasses = []) } /** - * Returns all document classes in the collection as keys and the corresponding index manager that manages them as values + * Returns all document classes as keys and the corresponding index manager that manages them as values * + * @param array $documentClasses Only return those classes if specified * @return array */ - public function getDocumentClassesIndices() + public function getDocumentClassesIndices(array $documentClasses = []) { $result = []; - foreach ($this->metadata as $index => $types) { - foreach ($types as $typeDocumentClass => $documentMetadata) { - $result[$typeDocumentClass] = $index; + foreach ($this->metadata as $index => $documentsMetadata) { + foreach ($documentsMetadata as $documentClass => $documentMetadata) { + $result[$documentClass] = $index; } } + if ($documentClasses) { + $result = array_intersect_key($result, array_flip($documentClasses)); + } + return $result; } diff --git a/Mapping/DocumentParser.php b/Mapping/DocumentParser.php index cb996d7..cdd4cef 100644 --- a/Mapping/DocumentParser.php +++ b/Mapping/DocumentParser.php @@ -174,18 +174,19 @@ private function getObjects() */ private function getPropertiesMetadata(\ReflectionClass $documentReflection) { - $reflectionName = $documentReflection->getName(); - if (array_key_exists($reflectionName, $this->propertiesMetadata)) { - return $this->propertiesMetadata[$reflectionName]; + $className = $documentReflection->getName(); + if (array_key_exists($className, $this->propertiesMetadata)) { + return $this->propertiesMetadata[$className]; } $propertyMetadata = []; + /** @var \ReflectionProperty $property */ - foreach ($this->getDocumentPropertiesReflection($documentReflection) as $name => $property) { + foreach ($this->getDocumentPropertiesReflection($documentReflection) as $propertyName => $property) { $propertyAnnotation = $this->getPropertyAnnotationData($property); if ($propertyAnnotation !== null) { $propertyMetadata[$propertyAnnotation->name] = [ - 'propertyName' => $name, + 'propertyName' => $propertyName, 'type' => $propertyAnnotation->type, ]; @@ -202,7 +203,9 @@ private function getPropertiesMetadata(\ReflectionClass $documentReflection) // If property is a (nested) object if (in_array($propertyAnnotation->type, ['object', 'nested'])) { if (!$propertyAnnotation->objectName) { - throw new \InvalidArgumentException(sprintf('Property "%s" in %s is missing "objectName" setting', $name, $reflectionName)); + throw new \InvalidArgumentException( + sprintf('Property "%s" in %s is missing "objectName" setting', $propertyName, $className) + ); } $child = new \ReflectionClass($this->documentLocator->resolveClassName($propertyAnnotation->objectName)); $propertyMetadata[$propertyAnnotation->name] = array_merge( @@ -214,12 +217,32 @@ private function getPropertiesMetadata(\ReflectionClass $documentReflection) ] ); } + + if ($property->isPublic()) { + $propertyAccess = DocumentMetadata::PROPERTY_ACCESS_PUBLIC; + } else { + $propertyAccess = DocumentMetadata::PROPERTY_ACCESS_PRIVATE; + $camelCaseName = ucfirst(Caser::camel($propertyName)); + $getterMethod = 'get'.$camelCaseName; + $setterMethod = 'set'.$camelCaseName; + if ($documentReflection->hasMethod($getterMethod) && $documentReflection->hasMethod($setterMethod)) { + $propertyMetadata[$propertyAnnotation->name]['methods'] = [ + 'getter' => $getterMethod, + 'setter' => $setterMethod, + ]; + } else { + $message = sprintf('Property "%s" either needs to be public or %s() and %s() methods must be defined', $propertyName, $getterMethod, $setterMethod); + throw new \LogicException($message); + } + } + + $propertyMetadata[$propertyAnnotation->name]['propertyAccess'] = $propertyAccess; } } - $this->propertiesMetadata[$reflectionName] = $propertyMetadata; + $this->propertiesMetadata[$className] = $propertyMetadata; - return $this->propertiesMetadata[$reflectionName]; + return $this->propertiesMetadata[$className]; } /** @@ -286,7 +309,7 @@ private function getProperties(\ReflectionClass $documentReflection, array $inde { $mapping = []; /** @var \ReflectionProperty $property */ - foreach ($this->getDocumentPropertiesReflection($documentReflection) as $name => $property) { + foreach ($this->getDocumentPropertiesReflection($documentReflection) as $propertyName => $property) { $propertyAnnotation = $this->getPropertyAnnotationData($property); if (empty($propertyAnnotation)) { @@ -346,15 +369,15 @@ private function getPropertyMapping(AbstractProperty $propertyAnnotation, $langu */ private function getObjectMapping($objectName, array $indexAnalyzers = []) { - $namespace = $this->documentLocator->resolveClassName($objectName); + $className = $this->documentLocator->resolveClassName($objectName); - if (array_key_exists($namespace, $this->objects)) { - return $this->objects[$namespace]; + if (array_key_exists($className, $this->objects)) { + return $this->objects[$className]; } - $this->objects[$namespace] = $this->getRelationMapping(new \ReflectionClass($namespace), $indexAnalyzers); + $this->objects[$className] = $this->getRelationMapping(new \ReflectionClass($className), $indexAnalyzers); - return $this->objects[$namespace]; + return $this->objects[$className]; } /** diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 50020aa..8586f68 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -4,6 +4,7 @@ parameters: sfes.profiler.template: SineflowElasticsearchBundle:Profiler:profiler.html.twig sfes.mlproperty.language_separator: '-' + sfes.document_converter.class: Sineflow\ElasticsearchBundle\Result\DocumentConverter sfes.provider_registry.class: Sineflow\ElasticsearchBundle\Document\Provider\ProviderRegistry sfes.provider_self.class: Sineflow\ElasticsearchBundle\Document\Provider\ElasticsearchProvider sfes.index_manager.class: Sineflow\ElasticsearchBundle\Manager\IndexManager @@ -19,6 +20,11 @@ parameters: sfes.knp_paginate_query_subscriber_class: Sineflow\ElasticsearchBundle\Subscriber\KnpPaginateQuerySubscriber services: + sfes.document_converter: + class: %sfes.document_converter.class% + arguments: [@sfes.document_metadata_collector, %sfes.mlproperty.language_separator%] + + sfes.provider_registry: class: %sfes.provider_registry.class% calls: @@ -26,7 +32,7 @@ services: sfes.index_manager_factory: class: %sfes.index_manager_factory.class% - arguments: [@sfes.document_metadata_collector, @sfes.provider_registry, @sfes.finder, %sfes.mlproperty.language_separator%] + arguments: [@sfes.document_metadata_collector, @sfes.provider_registry, @sfes.finder, @sfes.document_converter, %sfes.mlproperty.language_separator%] public: false sfes.index_manager_registry: @@ -37,7 +43,7 @@ services: sfes.finder: class: %sfes.finder.class% - arguments: [@sfes.document_metadata_collector, @sfes.index_manager_registry, %sfes.mlproperty.language_separator%] + arguments: [@sfes.document_metadata_collector, @sfes.index_manager_registry, @sfes.document_converter, %sfes.mlproperty.language_separator%] sfes.document_locator: class: %sfes.document_locator.class% diff --git a/Result/AbstractResultsIterator.php b/Result/AbstractResultsIterator.php deleted file mode 100644 index 23458d7..0000000 --- a/Result/AbstractResultsIterator.php +++ /dev/null @@ -1,159 +0,0 @@ -offsetGet($this->key()); - } - - /** - * {@inheritdoc} - */ - public function next() - { - next($this->documents); - } - - /** - * {@inheritdoc} - */ - public function key() - { - return key($this->documents); - } - - /** - * {@inheritdoc} - */ - public function valid() - { - return $this->key() !== null; - } - - /** - * {@inheritdoc} - */ - public function rewind() - { - reset($this->documents); - } - - /** - * {@inheritdoc} - */ - public function offsetExists($offset) - { - return array_key_exists($offset, $this->documents); - } - - /** - * {@inheritdoc} - */ - public function offsetGet($offset) - { - if (!isset($this->converted[$offset])) { - if (!isset($this->documents[$offset])) { - return null; - } - - $this->converted[$offset] = $this->convertDocument($this->documents[$offset]); - - // Clear memory. - $this->documents[$offset] = null; - } - - return $this->converted[$offset]; - } - - /** - * {@inheritdoc} - */ - public function offsetSet($offset, $value) - { - if ($offset === null) { - $offset = $this->getKey(); - } - - if (is_object($value)) { - $this->converted[$offset] = $value; - $this->documents[$offset] = null; - } elseif (is_array($value)) { - $this->documents[$offset] = $value; - // Also invalidate converted document. - unset($this->converted[$offset]); - } - } - - /** - * {@inheritdoc} - */ - public function offsetUnset($offset) - { - unset($this->documents[$offset], $this->converted[$offset]); - } - - /** - * {@inheritdoc} - */ - public function count() - { - return count($this->documents); - } - - /** - * Rewind's the iteration and returns first result. - * - * @return mixed|null - */ - public function first() - { - $this->rewind(); - - return $this->current(); - } - - /** - * Return an integer key to be used for a new element in array. - * - * @return int - */ - private function getKey() - { - $currentIntKeys = array_filter(array_keys($this->documents), 'is_int'); - if (empty($currentIntKeys)) { - $offset = 0; - } else { - $offset = max($currentIntKeys) + 1; - } - - return $offset; - } -} diff --git a/Result/Converter.php b/Result/Converter.php deleted file mode 100644 index 0f09c6e..0000000 --- a/Result/Converter.php +++ /dev/null @@ -1,300 +0,0 @@ -documentMetadata = $documentMetadata; - $this->languageSeparator = $languageSeparator; - } - - /** - * Converts raw array to document. - * - * @param array $rawData - * - * @return DocumentInterface - * - * @throws \LogicException - */ - public function convertToDocument($rawData) - { - $data = isset($rawData['_source']) ? $rawData['_source'] : array_map('reset', $rawData['fields']); - - /** @var DocumentInterface $object */ - $className = $this->documentMetadata->getClassName(); - $object = $this->assignArrayToObject($data, new $className(), $this->documentMetadata->getPropertiesMetadata()); - - $this->setObjectFields($object, $rawData, ['_id', '_score', 'fields _parent', 'fields _ttl']); - - return $object; - } - - /** - * Assigns all properties to object. Could be a document or a (nested) object. - * - * @param array $array - * @param object $object - * @param array $propertiesMetadata - * - * @return object - */ - public function assignArrayToObject(array $array, $object, array $propertiesMetadata) - { - foreach ($propertiesMetadata as $esField => $propertyMetadata) { - // Skip fields from the mapping that have no value set, unless they are multilanguage fields - if (empty($propertyMetadata['multilanguage']) && !isset($array[$esField])) { - continue; - } - - if ($propertyMetadata['type'] === 'string' && !empty($propertyMetadata['multilanguage'])) { - $objectValue = new MLProperty(); - foreach ($array as $fieldName => $value) { - $prefixLength = strlen($esField . $this->languageSeparator); - if (substr($fieldName, 0, $prefixLength) === $esField . $this->languageSeparator) { - $language = substr($fieldName, $prefixLength); - $objectValue->setValue($value, $language); - } - } - - } elseif ($propertyMetadata['type'] === 'date') { - $objectValue = \DateTime::createFromFormat( - // TODO: is this 'format' field being set anywhere at all? - isset($propertyMetadata['format']) ? $propertyMetadata['format'] : \DateTime::ISO8601, - $array[$esField] - ) ?: $array[$esField]; - - } elseif (in_array($propertyMetadata['type'], ['object', 'nested'])) { - if ($propertyMetadata['multiple']) { - $objectValue = new ObjectIterator($this, $array[$esField], $propertyMetadata); - } else { - $objectValue = $this->assignArrayToObject( - $array[$esField], - new $propertyMetadata['className'](), - $propertyMetadata['propertiesMetadata'] - ); - } - - } else { - $objectValue = $array[$esField]; - } - - $this->getPropertyAccessor()->setValue($object, $propertyMetadata['propertyName'], $objectValue); - } - - return $object; - } - - /** - * Converts document or (nested) object to an array. - * - * @param object $object - * @param array $propertiesMetadata - * - * @return array - */ - public function convertToArray($object, $propertiesMetadata = []) - { - if (empty($propertiesMetadata)) { - $propertiesMetadata = $this->documentMetadata->getPropertiesMetadata(); - } - - $array = []; - // Special fields. - if ($object instanceof DocumentInterface) { - $this->setArrayFields($array, $object, ['_id', '_parent', '_ttl']); - } - - // Variable $name defined in client. - foreach ($propertiesMetadata as $name => $propertyMetadata) { - $value = $this->getPropertyAccessor()->getValue($object, $propertyMetadata['propertyName']); - - if (isset($value)) { - if (array_key_exists('propertiesMetadata', $propertyMetadata)) { - $new = []; - if ($propertyMetadata['multiple']) { - $this->isTraversable($value); - foreach ($value as $item) { - $this->checkVariableType($item, [$propertyMetadata['className']]); - $new[] = $this->convertToArray($item, $propertyMetadata['propertiesMetadata']); - } - } else { - $this->checkVariableType($value, [$propertyMetadata['className']]); - $new = $this->convertToArray($value, $propertyMetadata['propertiesMetadata']); - } - $value = $new; - } - - if ($value instanceof \DateTime) { - $value = $value->format(isset($propertyMetadata['format']) ? $propertyMetadata['format'] : \DateTime::ISO8601); - } - - if ($value instanceof MLProperty) { - foreach ($value->getValues() as $language => $langValue) { - $array[$name . $this->languageSeparator . $language] = $langValue; - } - } else { - $array[$name] = $value; - } - } - } - - return $array; - } - - /** - * Sets fields into object from raw response. - * - * @param object $object Object to set values to. - * @param array $rawResponse Array to take values from. - * @param array $fields Values to take. - */ - private function setObjectFields($object, $rawResponse, $fields = []) - { - foreach ($fields as $field) { - $path = $this->getPropertyPathToAccess($field); - $value = $this->getPropertyAccessor()->getValue($rawResponse, $path); - - if ($value !== null) { - $this->getPropertyAccessor()->setValue($object, $this->getPropertyToAccess($field), $value); - } - } - } - - /** - * Sets fields into array from object. - * - * @param array $array To set values to. - * @param object $object Take values from. - * @param array $fields Fields to set. - */ - private function setArrayFields(&$array, $object, $fields = []) - { - foreach ($fields as $field) { - $value = $this->getPropertyAccessor()->getValue($object, $this->getPropertyToAccess($field)); - - if ($value !== null) { - $this - ->getPropertyAccessor() - ->setValue($array, $this->getPropertyPathToAccess($field), $value); - } - } - } - - /** - * Returns property to access for object used by property accessor. - * - * @param string $field - * - * @return string - */ - private function getPropertyToAccess($field) - { - $deep = strpos($field, ' '); - if ($deep !== false) { - $field = substr($field, $deep + 1); - } - - return $field; - } - - /** - * Returns property to access for array used by property accessor. - * - * @param string $field - * - * @return string - */ - private function getPropertyPathToAccess($field) - { - return '[' . str_replace(' ', '][', $field) . ']'; - } - - /** - * Check if class matches the expected one. - * - * @param object $object - * @param array $expectedClasses - * - * @throws \InvalidArgumentException - */ - private function checkVariableType($object, array $expectedClasses) - { - if (!is_object($object)) { - $msg = 'Expected variable of type object, got ' . gettype($object) . ". (field isn't multiple)"; - throw new \InvalidArgumentException($msg); - } - - $class = get_class($object); - if (!in_array($class, $expectedClasses)) { - throw new \InvalidArgumentException("Expected object of type {$expectedClasses[0]}, got {$class}."); - } - } - - /** - * Check if object is traversable, throw exception otherwise. - * - * @param mixed $value - * - * @return bool - * - * @throws \InvalidArgumentException - */ - private function isTraversable($value) - { - if (!(is_array($value) || (is_object($value) && $value instanceof \Traversable))) { - throw new \InvalidArgumentException("Variable isn't traversable, although field is set to multiple."); - } - - return true; - } - - /** - * Returns property accessor instance. - * - * @return PropertyAccessor - */ - private function getPropertyAccessor() - { - if (!$this->propertyAccessor) { - $this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() - ->enableMagicCall() - ->getPropertyAccessor(); - } - - return $this->propertyAccessor; - } -} diff --git a/Result/DocumentConverter.php b/Result/DocumentConverter.php new file mode 100644 index 0000000..19ab0f3 --- /dev/null +++ b/Result/DocumentConverter.php @@ -0,0 +1,210 @@ +metadataCollector = $metadataCollector; + $this->languageSeparator = $languageSeparator; + } + + + /** + * Converts raw array to document. + * + * @param array $rawData + * @param string $documentClass Document class in short notation (e.g. AppBundle:Product) + * + * @return DocumentInterface + */ + public function convertToDocument($rawData, $documentClass) + { + // Get document metadata + $metadata = $this->metadataCollector->getDocumentMetadata($documentClass); + + switch (true) { + case isset($rawData['_source']): + $data = $rawData['_source']; + break; + + case isset($rawData['fields']): + $data = array_map('reset', $rawData['fields']); + /** Check for partial fields as well (@see https://www.elastic.co/guide/en/elasticsearch/reference/1.4/search-request-fields.html) */ + foreach ($data as $key => $field) { + if (is_array($field)) { + $data = array_merge($data, $field); + unset($data[$key]); + } + } + break; + + default: + $data = []; + } + + // Add special fields to data + foreach (['_id', '_score'] as $specialField) { + if (isset($rawData[$specialField])) { + $data[$specialField] = $rawData[$specialField]; + } + } + + /** @var DocumentInterface $object */ + $className = $metadata->getClassName(); + $object = $this->assignArrayToObject($data, new $className(), $metadata->getPropertiesMetadata()); + + return $object; + } + + /** + * Assigns all properties to object. + * + * @param array $array Flat array with fields and their value + * @param object $object A document or a (nested) object object + * @param array $propertiesMetadata + * + * @return object + */ + public function assignArrayToObject(array $array, $object, array $propertiesMetadata) + { + foreach ($propertiesMetadata as $esField => $propertyMetadata) { + // Skip fields from the mapping that have no value set, unless they are multilanguage fields + if (empty($propertyMetadata['multilanguage']) && !isset($array[$esField])) { + continue; + } + + if ($propertyMetadata['type'] === 'string' && !empty($propertyMetadata['multilanguage'])) { + $objectValue = new MLProperty(); + foreach ($array as $fieldName => $value) { + $prefixLength = strlen($esField . $this->languageSeparator); + if (substr($fieldName, 0, $prefixLength) === $esField . $this->languageSeparator) { + $language = substr($fieldName, $prefixLength); + $objectValue->setValue($value, $language); + } + } + + } elseif (in_array($propertyMetadata['type'], ['object', 'nested'])) { + if ($propertyMetadata['multiple']) { + $objectValue = new ObjectIterator($this, $array[$esField], $propertyMetadata); + } else { + $objectValue = $this->assignArrayToObject( + $array[$esField], + new $propertyMetadata['className'](), + $propertyMetadata['propertiesMetadata'] + ); + } + + } else { + $objectValue = $array[$esField]; + } + + if ($propertyMetadata['propertyAccess'] == DocumentMetadata::PROPERTY_ACCESS_PRIVATE) { + $object->{$propertyMetadata['methods']['setter']}($objectValue); + } else { + $object->{$propertyMetadata['propertyName']} = $objectValue; + } + } + + return $object; + } + + /** + * Converts document or (nested) object to an array. + * + * @param mixed $object Can be instance of DocumentInterface or a (nested) object + * @param array $propertiesMetadata + * + * @return array + */ + public function convertToArray($object, $propertiesMetadata = []) + { + if (empty($propertiesMetadata)) { + $propertiesMetadata = $this->metadataCollector->getDocumentMetadata(get_class($object))->getPropertiesMetadata(); + } + + $array = []; + + foreach ($propertiesMetadata as $name => $propertyMetadata) { + if ($propertyMetadata['propertyAccess'] == DocumentMetadata::PROPERTY_ACCESS_PRIVATE) { + $value = $object->{$propertyMetadata['methods']['getter']}(); + } else { + $value = $object->{$propertyMetadata['propertyName']}; + } + + if (isset($value)) { + if (array_key_exists('propertiesMetadata', $propertyMetadata)) { + $new = []; + if ($propertyMetadata['multiple']) { + // Verify value is traversable + if (!(is_array($value) || (is_object($value) && $value instanceof \Traversable))) { + throw new \InvalidArgumentException(sprintf('Value of "%s" is not traversable, although field is set to "multiple"')); + } + + foreach ($value as $item) { + $this->checkVariableType($item, $propertyMetadata['className']); + $new[] = $this->convertToArray($item, $propertyMetadata['propertiesMetadata']); + } + } else { + $this->checkVariableType($value, $propertyMetadata['className']); + $new = $this->convertToArray($value, $propertyMetadata['propertiesMetadata']); + } + $value = $new; + } + + if ($value instanceof MLProperty) { + foreach ($value->getValues() as $language => $langValue) { + $array[$name . $this->languageSeparator . $language] = $langValue; + } + } else { + $array[$name] = $value; + } + } + } + + return $array; + } + + /** + * Check if object is the correct type + * + * @param object $object + * @param array $expectedClass + * + * @throws \InvalidArgumentException + */ + private function checkVariableType($object, $expectedClass) + { + if (!is_object($object) || get_class($object) !== $expectedClass) { + throw new \InvalidArgumentException( + sprintf('Expected object of type "%s", got "%s"', $expectedClass, get_class($object)) + ); + } + } +} diff --git a/Result/DocumentIterator.php b/Result/DocumentIterator.php index 2aed015..2e27479 100644 --- a/Result/DocumentIterator.php +++ b/Result/DocumentIterator.php @@ -2,51 +2,63 @@ namespace Sineflow\ElasticsearchBundle\Result; -use Sineflow\ElasticsearchBundle\DTO\IndicesAndTypesMetadataCollection; +use Sineflow\ElasticsearchBundle\Document\DocumentInterface; +use Sineflow\ElasticsearchBundle\DTO\TypesToDocumentClasses; /** - * This class is able to iterate over Elasticsearch result documents while casting data into models. + * This class is able to iterate over Elasticsearch result documents while casting data into objects */ -class DocumentIterator extends AbstractResultsIterator +class DocumentIterator implements \Countable, \Iterator { /** * @var array */ private $rawData; - /** - * @var IndicesAndTypesMetadataCollection + /**AbstractResultsIterator + * @var DocumentConverter */ - private $typesMetadataCollection; + private $documentConverter; /** - * @var string + * @var TypesToDocumentClasses */ - private $languageSeparator; + private $typesToDocumentClasses; /** * @var array */ private $suggestions = []; + /** + * @var array + */ + private $aggregations = []; + + /** + * @var array + */ + private $documents = []; + /** * Constructor. * - * @param array $rawData - * @param IndicesAndTypesMetadataCollection $typesMetadataCollection - * @param string $languageSeparator + * @param array $rawData + * @param DocumentConverter $documentConverter + * @param TypesToDocumentClasses $typesToDocumentClasses */ - public function __construct($rawData, IndicesAndTypesMetadataCollection $typesMetadataCollection, $languageSeparator) + public function __construct($rawData, DocumentConverter $documentConverter, TypesToDocumentClasses $typesToDocumentClasses) { $this->rawData = $rawData; - $this->typesMetadataCollection = $typesMetadataCollection; - $this->languageSeparator = $languageSeparator; + $this->documentConverter = $documentConverter; + $this->typesToDocumentClasses = $typesToDocumentClasses; if (isset($rawData['suggest'])) { - $this->suggestions = $rawData['suggest']; + $this->suggestions = &$rawData['suggest']; + } + if (isset($rawData['aggregations'])) { + $this->aggregations = &$rawData['aggregations']; } - - // Alias documents to have shorter path. if (isset($rawData['hits']['hits'])) { $this->documents = &$rawData['hits']['hits']; } @@ -61,18 +73,69 @@ public function getSuggestions() } /** - * Returns a converter. + * @return array + */ + public function getAggregations() + { + return $this->aggregations; + } + + /** + * Returns total count of records matching the query. * - * @param string $index - * @param string $type - * @return Converter + * @return int + */ + public function getTotalCount() + { + return $this->rawData['hits']['total']; + } + + /** + * @return int + */ + public function count() + { + return count($this->documents); + } + + /** + * @return DocumentInterface + */ + public function current() + { + return $this->convertToDocument($this->documents[$this->key()]); + } + + /** + * {@inheritdoc} + */ + public function next() + { + next($this->documents); + } + + /** + * {@inheritdoc} + */ + public function key() + { + return key($this->documents); + } + + /** + * {@inheritdoc} */ - protected function getConverter($index, $type) + public function valid() { - $metadata = $this->typesMetadataCollection->getTypeMetadata($type, $index); - $converter = new Converter($metadata, $this->languageSeparator); + return $this->key() !== null; + } - return $converter; + /** + * {@inheritdoc} + */ + public function rewind() + { + reset($this->documents); } /** @@ -84,18 +147,11 @@ protected function getConverter($index, $type) * * @throws \LogicException */ - protected function convertDocument($rawData) + private function convertToDocument($rawData) { - return $this->getConverter($rawData['_index'], $rawData['_type'])->convertToDocument($rawData); - } + $documentClass = $this->typesToDocumentClasses->get($rawData['_index'], $rawData['_type']); - /** - * Returns count of records found by given query. - * - * @return int - */ - public function getTotalCount() - { - return $this->rawData['hits']['total']; + return $this->documentConverter->convertToDocument($rawData, $documentClass); } + } diff --git a/Result/ObjectIterator.php b/Result/ObjectIterator.php index 87a545e..65d7d51 100644 --- a/Result/ObjectIterator.php +++ b/Result/ObjectIterator.php @@ -5,7 +5,7 @@ /** * ObjectIterator class. */ -class ObjectIterator extends AbstractResultsIterator +class ObjectIterator implements \Countable, \Iterator { /** * @var array property metadata information. @@ -13,31 +13,78 @@ class ObjectIterator extends AbstractResultsIterator private $propertyMetadata; /** - * @var Converter + * @var DocumentConverter */ - private $converter; + private $documentConverter; /** * Constructor. * - * @param Converter $converter - * @param array $rawData - * @param array $propertyMetadata + * @param DocumentConverter $documentConverter + * @param array $rawData + * @param array $propertyMetadata */ - public function __construct($converter, $rawData, $propertyMetadata) + public function __construct(DocumentConverter $documentConverter, array $rawData, array $propertyMetadata) { - $this->converter = $converter; + $this->documentConverter = $documentConverter; $this->propertyMetadata = $propertyMetadata; - $this->converted = []; - $this->documents = $rawData; + $this->objects = $rawData; + } + + /** + * @return int + */ + public function count() + { + return count($this->objects); + } + + /** + * @return DocumentInterface + */ + public function current() + { + return $this->convertToObject($this->objects[$this->key()]); + } + + /** + * {@inheritdoc} + */ + public function next() + { + next($this->objects); + } + + /** + * {@inheritdoc} + */ + public function key() + { + return key($this->objects); + } + + /** + * {@inheritdoc} + */ + public function valid() + { + return $this->key() !== null; + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + reset($this->objects); } /** * {@inheritdoc} */ - protected function convertDocument($rawData) + private function convertToObject($rawData) { - return $this->converter->assignArrayToObject( + return $this->documentConverter->assignArrayToObject( $rawData, new $this->propertyMetadata['className'](), $this->propertyMetadata['propertiesMetadata']