diff --git a/README.markdown b/README.markdown index fa0a6a2..384c359 100755 --- a/README.markdown +++ b/README.markdown @@ -47,8 +47,8 @@ $searchManager = new Doctrine\Search\SearchManager( ``` ## Mappings ## -Basic entity mappings for index and type generation can be annotated as shown in the following example. Mappings -can be rendered into a format suitable for automatically generating indexes and types using a build script +Basic entity mappings for index and type generation can be annotated as shown in the following example. Mappings +can be rendered into a format suitable for automatically generating indexes and types using a build script (advanced setup required). ```php getDatabaseConnection('elasticsearch'); - } - - public function postPersist(LifecycleEventArgs $oArgs) { - $oEntity = $oArgs->getEntity(); - if($oEntity instanceof SearchableEntityInterface) { - $this->getSearchManager()->persist($oEntity); - } - } - - public function postRemove(LifecycleEventArgs $oArgs) { - $oEntity = $oArgs->getEntity(); - if($oEntity instanceof SearchableEntityInterface) { - $this->getSearchManager()->remove($oEntity); - } - } -} -``` ### CallbackSerializer ### This approach simply expects a `toArray()` method on the entity, although this method be configured as required. @@ -132,7 +107,7 @@ entities that need to be persisted to the search engine (see above example). ... use Entities\Behaviour\SearchableEntityInterface -class Post implements SearchableEntityInterface +class Post implements SearchableEntityInterface { ... public function toArray() { @@ -147,7 +122,7 @@ class Post implements SearchableEntityInterface ``` ### JMS Serializer ### -You can alternatively use the advanced serialization power of the `JMS Serializer` to automatically handle +You can alternatively use the advanced serialization power of the `JMS Serializer` to automatically handle serialization for you based on annotations such as those shown in this example. ```php ... @@ -169,11 +144,11 @@ class Post implements SearchableEntityInterface * @JMS\Groups({"public", "search"}) */ private $title; - + /** * @ORM\Column(type="text") * @MAP\ElasticField(type="string", includeInAll=true) - * @JMS\Expose + * @JMS\Expose * @JMS\Groups({"public", "search"}) */ private $content; @@ -197,7 +172,7 @@ $hydrationQuery = $entityManager->createQueryBuilder() ->where('p.id IN (:ids)') ->orderBy('field') ->getQuery(); - + $query = $searchManager->createQuery() ->from('Entities\Post') ->searchWith(new Elastica\Query()) diff --git a/composer.json b/composer.json index 5b258fd..fdbc8c5 100644 --- a/composer.json +++ b/composer.json @@ -16,16 +16,23 @@ "doctrine/common": ">=2.3.0" }, "suggest": { - "ruflin/Elastica": "Elastica version correlating to your ElasticSearch installation is required." + "ruflin/Elastica": "Elastica version correlating to your ElasticSearch installation is required.", + "nette/neon": "Neon is a developer friendly format for config files, similar to YAML." }, "require-dev": { "doctrine/orm": "~2.4", + "jms/serializer": "~0.16", + "ruflin/elastica": "~1.3.0", + "nette/neon": "~2.2", "phpunit/phpunit": "~4.2" }, "autoload": { "psr-0": { "Doctrine\\Search": "lib/" - } + }, + "classmap": [ + "lib/Doctrine/Search/exceptions.php" + ] }, "autoload-dev": { "psr-0": { diff --git a/demo/index.php b/demo/index.php index 9b92047..266a4ca 100644 --- a/demo/index.php +++ b/demo/index.php @@ -34,7 +34,7 @@ echo PHP_EOL."*** Single lookup with no results ***".PHP_EOL; try { $user = $sm->find('Doctrine\Tests\Models\Comments\User', 'unknownid'); -} catch (Doctrine\Search\Exception\NoResultException $exception) { +} catch (\Doctrine\Search\NoResultException $exception) { print_r($exception->getMessage()); echo PHP_EOL; } diff --git a/lib/Doctrine/Search/Configuration.php b/lib/Doctrine/Search/Configuration.php index 856ba00..fe78b5f 100755 --- a/lib/Doctrine/Search/Configuration.php +++ b/lib/Doctrine/Search/Configuration.php @@ -38,6 +38,22 @@ class Configuration */ private $attributes; + /** + * @param string $prefix + */ + public function setIndexPrefix($prefix) + { + $this->attributes['indexPrefix'] = $prefix; + } + + /** + * @return string|NULL + */ + public function getIndexPrefix() + { + return isset($this->attributes['indexPrefix']) ? $this->attributes['indexPrefix'] : NULL; + } + /** * Gets the cache driver implementation that is used for the mapping metadata. * (Annotation is the default) @@ -46,11 +62,11 @@ class Configuration */ public function getMetadataDriverImpl() { - if (!isset($this->attributes['concreteMetadataDriver'])) { - $this->attributes['concreteMetadataDriver'] = $this->newDefaultAnnotationDriver(); + if (!isset($this->attributes['metadataDriver'])) { + $this->attributes['metadataDriver'] = $this->newDefaultAnnotationDriver(); } - return $this->attributes['concreteMetadataDriver']; + return $this->attributes['metadataDriver']; } /** @@ -60,7 +76,7 @@ public function getMetadataDriverImpl() */ public function setMetadataDriverImpl(MappingDriver $concreteDriver) { - $this->attributes['concreteMetadataDriver'] = $concreteDriver; + $this->attributes['metadataDriver'] = $concreteDriver; } /** @@ -94,9 +110,9 @@ public function getMetadataCacheImpl() */ public function newDefaultAnnotationDriver(array $paths = array()) { - $reader = new \Doctrine\Common\Annotations\AnnotationReader(); - - return new \Doctrine\Search\Mapping\Driver\AnnotationDriver($reader, $paths); + throw new NotImplementedException; +// $reader = new \Doctrine\Common\Annotations\AnnotationReader(); +// return new \Doctrine\Search\Mapping\Driver\AnnotationDriver($reader, $paths); } /** @@ -161,20 +177,21 @@ public function getEntitySerializer() } /** - * @param ObjectManager $entityManager + * @param ObjectManager $objectManager */ - public function setEntityManager(ObjectManager $entityManager) + public function setObjectManager(ObjectManager $objectManager) { - $this->attributes['entityManager'] = $entityManager; + $this->attributes['objectManager'] = $objectManager; } /** * @return ObjectManager */ - public function getEntityManager() + public function getObjectManager() { - if (isset($this->attributes['entityManager'])) { - return $this->attributes['entityManager']; + if (isset($this->attributes['objectManager'])) { + return $this->attributes['objectManager']; } } + } diff --git a/lib/Doctrine/Search/ElasticSearch/Client.php b/lib/Doctrine/Search/ElasticSearch/Client.php index 3088ffd..7d2fd03 100755 --- a/lib/Doctrine/Search/ElasticSearch/Client.php +++ b/lib/Doctrine/Search/ElasticSearch/Client.php @@ -19,19 +19,22 @@ namespace Doctrine\Search\ElasticSearch; -use Doctrine\Search\SearchClientInterface; +use Doctrine; use Doctrine\Search\Mapping\ClassMetadata; -use Doctrine\Search\Exception\NoResultException; +use Doctrine\Search\Mapping\TypeMetadata; +use Doctrine\Search\Mapping\TypeMetadataFactory; +use Doctrine\Search\NoResultException; +use Doctrine\Search\SearchClient; use Elastica\Client as ElasticaClient; -use Elastica\Type\Mapping; use Elastica\Document; +use Elastica\Exception\NotFoundException; +use Elastica\Filter\Term; use Elastica\Index; +use Elastica\Query; use Elastica\Query\MatchAll; -use Elastica\Filter\Term; -use Elastica\Exception\NotFoundException; use Elastica\Search; -use Doctrine\Common\Collections\ArrayCollection; -use Elastica\Query; + + /** * SearchManager for ElasticSearch-Backend @@ -39,7 +42,7 @@ * @author Mike Lohmann * @author Markus Bachmann */ -class Client implements SearchClientInterface +class Client implements SearchClient { /** * @var ElasticaClient @@ -63,33 +66,53 @@ public function getClient() } /** - * {@inheritDoc} + * @param string $name + * @return Index */ - public function addDocuments(ClassMetadata $class, array $documents) + protected function getIndex($name) { - $type = $this->getIndex($class->index)->getType($class->type); + return $this->client->getIndex($name); + } - $parameters = $this->getParameters($class->parameters); + /** + * @param ClassMetadata $class + * @return \Elastica\Type + */ + protected function getType(ClassMetadata $class) + { + return $this->getIndex($class->getIndexName())->getType($class->getTypeName()); + } + /** + * {@inheritDoc} + */ + public function addDocuments(ClassMetadata $class, array $documents) + { $bulk = array(); foreach ($documents as $id => $document) { $elasticaDoc = new Document($id); - foreach ($parameters as $name => $value) { - if (isset($document[$value])) { - if (method_exists($elasticaDoc, "set{$name}")) { - $elasticaDoc->{"set{$name}"}($document[$value]); - } else { - $elasticaDoc->setParam($name, $document[$value]); - } - unset($document[$value]); + + foreach ($class->type->parameters as $name => $value) { + if (!isset($document[$value])) { + continue; + } + + if (method_exists($elasticaDoc, "set{$name}")) { + $elasticaDoc->{"set{$name}"}($document[$value]); + } else { + $elasticaDoc->setParam($name, $document[$value]); } + unset($document[$value]); } - $elasticaDoc->setData($document); - $bulk[] = $elasticaDoc; + + $bulk[] = $elasticaDoc->setData($document); } + $type = $this->getType($class); + if (count($bulk) > 1) { $type->addDocuments($bulk); + } else { $type->addDocument($bulk[0]); } @@ -100,8 +123,7 @@ public function addDocuments(ClassMetadata $class, array $documents) */ public function removeDocuments(ClassMetadata $class, array $documents) { - $type = $this->getIndex($class->index)->getType($class->type); - $type->deleteIds(array_keys($documents)); + $this->getType($class)->deleteIds(array_keys($documents)); } /** @@ -109,9 +131,8 @@ public function removeDocuments(ClassMetadata $class, array $documents) */ public function removeAll(ClassMetadata $class, $query = null) { - $type = $this->getIndex($class->index)->getType($class->type); $query = $query ?: new MatchAll(); - $type->deleteByQuery($query); + $this->getType($class)->deleteByQuery($query); } /** @@ -120,8 +141,7 @@ public function removeAll(ClassMetadata $class, $query = null) public function find(ClassMetadata $class, $id, $options = array()) { try { - $type = $this->getIndex($class->index)->getType($class->type); - $document = $type->getDocument($id, $options); + $document = $this->getType($class)->getDocument($id, $options); } catch (NotFoundException $ex) { throw new NoResultException(); } @@ -155,16 +175,20 @@ public function findAll(array $classes) return $this->buildQuery($classes)->search(); } + /** + * @param array|ClassMetadata[] $classes + * @return Search + */ protected function buildQuery(array $classes) { $searchQuery = new Search($this->client); $searchQuery->setOption(Search::OPTION_VERSION, true); foreach ($classes as $class) { if ($class->index) { - $indexObject = $this->getIndex($class->index); + $indexObject = $this->getIndex($class->getIndexName()); $searchQuery->addIndex($indexObject); if ($class->type) { - $searchQuery->addType($indexObject->getType($class->type)); + $searchQuery->addType($indexObject->getType($class->getTypeName())); } } } @@ -179,32 +203,6 @@ public function search($query, array $classes) return $this->buildQuery($classes)->search($query); } - /** - * {@inheritDoc} - */ - public function createIndex($name, array $config = array()) - { - $index = $this->getIndex($name); - $index->create($config, true); - return $index; - } - - /** - * {@inheritDoc} - */ - public function getIndex($name) - { - return $this->client->getIndex($name); - } - - /** - * {@inheritDoc} - */ - public function deleteIndex($index) - { - $this->getIndex($index)->delete(); - } - /** * {@inheritDoc} */ @@ -214,175 +212,12 @@ public function refreshIndex($index) } /** - * {@inheritDoc} - */ - public function createType(ClassMetadata $metadata) - { - $type = $this->getIndex($metadata->index)->getType($metadata->type); - $properties = $this->getMapping($metadata->fieldMappings); - $rootProperties = $this->getRootMapping($metadata->rootMappings); - - $mapping = new Mapping($type, $properties); - $mapping->disableSource($metadata->source); - if (isset($metadata->boost)) { - $mapping->setParam('_boost', array('name' => '_boost', 'null_value' => $metadata->boost)); - } - if (isset($metadata->parent)) { - $mapping->setParent($metadata->parent); - } - foreach ($rootProperties as $key => $value) { - $mapping->setParam($key, $value); - } - - $mapping->send(); - - return $type; - } - - /** - * {@inheritDoc} - */ - public function deleteType(ClassMetadata $metadata) - { - $type = $this->getIndex($metadata->index)->getType($metadata->type); - return $type->delete(); - } - - /** - * Generates property mapping from entity annotations - * - * @param array $mappings - */ - protected function getMapping($mappings) - { - $properties = array(); - - foreach ($mappings as $propertyName => $fieldMapping) { - if (isset($fieldMapping->name)) { - $propertyName = $fieldMapping->name; - } - - $properties[$propertyName]['type'] = $fieldMapping->type; - - if (isset($fieldMapping->path)) { - $properties[$propertyName]['path'] = $fieldMapping->path; - } - - if (isset($fieldMapping->includeInAll)) { - $properties[$propertyName]['include_in_all'] = $fieldMapping->includeInAll; - } - - if (isset($fieldMapping->nullValue)) { - $properties[$propertyName]['null_value'] = $fieldMapping->nullValue; - } - - if (isset($fieldMapping->store)) { - $properties[$propertyName]['store'] = $fieldMapping->store; - } - - if (isset($fieldMapping->index)) { - $properties[$propertyName]['index'] = $fieldMapping->index; - } - - if (isset($fieldMapping->boost)) { - $properties[$propertyName]['boost'] = $fieldMapping->boost; - } - - if (isset($fieldMapping->analyzer)) { - $properties[$propertyName]['analyzer'] = $fieldMapping->analyzer; - } - - if (isset($fieldMapping->indexName)) { - $properties[$propertyName]['index_name'] = $fieldMapping->indexName; - } - - if ($fieldMapping->type == 'attachment' && isset($fieldMapping->fields)) { - $callback = function ($field) { - unset($field['type']); - return $field; - }; - $properties[$propertyName]['fields'] = array_map($callback, $this->getMapping($fieldMapping->fields)); - } - - if ($fieldMapping->type == 'multi_field' && isset($fieldMapping->fields)) { - $properties[$propertyName]['fields'] = $this->getMapping($fieldMapping->fields); - } - - if (in_array($fieldMapping->type, array('nested', 'object')) && isset($fieldMapping->properties)) { - $properties[$propertyName]['properties'] = $this->getMapping($fieldMapping->properties); - } - } - - return $properties; - } - - /** - * Generates parameter mapping from entity annotations - * - * @param array $paramMapping + * @param string $className + * @return TypeMetadata */ - protected function getParameters($paramMapping) + public function createTypeMetadata($className) { - $parameters = array(); - foreach ($paramMapping as $propertyName => $mapping) { - $paramName = isset($mapping->name) ? $mapping->name : $propertyName; - $parameters[$paramName] = $propertyName; - } - return $parameters; + return new Mapping\TypeMetadata($className); } - /** - * Generates root mapping from entity annotations - * - * @param array $mappings - */ - protected function getRootMapping($mappings) - { - $properties = array(); - - foreach ($mappings as $rootMapping) { - $propertyName = $rootMapping->name; - $mapping = array(); - - if (isset($rootMapping->value)) { - $mapping = $rootMapping->value; - } - - if (isset($rootMapping->match)) { - $mapping['match'] = $rootMapping->match; - } - - if (isset($rootMapping->pathMatch)) { - $mapping['path_match'] = $rootMapping->pathMatch; - } - - if (isset($rootMapping->unmatch)) { - $mapping['unmatch'] = $rootMapping->unmatch; - } - - if (isset($rootMapping->pathUnmatch)) { - $mapping['path_unmatch'] = $rootMapping->pathUnmatch; - } - - if (isset($rootMapping->matchPattern)) { - $mapping['match_pattern'] = $rootMapping->matchPattern; - } - - if (isset($rootMapping->matchMappingType)) { - $mapping['match_mapping_type'] = $rootMapping->matchMappingType; - } - - if (isset($rootMapping->mapping)) { - $mapping['mapping'] = current($this->getMapping($rootMapping->mapping)); - } - - if (isset($rootMapping->id)) { - $properties[$propertyName][][$rootMapping->id] = $mapping; - } else { - $properties[$propertyName] = $mapping; - } - } - - return $properties; - } } diff --git a/lib/Doctrine/Search/ElasticSearch/Mapping/TypeMetadata.php b/lib/Doctrine/Search/ElasticSearch/Mapping/TypeMetadata.php new file mode 100644 index 0000000..7863b45 --- /dev/null +++ b/lib/Doctrine/Search/ElasticSearch/Mapping/TypeMetadata.php @@ -0,0 +1,41 @@ + + */ +class TypeMetadata extends Search\Mapping\TypeMetadata +{ + + /** + * @var array + */ + public $settings = array(); + + /** + * @var array + */ + public $properties = array(); + + + + protected function validateSettings(array $options) + { + + } + + + + protected function validateProperty($name, array $property) + { + if (!isset($property['type'])) { + throw new Search\InvalidMetadataException("Type of property is mandatory."); + } + } + +} diff --git a/lib/Doctrine/Search/ElasticSearch/SchemaManager.php b/lib/Doctrine/Search/ElasticSearch/SchemaManager.php new file mode 100644 index 0000000..be38311 --- /dev/null +++ b/lib/Doctrine/Search/ElasticSearch/SchemaManager.php @@ -0,0 +1,256 @@ + + */ +class SchemaManager implements Doctrine\Search\SchemaManager +{ + + /** + * @var \Doctrine\Search\ElasticSearch\Client + */ + private $client; + + /** + * @var \Elastica\Client + */ + private $elastica; + + + + public function __construct(Client $client) + { + $this->client = $client; + $this->elastica = $client->getClient(); + } + + + + /** + * @param ClassMetadata $class + * @return \Elastica\Index + */ + protected function getIndex(ClassMetadata $class) + { + return $this->elastica + ->getIndex($class->getIndexName()); + } + + + + /** + * @param ClassMetadata $class + * @return \Elastica\Type + */ + protected function getType(ClassMetadata $class) + { + return $this->getIndex($class) + ->getType($class->getTypeName()); + } + + + + /** + * @param array|ClassMetadata[] $classes + */ + public function dropMappings(array $classes) + { + foreach ($classes as $class) { + if (!$this->hasIndex($class->getIndexName())) { + continue; + } + + if (!$this->hasType($class)) { + continue; + } + + $this->dropType($class); + } + + foreach ($classes as $class) { + if (!$this->hasIndex($class->getIndexName())) { + continue; + } + + $this->dropIndex($class->getIndexName()); + } + } + + + + /** + * @param array|ClassMetadata[] $classes + * @param bool $withAliases + * @return array + */ + public function createMappings(array $classes, $withAliases = FALSE) + { + $aliases = []; + $date = date('YmdHis'); + foreach ($classes as $class) { + if ($withAliases) { + $indexAlias = $class->getIndexName() . '_' . $date; + $aliases[$indexAlias] = $class->getIndexName(); + + $fakeMetadata = clone $class; + $fakeMetadata->index->name = $indexAlias; + + $class = $fakeMetadata; + } + + if (!$this->hasIndex($class->getIndexName())) { + $this->createIndex($class); + } + + $this->createType($class); + } + + return $aliases; + } + + + + public function createAliases(array $aliases) + { + foreach ($aliases as $alias => $original) { + try { + $this->createAlias($alias, $original); + } catch (ResponseException $e) { + } + } + } + + + + public function hasIndex($index) + { + return $this->elastica->getIndex($index)->exists(); + } + + + + public function createIndex(ClassMetadata $class) + { + $index = $this->elastica->getIndex($class->getIndexName()); + $response = $index->create(array( + 'number_of_shards' => $class->index->numberOfShards, + 'number_of_replicas' => $class->index->numberOfReplicas, + 'analysis' => array( + 'char_filter' => $class->index->charFilter, + 'analyzer' => $class->index->analyzer, + 'filter' => $class->index->filter, + ), + ), TRUE); + + return $response; + } + + + + public function dropIndex($index) + { + return $this->elastica->getIndex($index)->delete(); + } + + + + public function hasType(ClassMetadata $class) + { + return $this->getType($class)->exists(); + } + + + + public function createType(ClassMetadata $class) + { + $mapping = new ElasticaTypeMapping($this->getType($class), self::settingsToUnderscore($class->type->properties)); + $mapping->disableSource($class->type->source); + + if ($class->type->boost !== NULL) { + $mapping->setParam('_boost', array('name' => '_boost', 'null_value' => $class->type->boost)); + } + + if ($class->parent !== NULL) { + $mapping->setParent($class->parent); + } + + foreach ($class->type->settings as $key => $value) { + $mapping->setParam($key, $value); + } + + return $mapping->send(); + } + + + + public function dropType(ClassMetadata $class) + { + return $this->getType($class)->delete(); + } + + + + public function createAlias($alias, $original) + { + try { + $this->elastica->request(sprintf('_all/_alias/%s', $original), Request::DELETE); + + } catch (ResponseException $e) { + if (stripos($e->getMessage(), 'AliasesMissingException') === FALSE) { + throw $e; + } + } + + $this->elastica->request(sprintf('/%s/_alias/%s', $alias, $original), Request::PUT); + } + + + + private static function settingsToUnderscore($properties) + { + foreach ($properties as $name => $settings) { + $fixed = []; + foreach ($settings as $key => $value) { + $fixed[self::toUnderscore($key)] = $value; + } + + if (isset($settings['properties'])) { + $fixed['properties'] = self::settingsToUnderscore($settings['properties']); + } + + $properties[$name] = $fixed; + } + + return $properties; + } + + + + /** + * camelCaseField name -> underscore_separated. + * + * @param string $s + * @return string + */ + private static function toUnderscore($s) + { + $s = preg_replace('#(.)(?=[A-Z])#', '$1_', $s); + $s = strtolower($s); + $s = rawurlencode($s); + + return $s; + } + +} diff --git a/lib/Doctrine/Search/EntityRepository.php b/lib/Doctrine/Search/EntityRepository.php index 6f52c80..952355c 100644 --- a/lib/Doctrine/Search/EntityRepository.php +++ b/lib/Doctrine/Search/EntityRepository.php @@ -20,32 +20,32 @@ namespace Doctrine\Search; use Doctrine\Common\Persistence\ObjectRepository; -use Doctrine\Search\SearchManager; use Doctrine\Search\Mapping\ClassMetadata; -use Doctrine\Search\Exception\DoctrineSearchException; + + class EntityRepository implements ObjectRepository { /** * @var string */ - protected $_entityName; + protected $entityName; /** * @var \Doctrine\Search\Mapping\ClassMetadata */ - private $_class; + private $class; /** * @var \Doctrine\Search\SearchManager */ - private $_sm; + private $sm; public function __construct(SearchManager $sm, ClassMetadata $class) { - $this->_sm = $sm; - $this->_entityName = $class->className; - $this->_class = $class; + $this->sm = $sm; + $this->entityName = $class->className; + $this->class = $class; } /** @@ -56,7 +56,7 @@ public function __construct(SearchManager $sm, ClassMetadata $class) */ public function find($id) { - return $this->_sm->find($this->_entityName, $id); + return $this->sm->find($this->entityName, $id); } /** @@ -66,7 +66,7 @@ public function find($id) */ public function findAll() { - throw new DoctrineSearchException('Not yet implemented.'); + throw new NotImplementedException; } /** @@ -85,7 +85,7 @@ public function findAll() */ public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) { - throw new DoctrineSearchException('Not yet implemented.'); + throw new NotImplementedException(); } /** @@ -98,7 +98,7 @@ public function findOneBy(array $criteria) { $options = array('field' => key($criteria)); $value = current($criteria); - return $this->_sm->getUnitOfWork()->load($this->_class, $value, $options); + return $this->sm->getUnitOfWork()->load($this->class, $value, $options); } /** @@ -108,7 +108,7 @@ public function findOneBy(array $criteria) */ public function search($query) { - return $this->_sm->getUnitOfWork()->loadCollection(array($this->_class), $query); + return $this->sm->getUnitOfWork()->loadCollection(array($this->class), $query); } /** @@ -118,7 +118,7 @@ public function search($query) */ public function delete($query) { - $this->_sm->getClient()->removeAll($this->_class, $query); + $this->sm->getClient()->removeAll($this->class, $query); } /** @@ -128,7 +128,7 @@ public function delete($query) */ public function getClassName() { - return $this->_entityName; + return $this->entityName; } /** @@ -138,7 +138,7 @@ public function getClassName() */ public function getClassMetadata() { - return $this->_class; + return $this->class; } /** @@ -148,6 +148,6 @@ public function getClassMetadata() */ public function getSearchManager() { - return $this->_sm; + return $this->sm; } } diff --git a/lib/Doctrine/Search/EntityRepositoryCollection.php b/lib/Doctrine/Search/EntityRepositoryCollection.php index 3d5960c..8275f7b 100644 --- a/lib/Doctrine/Search/EntityRepositoryCollection.php +++ b/lib/Doctrine/Search/EntityRepositoryCollection.php @@ -20,7 +20,8 @@ namespace Doctrine\Search; use Doctrine\Common\Persistence\ObjectRepository; -use Doctrine\Search\Exception\DoctrineSearchException; + + class EntityRepositoryCollection implements ObjectRepository { @@ -67,7 +68,7 @@ public function find($id) */ public function findAll() { - throw new DoctrineSearchException('Not yet implemented.'); + throw new NotImplementedException; } /** @@ -86,7 +87,7 @@ public function findAll() */ public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) { - throw new DoctrineSearchException('Not yet implemented.'); + throw new NotImplementedException; } /** @@ -97,7 +98,7 @@ public function findBy(array $criteria, array $orderBy = null, $limit = null, $o */ public function findOneBy(array $criteria) { - throw new DoctrineSearchException('Not yet implemented.'); + throw new NotImplementedException; } /** diff --git a/lib/Doctrine/Search/EntityRiver.php b/lib/Doctrine/Search/EntityRiver.php new file mode 100644 index 0000000..41edb48 --- /dev/null +++ b/lib/Doctrine/Search/EntityRiver.php @@ -0,0 +1,26 @@ + + */ +interface EntityRiver +{ + + public function transfuse(Mapping\ClassMetadata $searchMeta); + +} diff --git a/lib/Doctrine/Search/Event/LifecycleEventArgs.php b/lib/Doctrine/Search/Event/LifecycleEventArgs.php index f9efa73..05914bc 100644 --- a/lib/Doctrine/Search/Event/LifecycleEventArgs.php +++ b/lib/Doctrine/Search/Event/LifecycleEventArgs.php @@ -20,6 +20,7 @@ namespace Doctrine\Search\Event; use Doctrine\Common\EventArgs; +use Doctrine\Search\Searchable; use Doctrine\Search\SearchManager; /** @@ -46,19 +47,19 @@ class LifecycleEventArgs extends EventArgs /** * Constructor * - * @param object $entity - * @param \Doctrine\Search\SearchManager $em + * @param Searchable $entity + * @param \Doctrine\Search\SearchManager $sm */ public function __construct($entity, SearchManager $sm) { $this->entity = $entity; - $this->sm = $sm; + $this->sm = $sm; } /** * Retrieve associated Entity. * - * @return object + * @return Searchable */ public function getEntity() { diff --git a/lib/Doctrine/Search/Event/LoadClassMetadataEventArgs.php b/lib/Doctrine/Search/Event/LoadClassMetadataEventArgs.php index 471af56..e932ce8 100644 --- a/lib/Doctrine/Search/Event/LoadClassMetadataEventArgs.php +++ b/lib/Doctrine/Search/Event/LoadClassMetadataEventArgs.php @@ -46,7 +46,7 @@ class LoadClassMetadataEventArgs extends EventArgs * Constructor. * * @param \Doctrine\Search\Mapping\ClassMetadata $classMetadata - * @param \Doctrine\Search\EntityManager $sm + * @param \Doctrine\Search\SearchManager $sm */ public function __construct(ClassMetadata $classMetadata, SearchManager $sm) { diff --git a/lib/Doctrine/Search/ZendLucene/Connection.php b/lib/Doctrine/Search/Event/PostLoadEventArgs.php similarity index 56% rename from lib/Doctrine/Search/ZendLucene/Connection.php rename to lib/Doctrine/Search/Event/PostLoadEventArgs.php index 13f6184..10b590b 100644 --- a/lib/Doctrine/Search/ZendLucene/Connection.php +++ b/lib/Doctrine/Search/Event/PostLoadEventArgs.php @@ -17,35 +17,50 @@ * . */ -namespace Doctrine\Search\ZendLucene; +namespace Doctrine\Search\Event; -use Doctrine\Search\Http\ClientInterface as HttpClientInterface; +use Doctrine\Common\EventArgs; +use Doctrine\Search\Searchable; +use Doctrine\Search\SearchManager; /** - * Connections handler for ZendLucene-Backend + * Lifecycle Events are triggered by the UnitOfWork during lifecycle transitions + * of entities. * - * @author Mike Lohmann + * @link www.doctrine-project.org + * @since 2.0 + * @author Roman Borschel + * @author Benjamin Eberlei */ -class Connection +class PostLoadEventArgs extends LifecycleEventArgs { - private $host; - private $port; + /** + * @var array + */ + private $data; - private $path; - private $httpClient; - public function __construct(HttpClientInterface $httpClient, $host = null, $port = null, $path = null) - { - $this->host = $host; - $this->port = $port; - $this->path = $path; - $this->httpClient = $httpClient; - } + /** + * @param Searchable $entity + * @param array $data + * @param SearchManager $sm + */ + public function __construct($entity, $data, SearchManager $sm) + { + parent::__construct($entity, $sm); + $this->data = $data; + } - public function initialize() - { - } + + /** + * @return array + */ + public function getRawData() + { + return $this->data; + } + } diff --git a/lib/Doctrine/Search/Exception/DoctrineSearchException.php b/lib/Doctrine/Search/Exception/DoctrineSearchException.php deleted file mode 100644 index 3ed9b09..0000000 --- a/lib/Doctrine/Search/Exception/DoctrineSearchException.php +++ /dev/null @@ -1,24 +0,0 @@ -. - */ - -namespace Doctrine\Search\Exception; - -class DoctrineSearchException extends \Exception -{ -} diff --git a/lib/Doctrine/Search/Exception/Driver/ClassIsNotAValidDocumentException.php b/lib/Doctrine/Search/Exception/Driver/ClassIsNotAValidDocumentException.php deleted file mode 100644 index 1f9a10b..0000000 --- a/lib/Doctrine/Search/Exception/Driver/ClassIsNotAValidDocumentException.php +++ /dev/null @@ -1,26 +0,0 @@ -. - */ - -namespace Doctrine\Search\Exception\Driver; - -use Doctrine\Search\Exception\DoctrineSearchException; - -class ClassIsNotAValidDocumentException extends DoctrineSearchException -{ -} diff --git a/lib/Doctrine/Search/Exception/Driver/PropertyDoesNotExistsInMetadataException.php b/lib/Doctrine/Search/Exception/Driver/PropertyDoesNotExistsInMetadataException.php deleted file mode 100644 index d3a52a9..0000000 --- a/lib/Doctrine/Search/Exception/Driver/PropertyDoesNotExistsInMetadataException.php +++ /dev/null @@ -1,26 +0,0 @@ -. - */ - -namespace Doctrine\Search\Exception\Driver; - -use Doctrine\Search\Exception\DoctrineSearchException; - -class PropertyDoesNotExistsInMetadataException extends DoctrineSearchException -{ -} diff --git a/lib/Doctrine/Search/Exception/NoResultException.php b/lib/Doctrine/Search/Exception/NoResultException.php deleted file mode 100644 index 97f8519..0000000 --- a/lib/Doctrine/Search/Exception/NoResultException.php +++ /dev/null @@ -1,34 +0,0 @@ -. - */ - -namespace Doctrine\Search\Exception; - -/** - * Exception thrown when an search query unexpectedly does not return any results. - * - * @author robo - * @since 2.0 - */ -class NoResultException extends DoctrineSearchException -{ - public function __construct($message = null) - { - parent::__construct($message ?: 'No result was found for query although at least one row was expected.'); - } -} diff --git a/lib/Doctrine/Search/Exception/UnexpectedTypeException.php b/lib/Doctrine/Search/Exception/UnexpectedTypeException.php deleted file mode 100644 index 7fa82c7..0000000 --- a/lib/Doctrine/Search/Exception/UnexpectedTypeException.php +++ /dev/null @@ -1,35 +0,0 @@ -. - */ - -namespace Doctrine\Search\Exception; - -class UnexpectedTypeException extends DoctrineSearchException -{ - - public function __construct($value, $expected) - { - parent::__construct( - sprintf( - 'Expected argument of type "%s", "%s" given', - $expected, - (is_object($value) ? get_class($value) : gettype($value)) - ) - ); - } -} diff --git a/lib/Doctrine/Search/Mapping/Annotations/ElasticField.php b/lib/Doctrine/Search/Mapping/Annotations/ElasticField.php deleted file mode 100755 index af49cc0..0000000 --- a/lib/Doctrine/Search/Mapping/Annotations/ElasticField.php +++ /dev/null @@ -1,74 +0,0 @@ -. - */ - -namespace Doctrine\Search\Mapping\Annotations; - -use Doctrine\Common\Annotations\Annotation; - -/** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) - */ -final class ElasticField extends Field -{ - /** - * @var boolean - */ - public $includeInAll; - - /** - * @var string - */ - public $index; - - /** - * @var array - */ - public $fields; - - /** - * @var array - */ - public $properties; - - /** - * @var string - */ - public $analyzer; - - /** - * @var string - */ - public $path; - - /** - * @var string - */ - public $indexName; - - /** - * @var boolean - */ - public $store; - - /** - * @var mixed - */ - public $nullValue; -} diff --git a/lib/Doctrine/Search/Mapping/Annotations/ElasticRoot.php b/lib/Doctrine/Search/Mapping/Annotations/ElasticRoot.php deleted file mode 100644 index f445a26..0000000 --- a/lib/Doctrine/Search/Mapping/Annotations/ElasticRoot.php +++ /dev/null @@ -1,79 +0,0 @@ -. - */ - -namespace Doctrine\Search\Mapping\Annotations; - -use Doctrine\Common\Annotations\Annotation; - -/** - * @Annotation - * @Target({"CLASS"}) - */ -final class ElasticRoot extends Annotation -{ - /** - * @var string - */ - public $name; - - /** - * @var string - */ - public $id; - - /** - * @var string - */ - public $match; - - /** - * @var string - */ - public $unmatch; - - /** - * @var string - */ - public $pathMatch; - - /** - * @var string - */ - public $pathUnmatch; - - /** - * @var string - */ - public $matchPattern; - - /** - * @var string - */ - public $matchMappingType; - - /** - * @var mixed - */ - public $value; - - /** - * @var array - */ - public $mapping; -} diff --git a/lib/Doctrine/Search/Mapping/Annotations/ElasticSearchable.php b/lib/Doctrine/Search/Mapping/Annotations/ElasticSearchable.php deleted file mode 100755 index 297f23d..0000000 --- a/lib/Doctrine/Search/Mapping/Annotations/ElasticSearchable.php +++ /dev/null @@ -1,65 +0,0 @@ -. - */ - -namespace Doctrine\Search\Mapping\Annotations; - -use Doctrine\Common\Annotations\Annotation; - -/** - * @Annotation - * @Target("CLASS") - */ -final class ElasticSearchable extends Searchable -{ - /** - * @var int $numberOfShards - */ - public $numberOfShards; - - /** - * @var int $numnberOfReplicas - */ - public $numberOfReplicas; - - /** - * @var string $op_type - */ - public $opType; - - /** - * @var string $parent - */ - public $parent; - - /** - * TTL in milliseconds - * @var int $timeToLive - */ - public $timeToLive; - - /** - * @var float - */ - public $boost; - - /** - * @var boolean - */ - public $source; -} diff --git a/lib/Doctrine/Search/Mapping/Annotations/Field.php b/lib/Doctrine/Search/Mapping/Annotations/Field.php deleted file mode 100755 index b0b74c6..0000000 --- a/lib/Doctrine/Search/Mapping/Annotations/Field.php +++ /dev/null @@ -1,44 +0,0 @@ -. - */ - -namespace Doctrine\Search\Mapping\Annotations; - -use Doctrine\Common\Annotations\Annotation; - -/** - * @Annotation - * @Target("PROPERTY") - */ -class Field extends Annotation -{ - /** - * @var float - */ - public $boost; - - /** - * @var string - */ - public $type; - - /** - * @var string - */ - public $name; -} diff --git a/lib/Doctrine/Search/Mapping/Annotations/Id.php b/lib/Doctrine/Search/Mapping/Annotations/Id.php deleted file mode 100644 index 6234014..0000000 --- a/lib/Doctrine/Search/Mapping/Annotations/Id.php +++ /dev/null @@ -1,30 +0,0 @@ -. - */ - -namespace Doctrine\Search\Mapping\Annotations; - -use Doctrine\Common\Annotations\Annotation; - -/** - * @Annotation - * @Target({"PROPERTY"}) - */ -final class Id extends Annotation -{ -} diff --git a/lib/Doctrine/Search/Mapping/Annotations/Parameter.php b/lib/Doctrine/Search/Mapping/Annotations/Parameter.php deleted file mode 100644 index 9df7773..0000000 --- a/lib/Doctrine/Search/Mapping/Annotations/Parameter.php +++ /dev/null @@ -1,39 +0,0 @@ -. - */ - -namespace Doctrine\Search\Mapping\Annotations; - -use Doctrine\Common\Annotations\Annotation; - -/** - * @Annotation - * @Target({"PROPERTY"}) - */ -final class Parameter extends Annotation -{ - /** - * @var string - */ - public $name; - - /** - * @var string - */ - public $type = 'string'; -} diff --git a/lib/Doctrine/Search/Mapping/Annotations/Searchable.php b/lib/Doctrine/Search/Mapping/Annotations/Searchable.php deleted file mode 100755 index 9ba42ac..0000000 --- a/lib/Doctrine/Search/Mapping/Annotations/Searchable.php +++ /dev/null @@ -1,39 +0,0 @@ -. - */ - -namespace Doctrine\Search\Mapping\Annotations; - -use Doctrine\Common\Annotations\Annotation; - -/** - * @Annotation - * @Target("CLASS") - */ -class Searchable extends Annotation -{ - /** - * @var string $index - */ - public $index; - - /** - * @var string $type - */ - public $type; -} diff --git a/lib/Doctrine/Search/Mapping/Annotations/SolrField.php b/lib/Doctrine/Search/Mapping/Annotations/SolrField.php deleted file mode 100755 index 0b9734d..0000000 --- a/lib/Doctrine/Search/Mapping/Annotations/SolrField.php +++ /dev/null @@ -1,31 +0,0 @@ -. - */ - -namespace Doctrine\Search\Mapping\Annotations; - -use Doctrine\Common\Annotations\Annotation; - -/** - * @Annotation - * @Target("PROPERTY") - */ -final class SolrField extends Field -{ - /* configuration */ -} diff --git a/lib/Doctrine/Search/Mapping/ClassMetadata.php b/lib/Doctrine/Search/Mapping/ClassMetadata.php index a72455d..cf24594 100644 --- a/lib/Doctrine/Search/Mapping/ClassMetadata.php +++ b/lib/Doctrine/Search/Mapping/ClassMetadata.php @@ -20,8 +20,7 @@ namespace Doctrine\Search\Mapping; use Doctrine\Common\Persistence\Mapping\ClassMetadata as ClassMetadataInterface; -use Doctrine\Search\Mapping\Annotations\ElasticField; -use Doctrine\Search\Mapping\Annotations\ElasticRoot; +use Doctrine\Common\Persistence\Mapping\ReflectionService; @@ -39,148 +38,88 @@ * get the whole class name, namespace inclusive, prepended to every property in * the serialized representation). * - * @link www.doctrine-project.com - * @since 1.0 - * @author Mike Lohmann + * @author Filip Procházka */ class ClassMetadata implements ClassMetadataInterface { /** - * @var string - */ - public $index; - - /** - * @var string + * @todo allow mapping to multiple types + * @var TypeMetadata */ public $type; /** - * @var int - */ - public $numberOfShards = 1; - - /** - * @var int - */ - public $numberOfReplicas = 0; - - /** - * @var int + * @todo allow mapping to multiple indexes + * @var IndexMetadata */ - public $opType = 1; + public $index; /** * @var string */ public $parent; - /** - * @var int - */ - public $timeToLive = 1; - - /** - * @var int - */ - public $value = 1; - - /** - * @var boolean - */ - public $source = true; - - /** - * @var float - */ - public $boost; - /** * @var string */ public $className; /** - * The ReflectionProperty instances of the mapped class. - * - * @var array|ElasticField[] - */ - public $fieldMappings = array(); - - /** - * Additional root annotations of the mapped class. - * - * @var array|ElasticRoot[] - */ - public $rootMappings = array(); - - /** - * The ReflectionProperty parameters of the mapped class. - * - * @var array + * @var string with class implementing \Doctrine\Search\EntityRiver */ - public $parameters = array(); + public $riverImplementation; /** - * The ReflectionClass instance of the mapped class. - * - * @var \ReflectionClass + * @var ClassMetadataInterface|\Doctrine\ORM\Mapping\ClassMetadata */ - public $reflClass; + private $parentMetadata; - /** - * The ReflectionClass instance of the mapped class. - * - * @var \ReflectionClass - */ - public $reflFields; - - /** - * READ-ONLY: The field names of all fields that are part of the identifier/primary key - * of the mapped entity class. - * - * @var mixed - */ - public $identifier = array(); - public function __construct($documentName) + public function __construct($entityName) { - $this->className = $documentName; - $this->reflClass = new \ReflectionClass($documentName); + $this->className = $entityName; } - /** Determines which fields get serialized. - * - * It is only serialized what is necessary for best unserialization performance. - * - * Parts that are also NOT serialized because they can not be properly unserialized: - * - reflClass (ReflectionClass) - * - reflFields (ReflectionProperty array) - * + + + /** * @return array The names of all the fields that should be serialized. */ public function __sleep() { // This metadata is always serialized/cached. return array( - 'boost', - 'className', - 'fieldMappings', - 'parameters', + 'type', 'index', - 'numberOfReplicas', - 'numberOfShards', - 'opType', 'parent', - 'timeToLive', - 'type', - 'value', - 'identifier', - 'rootMappings' + 'className', + 'riverImplementation', ); } + + + /** + * @return string + */ + public function getIndexName() + { + return $this->index->name; + } + + + + /** + * @return string + */ + public function getTypeName() + { + return $this->type->name; + } + + + /** * Get fully-qualified class name of this persistent class. * @@ -189,32 +128,23 @@ public function __sleep() public function getName() { return $this->className; - } + + /** * Gets the mapped identifier field name. * * The returned structure is an array of the identifier field names. * - * @return array + * @return string */ public function getIdentifier() { - return $this->identifier; + return $this->parentMetadata->getSingleIdentifierFieldName(); } - /** - * INTERNAL: - * Sets the mapped identifier key field of this class. - * Mainly used by the ClassMetadataFactory to assign inherited identifiers. - * - * @param mixed $identifier - */ - public function setIdentifier($identifier) - { - $this->identifier = $identifier; - } + /** * Gets the ReflectionClass instance for this mapped class. @@ -223,106 +153,81 @@ public function setIdentifier($identifier) */ public function getReflectionClass() { - return $this->reflClass; + return $this->parentMetadata->getReflectionClass(); } + + /** * Checks if the given field name is a mapped identifier for this class. * * @param string $fieldName + * * @return boolean */ public function isIdentifier($fieldName) { - return $this->identifier === $fieldName; + return $this->parentMetadata->isIdentifier($fieldName); } + + /** * Checks if the given field is a mapped property for this class. * * @param string $fieldName + * * @return boolean */ public function hasField($fieldName) { - return false; + return $this->parentMetadata->hasField($fieldName); } - /** - * This mapping is used in the _wakeup-method to set the reflFields after _sleep. - * - * @param \ReflectionProperty $field - * @param array $mapping - */ - public function addFieldMapping(\Reflector $field, $mapping = array()) - { - $fieldName = $field->getName(); - $this->fieldMappings[$fieldName] = $mapping; - } - - /** - * @param array $mapping - */ - public function addRootMapping($mapping = array()) - { - $this->rootMappings[] = $mapping; - } - - /** - * This mapping is used in the _wakeup-method to set the parameters after _sleep. - * - * @param \ReflectionProperty $field - * @param array $mapping - */ - public function addParameterMapping(\Reflector $field, $mapping = array()) - { - $fieldName = $field->getName(); - $this->parameters[$fieldName] = $mapping; - } - - /** - * @param \ReflectionProperty $field - */ - /*public function addField(\ReflectionProperty $field) - { - $fieldName = $field->getName(); - $this->reflFields[] = $field; - }*/ /** * Checks if the given field is a mapped association for this class. * * @param string $fieldName + * * @return boolean */ public function hasAssociation($fieldName) { - return false; + return $this->parentMetadata->hasAssociation($fieldName); } + + /** * Checks if the given field is a mapped single valued association for this class. * * @param string $fieldName + * * @return boolean */ public function isSingleValuedAssociation($fieldName) { - return false; + return $this->parentMetadata->isSingleValuedAssociation($fieldName); } + + /** * Checks if the given field is a mapped collection valued association for this class. * * @param string $fieldName + * * @return boolean */ public function isCollectionValuedAssociation($fieldName) { - return false; + return $this->parentMetadata->isCollectionValuedAssociation($fieldName); } + + /** * A numerically indexed list of field names of this persistent class. * @@ -332,19 +237,37 @@ public function isCollectionValuedAssociation($fieldName) */ public function getFieldNames() { - return array_keys($this->reflFields); + return $this->parentMetadata->getFieldNames(); + } + + + + /** + * Returns an array of identifier field names numerically indexed. + * + * @return array + */ + public function getIdentifierFieldNames() + { + return $this->parentMetadata->getIdentifierFieldNames(); } + + /** - * Currently not necessary but needed by Interface + * Returns a numerically indexed list of association names of this persistent class. + * + * This array includes identifier associations if present on this class. * * @return array */ public function getAssociationNames() { - return array(); + return $this->parentMetadata->getAssociationNames(); } + + /** * Returns a type name of this field. * @@ -352,87 +275,85 @@ public function getAssociationNames() * integer, string, boolean, float/double, datetime. * * @param string $fieldName + * * @return string */ public function getTypeOfField($fieldName) { - //@todo: check if $field exists - return gettype($this->$fieldName); + return $this->parentMetadata->getTypeOfField($fieldName); } + + /** - * Currently not necessary but needed by Interface - * + * Returns the target class name of the given association. * * @param string $assocName + * * @return string */ public function getAssociationTargetClass($assocName) { - return ''; + return $this->parentMetadata->getAssociationTargetClass($assocName); } + + + /** + * Checks if the association is the inverse side of a bidirectional association. + * + * @param string $assocName + * + * @return boolean + */ public function isAssociationInverseSide($assocName) { - return ''; + return $this->parentMetadata->isAssociationInverseSide($assocName); } + + + /** + * Returns the target field of the owning side of the association. + * + * @param string $assocName + * + * @return string + */ public function getAssociationMappedByTargetField($assocName) { - return ''; + return $this->parentMetadata->getAssociationMappedByTargetField($assocName); } + + /** - * Return the identifier of this object as an array with field name as key. + * Returns the identifier of this object as an array with field name as key. * * Has to return an empty array if no identifier isset. * * @param object $object + * * @return array */ public function getIdentifierValues($object) { - // TODO: Implement getIdentifierValues() method. + return $this->parentMetadata->getIdentifierValues($object); } - /** - * Returns an array of identifier field names numerically indexed. - * - * @return array - */ - public function getIdentifierFieldNames() + + + public function wakeupReflection(ReflectionService $reflService, ClassMetadataInterface $parentMetadata) { - // TODO: Implement getIdentifierFieldNames() method. + $this->parentMetadata = $parentMetadata; } - /** - * Restores some state that can not be serialized/unserialized. - * - * @param \Doctrine\Common\Persistence\Mapping\ReflectionService $reflService - * - * @return void - */ - public function wakeupReflection($reflService) - { - // Restore ReflectionClass and properties - $this->reflClass = $reflService->getClass($this->className); - foreach ($this->fieldMappings as $field => $mapping) { - $this->reflFields[$field] = $reflService->getAccessibleProperty($this->className, $field); - } - } - /** - * Initializes a new ClassMetadata instance that will hold the object-relational mapping - * metadata of the class with the given name. - * - * @param \Doctrine\Common\Persistence\Mapping\ReflectionService $reflService The reflection service. - * - * @return void - */ - public function initializeReflection($reflService) + public function __clone() { - $this->reflClass = $reflService->getClass($this->className); - $this->className = $this->reflClass->getName(); // normalize classname + $this->type = clone $this->type; + $this->index = clone $this->index; } + } diff --git a/lib/Doctrine/Search/Mapping/ClassMetadataFactory.php b/lib/Doctrine/Search/Mapping/ClassMetadataFactory.php index 9fca8f2..dd6b0d8 100644 --- a/lib/Doctrine/Search/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/Search/Mapping/ClassMetadataFactory.php @@ -19,6 +19,9 @@ namespace Doctrine\Search\Mapping; +use Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain; +use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Search\Mapping\Driver\DependentMappingDriver; use Doctrine\Search\SearchManager; use Doctrine\Search\Configuration; use Doctrine\Common\Persistence\Mapping\AbstractClassMetadataFactory; @@ -35,6 +38,8 @@ * @link www.doctrine-project.com * @since 1.0 * @author Mike Lohmann + * + * @method ClassMetadata[] getAllMetadata() */ class ClassMetadataFactory extends AbstractClassMetadataFactory { @@ -58,14 +63,41 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory */ private $evm; + /** + * @var AbstractClassMetadataFactory + */ + private $parentMetadataFactory; + + /** + * @var TypeMetadataFactory + */ + private $typeMetadataFactory; + /** * {@inheritDoc} */ protected function initialize() { - $this->driver = $this->config->getMetadataDriverImpl(); + $om = $this->sm->getObjectManager(); + $parentMetadataFactory = $om->getMetadataFactory(); + if (!$parentMetadataFactory instanceof AbstractClassMetadataFactory) { + throw new \LogicException("Parent metadata factory must be an instanceof AbstractClassMetadataFactory"); + } + + $parentMetadataFactory->initialize(); + + $driver = $this->config->getMetadataDriverImpl(); + foreach ($driver instanceof MappingDriverChain ? $driver->getDrivers() : array($driver) as $innerDriver) { + if (!$innerDriver instanceof DependentMappingDriver) { + throw new \LogicException("Driver must implement DependentMappingDriver interface"); + } + $innerDriver->setParentDriver($parentMetadataFactory->getDriver()); + } + + $this->driver = $driver; $this->evm = $this->sm->getEventManager(); - $this->initialized = true; + $this->parentMetadataFactory = $parentMetadataFactory; + $this->initialized = TRUE; } /** @@ -78,6 +110,14 @@ public function setSearchManager(SearchManager $sm) $this->sm = $sm; } + /** + * @param TypeMetadataFactory $factory + */ + public function setTypeMetadataFactory(TypeMetadataFactory $factory) + { + $this->typeMetadataFactory = $factory; + } + /** * Sets the Configuration instance * @@ -125,6 +165,10 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS //Manipulates $classMetadata; $this->driver->loadMetadataForClass($class->getName(), $class); + if (($prefix = $this->config->getIndexPrefix()) !== NULL) { + $class->index->name = $prefix . $class->index->name; + } + if ($this->evm->hasListeners(Events::loadClassMetadata)) { $eventArgs = new LoadClassMetadataEventArgs($class, $this->sm); $this->evm->dispatchEvent(Events::loadClassMetadata, $eventArgs); @@ -139,7 +183,17 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS */ protected function newClassMetadataInstance($className) { - return new ClassMetadata($className); + $metadata = new ClassMetadata($className); + + if (empty($metadata->type)) { + $metadata->type = $this->typeMetadataFactory->createTypeMetadata($className); + } + + if (empty($metadata->index)) { + $metadata->index = new IndexMetadata(); + } + + return $metadata; } /** @@ -152,7 +206,12 @@ protected function newClassMetadataInstance($className) */ protected function wakeupReflection(ClassMetadataInterface $class, ReflectionService $reflService) { - $class->wakeupReflection($reflService); + if (!$this->parentMetadataFactory) { + $om = $this->sm->getObjectManager(); + $this->parentMetadataFactory = $om->getMetadataFactory(); + } + + $class->wakeupReflection($reflService, $this->parentMetadataFactory->getMetadataFor($class->getName())); } /** @@ -164,7 +223,7 @@ protected function wakeupReflection(ClassMetadataInterface $class, ReflectionSer */ protected function initializeReflection(ClassMetadataInterface $class, ReflectionService $reflService) { - $class->initializeReflection($reflService); + $this->wakeupReflection($class, $reflService); } /** diff --git a/lib/Doctrine/Search/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/Search/Mapping/Driver/AnnotationDriver.php deleted file mode 100755 index 838da68..0000000 --- a/lib/Doctrine/Search/Mapping/Driver/AnnotationDriver.php +++ /dev/null @@ -1,210 +0,0 @@ -. - */ - -namespace Doctrine\Search\Mapping\Driver; - -use Doctrine\Common\Persistence\Mapping\Driver\AnnotationDriver as AbstractAnnotationDriver; -use Doctrine\Search\Mapping\Annotations as Search; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; -use Doctrine\Search\Exception\Driver as DriverException; - -/** - * The AnnotationDriver reads the mapping metadata from docblock annotations. - * - * @link www.doctrine-project.org - * @since 1.0 - * @author Mike Lohmann - */ -class AnnotationDriver extends AbstractAnnotationDriver -{ - /** - * {@inheritDoc} - */ - protected $entityAnnotationClasses = array( - 'Doctrine\\Search\\Mapping\\Annotations\\Searchable' => 1, - 'Doctrine\\Search\\Mapping\\Annotations\\ElasticSearchable' => 2, - 'Doctrine\\Search\\Mapping\\Annotations\\ElasticRoot' => 3, - ); - - protected $entityRootAnnotationClass = 'Doctrine\\Search\\Mapping\\Annotations\\ElasticRoot'; - - protected $entityIdAnnotationClass = 'Doctrine\\Search\\Mapping\\Annotations\\Id'; - - protected $entityParamAnnotationClass = 'Doctrine\\Search\\Mapping\\Annotations\\Parameter'; - - /** - * Document fields annotation classes, ordered by precedence. - */ - protected $entityFieldAnnotationClasses = array( - 'Doctrine\\Search\\Mapping\\Annotations\\Id', //Only here for convenience - 'Doctrine\\Search\\Mapping\\Annotations\\Parameter', //Only here for convenience - 'Doctrine\\Search\\Mapping\\Annotations\\Field', - 'Doctrine\\Search\\Mapping\\Annotations\\ElasticField', - 'Doctrine\\Search\\Mapping\\Annotations\\SolrField', - ); - - - - /** - * @param string $className - * @param ClassMetadata|\Doctrine\Search\Mapping\ClassMetadata $metadata - * - * @throws \ReflectionException - */ - public function loadMetadataForClass($className, ClassMetadata $metadata) - { - $reflClass = $metadata->getReflectionClass(); - - if (!$reflClass) { - $reflClass = new \ReflectionClass((string)$className); - } - - $reflProperties = $reflClass->getProperties(); - $reflMethods = $reflClass->getMethods(); - - $this->extractClassAnnotations($reflClass, $metadata); - $this->extractPropertiesAnnotations($reflProperties, $metadata); - $this->extractMethodsAnnotations($reflMethods, $metadata); - } - - - /** - * This function extracts the class annotations for search from the given reflected class and writes - * them into metadata. - * - * @param \ReflectionClass $reflClass - * @param ClassMetadata|\Doctrine\Search\Mapping\ClassMetadata $metadata - * - * @return ClassMetadata|\Doctrine\Search\Mapping\ClassMetadata - * - * @throws DriverException\ClassIsNotAValidDocumentException|DriverException\PropertyDoesNotExistsInMetadataException - */ - private function extractClassAnnotations(\ReflectionClass $reflClass, ClassMetadata $metadata) - { - $documentsClassAnnotations = array(); - foreach ($this->reader->getClassAnnotations($reflClass) as $annotation) { - foreach ($this->entityAnnotationClasses as $annotationClass => $index) { - if ($annotation instanceof $this->entityRootAnnotationClass) { - $metadata->addRootMapping($annotation); - break; - } elseif ($annotation instanceof $annotationClass) { - $documentsClassAnnotations[$index] = $annotation; - break; - } - } - } - - if (!$documentsClassAnnotations) { - throw new DriverException\ClassIsNotAValidDocumentException($metadata->getName()); - } - - //choose only one (the first one) - $annotationClass = reset($documentsClassAnnotations); - $reflClassAnnotations = new \ReflectionClass($annotationClass); - $metadata = $this->addValuesToMetadata( - $reflClassAnnotations->getProperties(), - $metadata, - $annotationClass - ); - - return $metadata; - } - - /** - * Extract the property annotations. - * - * @param \ReflectionProperty[] $reflProperties - * @param ClassMetadata|\Doctrine\Search\Mapping\ClassMetadata $metadata - * - * @return ClassMetadata|\Doctrine\Search\Mapping\ClassMetadata - */ - private function extractPropertiesAnnotations(array $reflProperties, ClassMetadata $metadata) - { - $documentsFieldAnnotations = array(); - foreach ($reflProperties as $reflProperty) { - foreach ($this->reader->getPropertyAnnotations($reflProperty) as $annotation) { - foreach ($this->entityFieldAnnotationClasses as $fieldAnnotationClass) { - if ($annotation instanceof $fieldAnnotationClass) { - if ($annotation instanceof $this->entityIdAnnotationClass) { - $metadata->setIdentifier($reflProperty->name); - } elseif ($annotation instanceof $this->entityParamAnnotationClass) { - $metadata->addParameterMapping($reflProperty, $annotation); - } else { - $metadata->addFieldMapping($reflProperty, $annotation); - } - continue 2; - } - } - } - } - - return $metadata; - } - - /** - * Extract the methods annotations. - * - * @param \ReflectionMethod[] $reflMethods - * @param ClassMetadata|\Doctrine\Search\Mapping\ClassMetadata $metadata - * - * @return ClassMetadata|\Doctrine\Search\Mapping\ClassMetadata - */ - private function extractMethodsAnnotations(array $reflMethods, ClassMetadata $metadata) - { - $documentsFieldAnnotations = array(); - foreach ($reflMethods as $reflMethod) { - foreach ($this->reader->getMethodAnnotations($reflMethod) as $annotation) { - foreach ($this->entityFieldAnnotationClasses as $fieldAnnotationClass) { - if ($annotation instanceof $fieldAnnotationClass) { - $metadata->addFieldMapping($reflMethod, $annotation); - continue 2; - } - } - } - } - - return $metadata; - } - - /** - * @param \ReflectionProperty[] $reflectedClassProperties - * @param ClassMetadata|\Doctrine\Search\Mapping\ClassMetadata $metadata - * @param string $class - * - * @return ClassMetadata|\Doctrine\Search\Mapping\ClassMetadata - * - * @throws DriverException\PropertyDoesNotExistsInMetadataException - */ - private function addValuesToMetadata(array $reflectedClassProperties, ClassMetadata $metadata, $class) - { - foreach ($reflectedClassProperties as $reflectedProperty) { - $propertyName = $reflectedProperty->getName(); - - if (false === property_exists($metadata, $propertyName)) { - throw new DriverException\PropertyDoesNotExistsInMetadataException($reflectedProperty->getName()); - } else { - if (!is_null($class->$propertyName)) { - $metadata->$propertyName = $class->$propertyName; - } - } - } - - return $metadata; - } -} diff --git a/lib/Doctrine/Search/Mapping/Driver/DependentMappingDriver.php b/lib/Doctrine/Search/Mapping/Driver/DependentMappingDriver.php new file mode 100644 index 0000000..1eca94c --- /dev/null +++ b/lib/Doctrine/Search/Mapping/Driver/DependentMappingDriver.php @@ -0,0 +1,122 @@ +. + */ + +namespace Doctrine\Search\Mapping\Driver; + +use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver; + + + +/** + * If a driver implements this interface, + * it should delegate the getAllClassNames method to the parent driver. + * + * @author Filip Procházka + */ +class DependentMappingDriver implements MappingDriver +{ + + /** + * @var MappingDriver + */ + private $innerDriver; + + /** + * @var MappingDriver + */ + private $parentDriver; + + /** + * @var array + */ + private $classNames; + + + + public function __construct(MappingDriver $innerDriver) + { + $this->innerDriver = $innerDriver; + } + + + + /** + * @param MappingDriver $driver + */ + public function setParentDriver(MappingDriver $driver) + { + $this->parentDriver = $driver; + } + + + + /** + * Loads the metadata for the specified class into the provided container. + * + * @param string $className + * @param ClassMetadata $metadata + */ + public function loadMetadataForClass($className, ClassMetadata $metadata) + { + $this->innerDriver->loadMetadataForClass($className, $metadata); + } + + + + /** + * Gets the names of all mapped classes known to this driver. + * + * @return array The names of all mapped classes known to this driver. + */ + public function getAllClassNames() + { + if ($this->classNames !== NULL) { + return $this->classNames; + } + + if ($this->parentDriver === NULL) { + throw new \LogicException(sprintf("Setting the parent driver using %s::setParentDriver(\$driver) is mandatory.", get_called_class())); + } + + $classes = array(); + foreach ($this->parentDriver->getAllClassNames() as $className) { + if (!$this->isTransient($className)) { + $classes[] = $className; + } + } + + return $this->classNames = $classes; + } + + + + /** + * Returns whether the class with the specified name should have its metadata loaded. + * This is only the case if it is either mapped as an Entity or a MappedSuperclass. + * + * @param string $className + * @return boolean + */ + public function isTransient($className) + { + return $this->innerDriver->isTransient($className); + } + +} diff --git a/lib/Doctrine/Search/Mapping/Driver/NeonDriver.php b/lib/Doctrine/Search/Mapping/Driver/NeonDriver.php new file mode 100644 index 0000000..dbdbcb5 --- /dev/null +++ b/lib/Doctrine/Search/Mapping/Driver/NeonDriver.php @@ -0,0 +1,246 @@ + + */ +class NeonDriver implements MappingDriver +{ + + /** + * @var array + */ + private $classNames; + + /** + * @var string + */ + private $metadataDirectory; + + /** + * @var array + */ + private $typesMapping; + + /** + * @var array + */ + private $indexesMapping; + + + + public function __construct($metadataDirectory) + { + $this->metadataDirectory = $metadataDirectory; + } + + + + /** + * Loads the metadata for the specified class into the provided container. + * + * @param string $className + * @param ClassMetadata|\Doctrine\Search\Mapping\ClassMetadata $metadata + * + * @return void + */ + public function loadMetadataForClass($className, ClassMetadata $metadata) + { + if (!$typeMapping = $this->getTypeMapping($className)) { + return; + } + + $this->loadTypeMapping($metadata->type, $typeMapping); + + $indexMapping = $this->getIndexMapping($typeMapping['index']); + $this->loadIndexMapping($metadata->index, $indexMapping); + + if (!empty($typeMapping['river'])) { + $metadata->riverImplementation = $typeMapping['river']; + } + } + + + + protected function loadTypeMapping(TypeMetadata $type, $typeMapping) + { + $type->name = $typeMapping['type']; + + if (!empty($typeMapping['properties'])) { + $type->setProperties($typeMapping['properties']); + } + + if (!empty($typeMapping['parameters'])) { + $type->setParameters($typeMapping['parameters']); + } + + unset($typeMapping['properties'], $typeMapping['parameters']); + + $type->source = !empty($typeMapping['source']); + $type->boost = !empty($typeMapping['boost']) ? $typeMapping['boost'] : NULL; + + unset($typeMapping['class'], $typeMapping['subclasses'], $typeMapping['river'], $typeMapping['index'], $typeMapping['source'], $typeMapping['type']); + $type->setSettings((array) $typeMapping); + } + + + + protected function loadIndexMapping(IndexMetadata $index, $indexMapping) + { + foreach (array('name', 'numberOfShards', 'numberOfReplicas', 'charFilter', 'filter', 'analyzer') as $key) { + if (!array_key_exists($key, $indexMapping)) { + continue; + } + + $index->{$key} = $indexMapping[$key]; + } + } + + + + public function getAllClassNames() + { + if ($this->classNames !== NULL) { + return $this->classNames; + } + + $classes = array(); + foreach ($this->getTypesMapping() as $meta) { + $classes[] = $meta['class']; + } + + return $this->classNames = $classes; + } + + + + /** + * Returns whether the class with the specified name should have its metadata loaded. + * + * @param string $className + * @return boolean + */ + public function isTransient($className) + { + return ! $this->getTypeMapping($className); + } + + + + /** + * @param string $className + * @return array + */ + protected function getTypeMapping($className) + { + foreach ($this->getTypesMapping() as $mapping) { + if ($mapping['class'] === $className) { + return $mapping; + } + + if (isset($mapping['subclasses']) && in_array($className, $mapping['subclasses'])) { + return $mapping; + } + } + + return NULL; + } + + + + /** + * @return array + */ + protected function getTypesMapping() + { + if ($this->typesMapping !== NULL) { + return $this->typesMapping; + } + + $this->typesMapping = array(); + foreach (glob($this->metadataDirectory . '/*.type.neon') as $file) { + $meta = Neon::decode(file_get_contents($file)); + if (!isset($meta['class'])) { + throw new \InvalidArgumentException("The metadata file $file is missing a required field 'class' with entity name."); + } + + $meta['type'] = basename($file, '.type.neon'); + $this->typesMapping[$meta['type']] = $meta; + } + + return $this->typesMapping; + } + + + + /** + * @param string $indexName + * @return array + */ + protected function getIndexMapping($indexName) + { + foreach ($this->getIndexesMapping() as $mapping) { + if ($mapping['name'] === $indexName) { + return $mapping; + } + } + + throw new InvalidArgumentException(sprintf('Metadata of index %s not found', $indexName)); + } + + + + /** + * @return array + */ + protected function getIndexesMapping() + { + if ($this->indexesMapping !== NULL) { + return $this->indexesMapping; + } + + // todo: refactor away usage of Nette\DI + $adapter = new Config\Adapters\NeonAdapter(); + + $this->indexesMapping = array(); + foreach (glob($this->metadataDirectory . '/*.index.neon') as $file) { + $meta = $adapter->load($file); + $meta['name'] = basename($file, '.index.neon'); + + // $indexConfig = Config\Helpers::merge($meta, $this->indexDefaults); + unset($analysisSection); + foreach (array('charFilter', 'filter', 'analyzer') as $analysisType) { + $analysisSection = $meta[$analysisType]; + unset($setup); + foreach ($analysisSection as $name => $setup) { + if (!Config\Helpers::isInheriting($setup)) { + continue; + } + $parent = Config\Helpers::takeParent($setup); + if (!isset($analysisSection[$parent])) { + throw new \Nette\Utils\AssertionException(sprintf('The %s.%s cannot inherit undefined %s.%s', $analysisType, $name, $analysisType, $parent)); + } + $analysisSection[$name] = Config\Helpers::merge($setup, $analysisSection[$parent]); + } + + $meta[$analysisType] = $analysisSection; + } + + $this->indexesMapping[$meta['name']] = $meta; + } + + return $this->indexesMapping; + } + +} diff --git a/lib/Doctrine/Search/Mapping/Helpers.php b/lib/Doctrine/Search/Mapping/Helpers.php new file mode 100644 index 0000000..7f11f21 --- /dev/null +++ b/lib/Doctrine/Search/Mapping/Helpers.php @@ -0,0 +1,44 @@ + + * @see http://php.net/manual/en/function.ucwords.php#92092 + */ +class Helpers +{ + + /** + * underscored to lower-camelcase + * e.g. "this_method_name" -> "thisMethodName" + * + * @param string $string + * @return string + */ + public function camelCase($string) + { + return preg_replace('/_(.?)/e', "strtoupper('$1')", $string); + } + + + + /** + * camelcase (lower or upper) to underscored + * e.g. "thisMethodName" -> "this_method_name" + * e.g. "ThisMethodName" -> "this_method_name" + * + * @param string $string + * @return string + */ + public function underscore($string) + { + return strtolower(preg_replace('/([^A-Z])([A-Z])/', "$1_$2", $string)); + } + +} diff --git a/lib/Doctrine/Search/Mapping/IndexMetadata.php b/lib/Doctrine/Search/Mapping/IndexMetadata.php new file mode 100644 index 0000000..059655b --- /dev/null +++ b/lib/Doctrine/Search/Mapping/IndexMetadata.php @@ -0,0 +1,43 @@ + + */ +class IndexMetadata +{ + + /** + * @var string + */ + public $name; + + /** + * @var int + */ + public $numberOfShards = 1; + + /** + * @var int + */ + public $numberOfReplicas = 1; + + /** + * @var array + */ + public $charFilter = array(); + + /** + * @var array + */ + public $filter = array(); + + /** + * @var array + */ + public $analyzer = array(); + +} diff --git a/lib/Doctrine/Search/Mapping/TypeMetadata.php b/lib/Doctrine/Search/Mapping/TypeMetadata.php new file mode 100644 index 0000000..21803f4 --- /dev/null +++ b/lib/Doctrine/Search/Mapping/TypeMetadata.php @@ -0,0 +1,84 @@ + + */ +abstract class TypeMetadata +{ + + /** + * @var string + */ + public $name; + + /** + * @var bool + */ + public $source = TRUE; + + /** + * @var integer + */ + public $boost; + + /** + * @var array + */ + public $settings = array(); + + /** + * @var array + */ + public $properties = array(); + + /** + * @var array + */ + public $parameters = array(); + + + + public function __construct($className) + { + $this->className = $className; + } + + + + public function setSettings(array $options) + { + $this->validateSettings($options); + $this->settings = $options; + } + + + + public function setProperties(array $properties) + { + foreach ($properties as $name => $property) { + $this->validateProperty($name, $property); + } + + $this->properties = $properties; + } + + + + public function setParameters(array $parameters) + { + foreach ($parameters as $name => $field) { + $this->parameters[is_numeric($name) ? $field : $name] = $field; + } + } + + + + abstract protected function validateSettings(array $options); + + abstract protected function validateProperty($name, array $property); + +} diff --git a/lib/Doctrine/Search/Mapping/TypeMetadataFactory.php b/lib/Doctrine/Search/Mapping/TypeMetadataFactory.php new file mode 100644 index 0000000..d0fe7fa --- /dev/null +++ b/lib/Doctrine/Search/Mapping/TypeMetadataFactory.php @@ -0,0 +1,22 @@ + + */ +interface TypeMetadataFactory +{ + + /** + * @param string $className + * @return TypeMetadata + */ + public function createTypeMetadata($className); + +} diff --git a/lib/Doctrine/Search/Query.php b/lib/Doctrine/Search/Query.php index 61bbf57..4300408 100644 --- a/lib/Doctrine/Search/Query.php +++ b/lib/Doctrine/Search/Query.php @@ -19,14 +19,16 @@ namespace Doctrine\Search; -use Doctrine\Search\Exception\DoctrineSearchException; +use Doctrine; use Elastica; + + class Query { const HYDRATE_BYPASS = -1; - const HYDRATE_INTERNAL = -2; + const HYDRATE_QUERY = NULL; const HYDRATION_PARAMETER = 'ids'; @@ -36,7 +38,7 @@ class Query protected $sm; /** - * @var object + * @var Elastica\Query */ protected $query; @@ -58,7 +60,7 @@ class Query /** * @var integer */ - protected $hydrationMode; + protected $hydrationMode = self::HYDRATE_INTERNAL; /** * @var boolean @@ -70,6 +72,11 @@ class Query */ protected $cacheLifetime; + /** + * @var string + */ + protected $resultCacheId; + /** * @var integer */ @@ -80,17 +87,32 @@ class Query */ protected $facets; + /** + * @var integer + */ + protected $firstResult; + + /** + * @var integer + */ + protected $maxResults; + + + public function __construct(SearchManager $sm) { $this->sm = $sm; } + + /** * Magic method to pass query building to the underlying query * object, saving the need to abstract. * * @param string $method * @param array $arguments + * @return Query */ public function __call($method, $arguments) { @@ -102,10 +124,13 @@ public function __call($method, $arguments) return $this; } + + /** * Specifies the searchable entity class to search against. * - * @param mixed $entityClasses + * @param string|array $entityClasses + * @return Query */ public function from($entityClasses) { @@ -113,27 +138,44 @@ public function from($entityClasses) return $this; } + + /** * Set the query object to be executed on the search engine * * @param mixed $query + * @return Query */ public function searchWith($query) { + $client = $this->sm->getClient(); + + if ($client instanceof ElasticSearch\Client) { + $query = Elastica\Query::create($query); + + } else { + throw new NotImplementedException; + } + $this->query = $query; return $this; } + + protected function getSearchManager() { return $this->sm; } + + /** * Set the hydration mode from the underlying query modes * or bypass and return search result directly from the client * * @param integer $mode + * @return Query */ public function setHydrationMode($mode) { @@ -141,37 +183,71 @@ public function setHydrationMode($mode) return $this; } + + /** * If hydrating with Doctrine then you can use the result cache * on the default or provided query * * @param boolean $useCache * @param integer $cacheLifetime + * @param string $resultCacheId + * @return Query */ - public function useResultCache($useCache, $cacheLifetime = null) + public function useResultCache($useCache, $cacheLifetime = null, $resultCacheId = NULL) { $this->useResultCache = $useCache; $this->cacheLifetime = $cacheLifetime; + $this->resultCacheId = $resultCacheId; return $this; } + + /** - * Return the total hit count for the given query as provided by - * the search engine. + * @return int */ - public function count() + public function getFirstResult() { - return $this->count; + return $this->firstResult; } + + /** - * @return array + * @param int $firstResult + * @return Query */ - public function getFacets() + public function setFirstResult($firstResult) { - return $this->facets; + $this->firstResult = $firstResult; + return $this; + } + + + + /** + * @return int + */ + public function getMaxResults() + { + return $this->maxResults; } + + + /** + * @param int $maxResults + * @return Query + */ + public function setMaxResults($maxResults) + { + $this->maxResults = $maxResults; + return $this; + } + + + /** * Set a custom Doctrine Query to execute in order to hydrate the search * engine results into required entities. The assumption is made the the @@ -180,36 +256,57 @@ public function getFacets() * * @param object $hydrationQuery * @param string $parameter + * @return Query */ public function hydrateWith($hydrationQuery, $parameter = null) { + if ($hydrationQuery instanceof Doctrine\ORM\QueryBuilder) { + if (!$this->entityClasses) { + $this->entityClasses = $hydrationQuery->getRootEntities(); + } + + $hydrationQuery = $hydrationQuery->getQuery(); + + } elseif ($hydrationQuery instanceof Doctrine\ORM\AbstractQuery) { + // pass + + } else { + throw new NotImplementedException(sprintf('Unsupported type of hydration query provided: %s', get_class($hydrationQuery))); + } + + $this->hydrationMode = self::HYDRATE_QUERY; $this->hydrationQuery = $hydrationQuery; if ($parameter) { $this->hydrationParameter = $parameter; } + return $this; } + + /** * Return a provided hydration query * - * @return object + * @return Doctrine\ORM\AbstractQuery */ protected function getHydrationQuery() { if (!$this->hydrationQuery) { - throw new DoctrineSearchException('A hydration query is required for hydrating results to entities.'); + throw new InvalidStateException('A hydration query is required for hydrating results to entities.'); } return $this->hydrationQuery; } + + /** * Execute search and hydrate results if required. * * @param integer $hydrationMode - * @throws DoctrineSearchException - * @return mixed + * @throws InvalidStateException + * @return array|Elastica\ResultSet|Searchable[] */ public function getResult($hydrationMode = null) { @@ -222,35 +319,66 @@ public function getResult($hydrationMode = null) $classes[] = $this->sm->getClassMetadata($entityClass); } - $resultSet = $this->getSearchManager()->getClient()->search($this->query, $classes); + if ($this->query instanceof Elastica\Query) { + if ($this->maxResults) { + $this->query->setSize($this->maxResults); + } + if ($this->firstResult) { + $this->query->setFrom($this->firstResult); + } + } + + $client = $this->getSearchManager()->getClient(); + $resultSet = $client->search($this->query, $classes); - // TODO: abstraction of support for different result sets if ($resultSet instanceof Elastica\ResultSet) { $this->count = $resultSet->getTotalHits(); $this->facets = $resultSet->getFacets(); $results = $resultSet->getResults(); } else { - $resultClass = get_class($resultSet); - throw new DoctrineSearchException("Unexpected result set class '$resultClass'"); + throw new NotImplementedException(sprintf('Unexpected result set class \'%s\'', get_class($resultSet))); } // Return results depending on hydration mode if ($this->hydrationMode == self::HYDRATE_BYPASS) { return $resultSet; + } elseif ($this->hydrationMode == self::HYDRATE_INTERNAL) { return $this->sm->getUnitOfWork()->hydrateCollection($classes, $resultSet); } // Document ids are used to lookup dbms results - $fn = function ($result) { + $ids = array_map(function ($result) { + /** @var Elastica\Document $result */ return $result->getId(); - }; - $ids = array_map($fn, $results); + }, $results); return $this->getHydrationQuery() ->setParameter($this->hydrationParameter, $ids ?: null) - ->useResultCache($this->useResultCache, $this->cacheLifetime) + ->useResultCache($this->useResultCache, $this->cacheLifetime, $this->resultCacheId) ->getResult($this->hydrationMode); } + + + + /** + * Return the total hit count for the given query as provided by + * the search engine. + */ + public function count() + { + return $this->count; + } + + + + /** + * @return array + */ + public function getFacets() + { + return $this->facets; + } + } diff --git a/lib/Doctrine/Search/SchemaManager.php b/lib/Doctrine/Search/SchemaManager.php new file mode 100644 index 0000000..3e79f63 --- /dev/null +++ b/lib/Doctrine/Search/SchemaManager.php @@ -0,0 +1,79 @@ + + */ +interface SchemaManager +{ + + /** + * @param array|ClassMetadata[] $classes + */ + public function dropMappings(array $classes); + + /** + * @param array|ClassMetadata[] $classes + * @param bool $withAliases + * @return array + */ + public function createMappings(array $classes, $withAliases = FALSE); + + /** + * @param array $aliases + * @return void + */ + public function createAliases(array $aliases); + + /** + * @param string $index + * @return boolean + */ + public function hasIndex($index); + + /** + * @param ClassMetadata $class + * @return boolean + */ + public function createIndex(ClassMetadata $class); + + /** + * @param string $index + * @return boolean + */ + public function dropIndex($index); + + /** + * @param ClassMetadata $class + * @return boolean + */ + public function hasType(ClassMetadata $class); + + /** + * @param ClassMetadata $class + * @return boolean + */ + public function createType(ClassMetadata $class); + + /** + * @param ClassMetadata $class + * @return boolean + */ + public function dropType(ClassMetadata $class); + + /** + * @param string $alias + * @param string $original + */ + public function createAlias($alias, $original); + +} diff --git a/lib/Doctrine/Search/SearchClientInterface.php b/lib/Doctrine/Search/SearchClient.php similarity index 74% rename from lib/Doctrine/Search/SearchClientInterface.php rename to lib/Doctrine/Search/SearchClient.php index 8042c9a..ecfee87 100755 --- a/lib/Doctrine/Search/SearchClientInterface.php +++ b/lib/Doctrine/Search/SearchClient.php @@ -20,13 +20,17 @@ namespace Doctrine\Search; use Doctrine\Search\Mapping\ClassMetadata; +use Doctrine\Search\Mapping\TypeMetadata; +use Doctrine\Search\Mapping\TypeMetadataFactory; + + /** * Interface for a Doctrine SearchManager class to implement. * * @author Mike Lohmann */ -interface SearchClientInterface +interface SearchClient extends TypeMetadataFactory { /** * Finds document by id. @@ -34,7 +38,7 @@ interface SearchClientInterface * @param ClassMetadata $class * @param mixed $id * @param array $options - * @throws \Doctrine\Search\Exception\NoResultException + * @throws \Doctrine\Search\NoResultException */ public function find(ClassMetadata $class, $id, $options = array()); @@ -44,7 +48,7 @@ public function find(ClassMetadata $class, $id, $options = array()); * @param ClassMetadata $class * @param string $field * @param mixed $value - * @throws \Doctrine\Search\Exception\NoResultException + * @throws \Doctrine\Search\NoResultException */ public function findOneBy(ClassMetadata $class, $field, $value); @@ -63,50 +67,6 @@ public function findAll(array $classes); */ public function search($query, array $classes); - /** - * Creates a document index - * - * @param string $name The name of the index. - * @param string $config The configuration of the index. - */ - public function createIndex($name, array $config = array()); - - /** - * Gets a document index reference - * - * @param string $name The name of the index. - */ - public function getIndex($name); - - /** - * Deletes an index and its types and documents - * - * @param string $index - */ - public function deleteIndex($index); - - /** - * Refresh the index to make documents available for search - * - * @param string $index - */ - public function refreshIndex($index); - - /** - * Create a document type mapping as defined in the - * class annotations - * - * @param ClassMetadata $metadata - */ - public function createType(ClassMetadata $metadata); - - /** - * Delete a document type - * - * @param ClassMetadata $metadata - */ - public function deleteType(ClassMetadata $metadata); - /** * Adds documents of a given type to the specified index * @@ -131,4 +91,18 @@ public function removeDocuments(ClassMetadata $class, array $documents); * @param object $query */ public function removeAll(ClassMetadata $class, $query = null); + + /** + * Refresh the index to make documents available for search + * + * @param string $index + */ + public function refreshIndex($index); + + /** + * @param string $className + * @return TypeMetadata + */ + public function createTypeMetadata($className); + } diff --git a/lib/Doctrine/Search/SearchManager.php b/lib/Doctrine/Search/SearchManager.php index c4a1bfa..32357dc 100755 --- a/lib/Doctrine/Search/SearchManager.php +++ b/lib/Doctrine/Search/SearchManager.php @@ -19,9 +19,10 @@ namespace Doctrine\Search; -use Doctrine\Common\Persistence\ObjectManager; -use Doctrine\Search\Exception\UnexpectedTypeException; use Doctrine\Common\EventManager; +use Doctrine\Common\Persistence\ObjectManager; + + /** * Interface for a Doctrine SearchManager class to implement. @@ -31,7 +32,7 @@ class SearchManager implements ObjectManager { /** - * @var SearchClientInterface + * @var SearchClient */ private $client; @@ -53,7 +54,7 @@ class SearchManager implements ObjectManager /** * @var ObjectManager */ - private $entityManager; + private $objectManager; /** * The event manager that is the central point of the event system. @@ -80,10 +81,10 @@ class SearchManager implements ObjectManager * Constructor * * @param Configuration $config - * @param SearchClientInterface $client + * @param SearchClient $client * @param EventManager $eventManager */ - public function __construct(Configuration $config, SearchClientInterface $client, EventManager $eventManager) + public function __construct(Configuration $config, SearchClient $client, EventManager $eventManager) { $this->configuration = $config; $this->client = $client; @@ -91,31 +92,30 @@ public function __construct(Configuration $config, SearchClientInterface $client $this->metadataFactory = $this->configuration->getClassMetadataFactory(); $this->metadataFactory->setSearchManager($this); + $this->metadataFactory->setTypeMetadataFactory($client); $this->metadataFactory->setConfiguration($this->configuration); $this->metadataFactory->setCacheDriver($this->configuration->getMetadataCacheImpl()); $this->serializer = $this->configuration->getEntitySerializer(); - $this->entityManager = $this->configuration->getEntityManager(); + $this->objectManager = $this->configuration->getObjectManager(); $this->unitOfWork = new UnitOfWork($this); } /** - * Inject a Doctrine 2 object manager - * - * @param ObjectManager $om + * @param ObjectManager $objectManager */ - public function setEntityManager(ObjectManager $om) + public function setObjectManager(ObjectManager $objectManager) { - $this->entityManager = $om; + $this->objectManager = $objectManager; } /** - * @return ObjectManager|\Doctrine\ORM\EntityManager + * @return ObjectManager */ - public function getEntityManager() + public function getObjectManager() { - return $this->entityManager; + return $this->objectManager; } /** @@ -167,7 +167,7 @@ public function getClassMetadataFactory() } /** - * @return SearchClientInterface + * @return SearchClient */ public function getClient() { @@ -277,23 +277,7 @@ public function getRepository($entityName) } /** - * Gets a collection of entity repositories. - * - * @param array $entityNames The names of the entities. - * @return EntityRepositoryCollection The repository class. - */ - public function getRepositories(array $entityNames) - { - $repositoryCollection = new EntityRepositoryCollection($this); - foreach ($entityNames as $entityName) { - $repositoryCollection->addRepository($this->getRepository($entityName)); - } - return $repositoryCollection; - } - - /** - * Returns a search engine Query wrapper which can be executed - * to retrieve results. + * Returns a search engine Query wrapper which can be executed to retrieve results. * * @return Query */ @@ -332,4 +316,5 @@ public function detach($object) public function refresh($object) { } + } diff --git a/lib/Doctrine/Search/Searchable.php b/lib/Doctrine/Search/Searchable.php new file mode 100644 index 0000000..dd6999c --- /dev/null +++ b/lib/Doctrine/Search/Searchable.php @@ -0,0 +1,26 @@ + + */ +interface Searchable +{ + +// /** +// * @return array +// */ +// public function toArray(); + + + +// /** +// * @param array $data +// * @return void +// */ +// public function fromArray($data); + +} diff --git a/lib/Doctrine/Search/Serializer/AnnotationSerializer.php b/lib/Doctrine/Search/Serializer/AnnotationSerializer.php deleted file mode 100644 index 0ea6330..0000000 --- a/lib/Doctrine/Search/Serializer/AnnotationSerializer.php +++ /dev/null @@ -1,33 +0,0 @@ -. - */ - -namespace Doctrine\Search\Serializer; - -use Doctrine\Search\SerializerInterface; - -class AnnotationSerializer implements SerializerInterface -{ - public function serialize($object) - { - } - - public function deserialize($entityName, $data) - { - } -} diff --git a/lib/Doctrine/Search/Serializer/ChainSerializer.php b/lib/Doctrine/Search/Serializer/ChainSerializer.php new file mode 100644 index 0000000..c58b203 --- /dev/null +++ b/lib/Doctrine/Search/Serializer/ChainSerializer.php @@ -0,0 +1,85 @@ + + */ +class ChainSerializer implements SerializerInterface +{ + + /** + * @var SerializerInterface[] + */ + private $serializers = []; + + /** + * @var SerializerInterface + */ + private $defaultSerializer; + + + + public function addSerializer($classType, SerializerInterface $serializer) + { + $this->serializers[$classType] = $serializer; + } + + + + public function setDefaultSerializer(SerializerInterface $serializer) + { + $this->defaultSerializer = $serializer; + } + + + + /** + * @param object $object + * @throws DefaultSerializerNotProvidedException + * @return string + */ + public function serialize($object) + { + foreach ($this->serializers as $classType => $serializer) { + if ($object instanceof $classType) { + return $serializer->serialize($object); + } + } + + if (!$this->defaultSerializer) { + throw new DefaultSerializerNotProvidedException; + } + + return $this->defaultSerializer->serialize($object); + } + + + + /** + * @param string $entityName + * @param string $data + * @throws DefaultSerializerNotProvidedException + * @return object + */ + public function deserialize($entityName, $data) + { + $classType = $entityName; + if (isset($this->serializers[$classType])) { + return $this->serializers[$classType]->deserialize($entityName, $data); + } + + if (!$this->defaultSerializer) { + throw new DefaultSerializerNotProvidedException; + } + + return $this->defaultSerializer->deserialize($entityName, $data); + } + +} diff --git a/lib/Doctrine/Search/Serializer/JMSSerializer.php b/lib/Doctrine/Search/Serializer/JMSSerializer.php index 8ed383f..b153ac3 100644 --- a/lib/Doctrine/Search/Serializer/JMSSerializer.php +++ b/lib/Doctrine/Search/Serializer/JMSSerializer.php @@ -20,33 +20,66 @@ namespace Doctrine\Search\Serializer; use Doctrine\Search\SerializerInterface; -use JMS\Serializer\SerializerBuilder; -use JMS\Serializer\SerializationContext; -use JMS\Serializer\Naming\SerializedNameAnnotationStrategy; use JMS\Serializer\Naming\IdenticalPropertyNamingStrategy; +use JMS\Serializer\Naming\SerializedNameAnnotationStrategy; +use JMS\Serializer\SerializationContext; +use JMS\Serializer\Serializer; +use JMS\Serializer\SerializerBuilder; + + class JMSSerializer implements SerializerInterface { + + /** + * @var Serializer + */ protected $serializer; + + /** + * @var SerializationContext + */ protected $context; - public function __construct(SerializationContext $context = null) + + + public function __construct(Serializer $serializer = NULL, SerializationContext $context = NULL) { $this->context = $context; - $this->serializer = SerializerBuilder::create() - ->setPropertyNamingStrategy(new SerializedNameAnnotationStrategy(new IdenticalPropertyNamingStrategy())) - ->addDefaultHandlers() - ->build(); + $this->serializer = $serializer; } + + public function serialize($object) { - $context = $this->context ? clone $this->context : null; - return json_decode($this->serializer->serialize($object, 'json', $context), true); + $context = $this->context ? clone $this->context : NULL; + + return json_decode($this->getSerializer()->serialize($object, 'json', $context), TRUE); } + + public function deserialize($entityName, $data) { - return $this->serializer->deserialize($data, $entityName, 'json'); + return $this->getSerializer()->deserialize($data, $entityName, 'json'); + } + + + + /** + * @return Serializer + */ + protected function getSerializer() + { + if (!$this->serializer === NULL) { + $this->serializer = SerializerBuilder::create() + ->setPropertyNamingStrategy(new SerializedNameAnnotationStrategy(new IdenticalPropertyNamingStrategy())) + ->addDefaultHandlers() + ->build(); + } + + return $this->serializer; } + } diff --git a/lib/Doctrine/Search/Solr/Client.php b/lib/Doctrine/Search/Solr/Client.php deleted file mode 100755 index 6552b6d..0000000 --- a/lib/Doctrine/Search/Solr/Client.php +++ /dev/null @@ -1,97 +0,0 @@ -. - */ - -namespace Doctrine\Search\Solr; - -use Doctrine\Search\SearchClientInterface; -use Doctrine\Search\Mapping\ClassMetadata; -use Doctrine\Common\Persistence\ObjectManager; - -/** - * SearchManager for Solr-Backend - * - * @author Mike Lohmann - */ -class Client implements SearchClientInterface -{ - - private $config; - - private $connection; - - /* - * @param Connection $conn - * @param Configuration $config - */ - public function __construct(Connection $conn = null, Configuration $config = null) - { - $this->connection = $conn; - $this->config = $config; - } - - public function find($index, $type, $query) - { - - } - - public function createIndex($index, array $data) - { - - } - - public function createType(ClassMetadata $metadata) - { - - } - - public function deleteType(ClassMetadata $metadata) - { - - } - - public function getIndex($index) - { - - } - - public function deleteIndex($index) - { - - } - - public function refreshIndex($index) - { - - } - - public function addDocuments($index, $type, array $documents) - { - - } - - public function removeDocuments($index, $type, array $documents) - { - - } - - public function removeAll($index, $type) - { - - } -} diff --git a/lib/Doctrine/Search/Solr/Configuration.php b/lib/Doctrine/Search/Solr/Configuration.php deleted file mode 100755 index 052e519..0000000 --- a/lib/Doctrine/Search/Solr/Configuration.php +++ /dev/null @@ -1,35 +0,0 @@ -. - */ - -namespace Doctrine\Search\Solr; - -/** - * Configuration handler for Solr-Backend - * - * @author Mike Lohmann - */ -class Configuration -{ - - - public function __construct() - { - - } -} diff --git a/lib/Doctrine/Search/Solr/Connection.php b/lib/Doctrine/Search/Solr/Connection.php deleted file mode 100755 index 4bbd344..0000000 --- a/lib/Doctrine/Search/Solr/Connection.php +++ /dev/null @@ -1,51 +0,0 @@ -. - */ - -namespace Doctrine\Search\Solr; - -use Doctrine\Search\Http\ClientInterface as HttpClientInterface; - -/** - * Connections handler for Solr-Backend - * - * @author Mike Lohmann - */ -class Connection -{ - private $host; - - private $port; - - private $path; - - private $httpClient; - - public function __construct(HttpClientInterface $httpClient, $host = null, $port = null, $path = null) - { - $this->host = $host; - $this->port = $port; - $this->path = $path; - $this->httpClient = $httpClient; - } - - public function initialize() - { - - } -} diff --git a/lib/Doctrine/Search/Tools/OrmSearchableListener.php b/lib/Doctrine/Search/Tools/OrmSearchableListener.php new file mode 100644 index 0000000..1d84df0 --- /dev/null +++ b/lib/Doctrine/Search/Tools/OrmSearchableListener.php @@ -0,0 +1,78 @@ + + */ +class OrmSearchableListener implements EventSubscriber +{ + + /** + * @var SearchManager + */ + private $sm; + + + + public function __construct(SearchManager $sm) + { + $this->sm = $sm; + } + + + + public function getSubscribedEvents() + { + return array( + Doctrine\ORM\Events::prePersist => 'prePersist', + Doctrine\ORM\Events::preUpdate => 'prePersist', + Doctrine\ORM\Events::preRemove => 'preRemove', + Doctrine\ORM\Events::postFlush => 'postFlush', + ); + } + + + + public function prePersist(LifecycleEventArgs $oArgs) + { + $oEntity = $oArgs->getEntity(); + if ($oEntity instanceof Searchable) { + $this->sm->persist($oEntity); + } + } + + + + public function preRemove(LifecycleEventArgs $oArgs) + { + $oEntity = $oArgs->getEntity(); + if ($oEntity instanceof Searchable) { + $this->sm->remove(clone $oEntity); + } + } + + + + public function postFlush() + { + $this->sm->flush(); + } + +} diff --git a/lib/Doctrine/Search/UnitOfWork.php b/lib/Doctrine/Search/UnitOfWork.php index 0167ebe..f405f46 100644 --- a/lib/Doctrine/Search/UnitOfWork.php +++ b/lib/Doctrine/Search/UnitOfWork.php @@ -19,12 +19,14 @@ namespace Doctrine\Search; -use Doctrine\Search\SearchManager; -use Doctrine\Search\Exception\DoctrineSearchException; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Util\ClassUtils; use Doctrine\Search\Mapping\ClassMetadata; +use Elastica\Document; use Traversable; + + class UnitOfWork { /** @@ -79,7 +81,8 @@ public function persist($entity) } $oid = spl_object_hash($entity); - $this->scheduledForPersist[$oid] = $entity; + $class = ClassUtils::getRealClass(get_class($entity)); + $this->scheduledForPersist[$class][$oid] = $entity; if ($this->evm->hasListeners(Events::postPersist)) { $this->evm->dispatchEvent(Events::postPersist, new Event\LifecycleEventArgs($entity, $this->sm)); @@ -98,7 +101,9 @@ public function remove($entity) } $oid = spl_object_hash($entity); - $this->scheduledForDelete[$oid] = $entity; + $class = ClassUtils::getRealClass(get_class($entity)); + unset($this->scheduledForPersist[$class][$oid]); + $this->scheduledForDelete[$class][$oid] = $entity; if ($this->evm->hasListeners(Events::postRemove)) { $this->evm->dispatchEvent(Events::postRemove, new Event\LifecycleEventArgs($entity, $this->sm)); @@ -117,7 +122,7 @@ public function clear($entityName = null) $this->scheduledForPersist = $this->updatedIndexes = array(); } else { - //TODO: implement for named entity classes + throw new NotImplementedException; } if ($this->evm->hasListeners(Events::onClear)) { @@ -142,16 +147,20 @@ public function commit($entity = null) $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->sm)); } - //TODO: single/array entity commit handling - $this->commitRemoved(); - $this->commitPersisted(); + if (is_object($entity)) { + throw new NotImplementedException; - //Force refresh of updated indexes - if ($entity === true) { - $client = $this->sm->getClient(); - foreach (array_unique($this->updatedIndexes) as $index) { - $client->refreshIndex($index); - } + } elseif (is_array($entity)) { + throw new NotImplementedException; + + } else { + $this->commitRemoved(); + $this->commitPersisted(); + } + + $client = $this->sm->getClient(); + foreach (array_unique($this->updatedIndexes) as $index) { + $client->refreshIndex($index); } $this->clear(); @@ -171,8 +180,8 @@ private function commitPersisted() foreach ($sortedDocuments as $entityName => $documents) { $classMetadata = $this->sm->getClassMetadata($entityName); - $this->updatedIndexes[] = $classMetadata->index; $client->addDocuments($classMetadata, $documents); + $this->updatedIndexes[] = $classMetadata->getIndexName(); } } @@ -181,13 +190,13 @@ private function commitPersisted() */ private function commitRemoved() { - $documents = $this->sortObjects($this->scheduledForDelete, false); + $sortedDocuments = $this->sortObjects($this->scheduledForDelete, false); $client = $this->sm->getClient(); - foreach ($documents as $entityName => $documents) { + foreach ($sortedDocuments as $entityName => $documents) { $classMetadata = $this->sm->getClassMetadata($entityName); - $this->updatedIndexes[] = $classMetadata->index; $client->removeDocuments($classMetadata, $documents); + $this->updatedIndexes[] = $classMetadata->getIndexName(); } } @@ -195,25 +204,29 @@ private function commitRemoved() * Prepare entities for commit. Entities scheduled for deletion do not need * to be serialized. * - * @param array $objects + * @param array $scheduledObjects * @param boolean $serialize * @throws DoctrineSearchException * @return array */ - private function sortObjects(array $objects, $serialize = true) + private function sortObjects(array $scheduledObjects, $serialize = true) { $documents = array(); $serializer = $this->sm->getSerializer(); - foreach ($objects as $object) { - $document = $serialize ? $serializer->serialize($object) : $object; + foreach ($scheduledObjects as $type => $objects) { + $metadata = $this->sm->getClassMetadata($type); - $id = $object->getId(); - if (!$id) { - throw new DoctrineSearchException('Entity must have an id to be indexed'); - } + foreach ($objects as $object) { + $document = $serialize ? $serializer->serialize($object) : $object; - $documents[get_class($object)][$id] = $document; + if (!$metadata->getIdentifier()) { + throw new DoctrineSearchException('Entity must have an id to be indexed'); + } + + $id = implode('-', (array) $metadata->getIdentifierValues($object)); + $documents[$type][$id] = $document; + } } return $documents; @@ -243,7 +256,8 @@ public function load(ClassMetadata $class, $value, $options = array()) * Load and hydrate a document collection * * @param array $classes - * @param unknown $query + * @param mixed $query + * @return ArrayCollection|Searchable[] */ public function loadCollection(array $classes, $query) { @@ -254,34 +268,55 @@ public function loadCollection(array $classes, $query) /** * Construct an entity collection * - * @param array $classes - * @param Traversable $resultSet + * @param array|ClassMetadata[] $classes + * @param Traversable|Document[] $resultSet + * @return ArrayCollection|Searchable[] */ public function hydrateCollection(array $classes, Traversable $resultSet) { + $map = array(); + foreach ($classes as $class) { + $map[$class->getIndexName()][$class->getTypeName()] = $class; + } + + if ($om = $this->sm->getObjectManager()) { // preload entities by one query + $documentsByType = array(); + foreach ($resultSet as $document) { + /** @var ClassMetadata $class */ + $class = $map[$document->getIndex()][$document->getType()]; + $documentsByType[$class->className][$document->getId()] = $document; + } + + foreach ($documentsByType as $className => $documents) { + $metadata = $this->sm->getClassMetadata($className); + + $repository = $om->getRepository($className); + $repository->findBy([$metadata->getIdentifier() => array_keys($documents)]); + } + } + $collection = new ArrayCollection(); foreach ($resultSet as $document) { - foreach ($classes as $class) { - if ($document->getIndex() == $class->index && $document->getType() == $class->type) { - break; - } - } + /** @var ClassMetadata $class */ + $class = $map[$document->getIndex()][$document->getType()]; $collection[] = $this->hydrateEntity($class, $document); } return $collection; } + + /** * Construct an entity object * * @param ClassMetadata $class - * @param object $document + * @param object|Document $document + * @return Searchable */ public function hydrateEntity(ClassMetadata $class, $document) { - // TODO: add support for different result set types from different clients - // perhaps by wrapping documents in a layer of abstraction + // TODO: add support for different result set types from different clients by implementing Persisters per client $data = $document->getData(); $fields = array_merge( $document->hasFields() ? $document->getFields() : array(), @@ -289,24 +324,25 @@ public function hydrateEntity(ClassMetadata $class, $document) ); foreach ($fields as $name => $value) { - if (isset($class->parameters[$name])) { + if (isset($class->type->parameters[$name])) { $data[$name] = $value; - } else { - foreach ($class->parameters as $param => $mapping) { - if ($mapping->name == $name) { - $data[$param] = $value; - break; - } - } + + } elseif ($key = array_search($name, $class->type->parameters, TRUE)) { + $data[$key] = $value; } } $data[$class->getIdentifier()] = $document->getId(); - $entity = $this->sm->getSerializer()->deserialize($class->className, json_encode($data)); + if ($om = $this->sm->getObjectManager()) { + $entity = $om->find($class->className, $document->getId()); + + } else { + $entity = $this->sm->getSerializer()->deserialize($class->className, json_encode($data)); + } if ($this->evm->hasListeners(Events::postLoad)) { - $this->evm->dispatchEvent(Events::postLoad, new Event\LifecycleEventArgs($entity, $this->sm)); + $this->evm->dispatchEvent(Events::postLoad, new Event\PostLoadEventArgs($entity, $data, $this->sm)); } return $entity; @@ -315,13 +351,35 @@ public function hydrateEntity(ClassMetadata $class, $document) /** * Checks whether an entity is registered in the identity map of this UnitOfWork. * - * @param object $entity - * + * @param Searchable $entity * @return boolean */ public function isInIdentityMap($entity) { $oid = spl_object_hash($entity); - return isset($this->scheduledForPersist[$oid]) || isset($this->scheduledForDelete[$oid]); + $class = ClassUtils::getRealClass(get_class($entity)); + return isset($this->scheduledForPersist[$class][$oid]) + || isset($this->scheduledForDelete[$class][$oid]); + } + + + + /** + * @return array + */ + public function getScheduledForDelete() + { + return $this->scheduledForDelete; + } + + + + /** + * @return array + */ + public function getScheduledForPersist() + { + return $this->scheduledForPersist; } + } diff --git a/lib/Doctrine/Search/ZendLucene/Client.php b/lib/Doctrine/Search/ZendLucene/Client.php deleted file mode 100644 index db3e7e1..0000000 --- a/lib/Doctrine/Search/ZendLucene/Client.php +++ /dev/null @@ -1,98 +0,0 @@ -. - */ - -namespace Doctrine\Search\ZendLucene; - - -use Doctrine\Search\SearchClientInterface; -use Doctrine\Search\Mapping\ClassMetadata; -use Doctrine\Common\Persistence\ObjectManager; - -/** - * SearchManager for ZendLucene-Backend - * - * @author Mike Lohmann - */ -class Client implements SearchClientInterface -{ - - private $config; - - private $connection; - - /* - * @param Connection $conn - * @param Configuration $config - */ - public function __construct(Connection $conn = null, Configuration $config = null) - { - $this->connection = $conn; - $this->config = $config; - } - - public function find($index, $type, $query) - { - - } - - public function createIndex($index, array $data) - { - - } - - public function createType(ClassMetadata $metadata) - { - - } - - public function deleteType(ClassMetadata $metadata) - { - - } - - public function getIndex($index) - { - - } - - public function deleteIndex($index) - { - - } - - public function refreshIndex($index) - { - - } - - public function addDocuments($index, $type, array $documents) - { - - } - - public function removeDocuments($index, $type, array $documents) - { - - } - - public function removeAll($index, $type) - { - - } -} diff --git a/lib/Doctrine/Search/ZendLucene/Configuration.php b/lib/Doctrine/Search/ZendLucene/Configuration.php deleted file mode 100644 index 871db67..0000000 --- a/lib/Doctrine/Search/ZendLucene/Configuration.php +++ /dev/null @@ -1,35 +0,0 @@ -. - */ - -namespace Doctrine\Search\ZendLucene; - -/** - * Configuration handler for ZendLucene-Backend - * - * @author Mike Lohmann - */ -class Configuration -{ - - - public function __construct() - { - - } -} diff --git a/lib/Doctrine/Search/exceptions.php b/lib/Doctrine/Search/exceptions.php new file mode 100644 index 0000000..216a664 --- /dev/null +++ b/lib/Doctrine/Search/exceptions.php @@ -0,0 +1,91 @@ +