├── .gitignore ├── .travis.yml ├── LICENCE ├── README.markdown ├── composer.json ├── composer.lock ├── demo ├── ElasticSearch.php ├── Pagination │ └── PagerfantaAdapter.php ├── README ├── build.php ├── composer.json ├── config.php ├── index.php └── yaml │ ├── Doctrine.Tests.Models.Comments.Comment.yml │ └── Doctrine.Tests.Models.Comments.User.yml ├── lib └── Doctrine │ └── Search │ ├── Configuration.php │ ├── ElasticSearch │ └── Client.php │ ├── EntityRepository.php │ ├── EntityRepositoryCollection.php │ ├── Event │ ├── LifecycleEventArgs.php │ ├── LoadClassMetadataEventArgs.php │ ├── OnClearEventArgs.php │ ├── PostFlushEventArgs.php │ └── PreFlushEventArgs.php │ ├── Events.php │ ├── Exception │ ├── DoctrineSearchException.php │ ├── Driver │ │ ├── ClassIsNotAValidDocumentException.php │ │ └── PropertyDoesNotExistsInMetadataException.php │ ├── NoResultException.php │ └── UnexpectedTypeException.php │ ├── Mapping │ ├── Annotations │ │ ├── DoctrineAnnotations.php │ │ ├── ElasticField.php │ │ ├── ElasticRoot.php │ │ ├── ElasticSearchable.php │ │ ├── Field.php │ │ ├── Id.php │ │ ├── Parameter.php │ │ ├── Searchable.php │ │ └── SolrField.php │ ├── ClassMetadata.php │ ├── ClassMetadataFactory.php │ ├── Driver │ │ ├── AnnotationDriver.php │ │ └── YamlDriver.php │ └── MappingException.php │ ├── Query.php │ ├── SearchClientInterface.php │ ├── SearchManager.php │ ├── Serializer │ ├── AnnotationSerializer.php │ ├── CallbackSerializer.php │ └── JMSSerializer.php │ ├── SerializerInterface.php │ ├── Solr │ ├── Client.php │ ├── Configuration.php │ └── Connection.php │ ├── UnitOfWork.php │ └── Version.php ├── phpunit.xml.dist └── tests ├── Doctrine └── Tests │ ├── Models │ ├── Blog │ │ └── BlogPost.php │ └── Comments │ │ ├── Comment.php │ │ ├── Email.php │ │ └── User.php │ └── Search │ ├── ConfigurationTest.php │ ├── ElasticSearch │ └── ClientTest.php │ ├── Mapping │ ├── ClassMetadataFactoryTest.php │ ├── ClassMetadataTest.php │ └── Driver │ │ ├── AbstractDriverTest.php │ │ ├── AnnotationDriverTest.php │ │ ├── YamlDriverTest.php │ │ └── files │ │ ├── Doctrine.Tests.Search.Mapping.Driver.YamlSuperUser.dcm.yml │ │ └── Doctrine.Tests.Search.Mapping.Driver.YamlUser.dcm.yml │ ├── SearchManagerTest.php │ └── VersionTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | phpunit.xml 2 | vendor/ 3 | demo/vendor/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | - 5.6 8 | 9 | services: 10 | - elasticsearch 11 | 12 | install: 13 | - composer install --no-interaction --prefer-source --dev 14 | 15 | script: vendor/bin/phpunit --coverage-text 16 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Doctrine Project 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Doctrine Search # 2 | 3 | Note: This project is a prototype at the moment. See `demo` folder for practical implementation example. 4 | 5 | __Supported search engines__ 6 | 7 | * [ElasticSearch](http://www.elasticsearch.org/) (functional) 8 | * [Solr](http://lucene.apache.org/solr/) (partial implementation) 9 | 10 | 11 | __Features__ 12 | * SearchManager 13 | * Can be used stand-alone or in a hybrid configuration 14 | * Configurable search manager supports aggregate entity manager 15 | * supports direct API calls through search engine adapters such as Elastica 16 | * transforms returned ID's via batch operation into hydrated objects as required 17 | * Supports event manager listeners for customizable entity handling 18 | * Support for indexing through event listeners via JMS Serializer or simple entity callback. 19 | * Annotations for index and data type creation using ObjectManager::getClassMetadata() as the base structure 20 | 21 | #Usage# 22 | 23 | ## Configuration ## 24 | The search manager connection can be configured as shown in the following example: 25 | ```php 26 | $config = new Doctrine\Search\Configuration(); 27 | $config->setMetadataCacheImpl(new Doctrine\Common\Cache\ArrayCache()); 28 | $config->setEntitySerializer( 29 | new Doctrine\Search\Serializer\JMSSerializer( 30 | JMS\Serializer\SerializationContext::create()->setGroups('search') 31 | ) 32 | ); 33 | 34 | $eventManager = new Doctrine\Search\EventManager(); 35 | $eventManager->addListener($listener); 36 | 37 | $searchManager = new Doctrine\Search\SearchManager( 38 | $config, 39 | new Doctrine\Search\ElasticSearch\Client( 40 | new Elastica\Client(array( 41 | array('host' => 'localhost', 'port' => '9200') 42 | ) 43 | ), 44 | $eventManager 45 | ); 46 | ``` 47 | 48 | ## Mappings ## 49 | Basic entity mappings for index and type generation can be annotated as shown in the following example. Mappings 50 | can be rendered into a format suitable for automatically generating indexes and types using a build script 51 | (advanced setup required). 52 | ```php 53 | tags->slice(0,3); 89 | } 90 | } 91 | ``` 92 | 93 | ## Indexing ## 94 | Documents can be serialized for indexing currently in the following ways. If required an event listener can 95 | be used with your ORM as shown in this example. If an event listener is not needed, entities can be persisted 96 | or removed directly using the search manager. 97 | ```php 98 | getDatabaseConnection('elasticsearch'); 108 | } 109 | 110 | public function postPersist(LifecycleEventArgs $oArgs) { 111 | $oEntity = $oArgs->getEntity(); 112 | if($oEntity instanceof SearchableEntityInterface) { 113 | $this->getSearchManager()->persist($oEntity); 114 | } 115 | } 116 | 117 | public function postRemove(LifecycleEventArgs $oArgs) { 118 | $oEntity = $oArgs->getEntity(); 119 | if($oEntity instanceof SearchableEntityInterface) { 120 | $this->getSearchManager()->remove($oEntity); 121 | } 122 | } 123 | } 124 | ``` 125 | 126 | ### CallbackSerializer ### 127 | This approach simply expects a `toArray()` method on the entity, although this method be configured as required. 128 | The interface suggested in this example can be any interface you desire, as long as your event listener can identify 129 | entities that need to be persisted to the search engine (see above example). 130 | ```php 131 | ... 132 | use Entities\Behaviour\SearchableEntityInterface 133 | 134 | class Post implements SearchableEntityInterface 135 | { 136 | ... 137 | public function toArray() { 138 | return array( 139 | 'id' => $this->id, 140 | 'title' => $this->title, 141 | 'content' => $this->content 142 | ... 143 | ); 144 | } 145 | } 146 | ``` 147 | 148 | ### JMS Serializer ### 149 | You can alternatively use the advanced serialization power of the `JMS Serializer` to automatically handle 150 | serialization for you based on annotations such as those shown in this example. 151 | ```php 152 | ... 153 | use JMS\Serializer\Annotation as JMS; 154 | use Entities\Behaviour\SearchableEntityInterface 155 | 156 | /** 157 | * @ORM\Entity 158 | * @MAP\ElasticSearchable(index="indexname", type="post", source=true) 159 | * @JMS\ExclusionPolicy("all") 160 | */ 161 | class Post implements SearchableEntityInterface 162 | { 163 | ... 164 | /** 165 | * @ORM\Column(type="string") 166 | * @MAP\ElasticField(type="string", includeInAll=true, boost=5.0) 167 | * @JMS\Expose 168 | * @JMS\Groups({"public", "search"}) 169 | */ 170 | private $title; 171 | 172 | /** 173 | * @ORM\Column(type="text") 174 | * @MAP\ElasticField(type="string", includeInAll=true) 175 | * @JMS\Expose 176 | * @JMS\Groups({"public", "search"}) 177 | */ 178 | private $content; 179 | ... 180 | } 181 | ``` 182 | 183 | ### AnnotationSerializer ### 184 | Not yet available. 185 | 186 | 187 | ## Queries ## 188 | Queries can be executed through the search manager as shown below. Use of the result cache refers to using the 189 | cache for the hydration query. Search engine specific adapter interfaces are exposed magically so as in this 190 | example, `Elastica\Query::addSort` method is amalgamated with `Doctrine\Search\Query` methods. Thus any complexity 191 | of query supported by the search engine client library is supported. 192 | ```php 193 | $hydrationQuery = $entityManager->createQueryBuilder() 194 | ->select(array('p', 'field(p.id, :ids) as HIDDEN field')) 195 | ->from('Entities\Post', 'p') 196 | ->where('p.id IN (:ids)') 197 | ->orderBy('field') 198 | ->getQuery(); 199 | 200 | $query = $searchManager->createQuery() 201 | ->from('Entities\Post') 202 | ->searchWith(new Elastica\Query()) 203 | ->hydrateWith($hydrationQuery) 204 | ->addSort('_score') 205 | ->setFrom(0) 206 | ->setLimit(10) 207 | ->getResult(); 208 | ``` 209 | 210 | Simple Repository ID queries and `Term` search can by done using the following technique. Deserialization is done 211 | independently of `Doctrine\ORM` but the same models are hydrated according to the registered `SerializerInterface`. 212 | ```php 213 | $entity = $searchManager->getRepository('Entities\Post')->find($id); 214 | $entity = $searchManager->getRepository('Entities\Post')->findOneBy(array($key => $term)); 215 | ``` 216 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doctrine/search", 3 | "description": "ElasticSearch and Solr entity mapping library", 4 | "keywords": ["elasticsearch", "solr", "lucene", "search", "odm", "orm"], 5 | "homepage": "https://github.com/doctrine/search", 6 | "type": "library", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Doctrine Search Community", 11 | "homepage": "https://github.com/doctrine/search/graphs/contributors" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.3.2", 16 | "doctrine/common": ">=2.3.0" 17 | }, 18 | "suggest": { 19 | "ruflin/Elastica": "Elastica version correlating to your ElasticSearch installation is required.", 20 | "symfony/yaml": "If you want to use YAML Metadata Mapping Driver" 21 | }, 22 | "require-dev": { 23 | "doctrine/orm": "~2.4", 24 | "phpunit/phpunit": "~4.2" 25 | }, 26 | "autoload": { 27 | "psr-0": { 28 | "Doctrine\\Search": "lib/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-0": { 33 | "Doctrine\\Tests\\Search": "tests/", 34 | "Doctrine\\Tests\\Models": "tests/" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /demo/ElasticSearch.php: -------------------------------------------------------------------------------- 1 | newDefaultAnnotationDriver(array($entities['path'])); 28 | $config->setMetadataDriverImpl($md); 29 | $config->setMetadataCacheImpl(new ArrayCache()); 30 | 31 | //Set and configure preferred serializer for persistence 32 | $serializer = SerializerBuilder::create() 33 | ->addMetadataDir(__DIR__.'/yaml') 34 | ->setPropertyNamingStrategy(new SerializedNameAnnotationStrategy(new IdenticalPropertyNamingStrategy())) 35 | ->addDefaultHandlers() 36 | ->build(); 37 | 38 | //If using serialization groups you can sepcify the names here 39 | $config->setEntitySerializer(new JMSSerializer( 40 | SerializationContext::create()->setGroups(array('Default')), 41 | $serializer 42 | )); 43 | 44 | //Add event listeners here 45 | $eventManager = new EventManager(); 46 | //$eventManager->addEventListener('prePersist', $listener); 47 | 48 | //Create the client 49 | $client = new Client(array('connections' => Config::getServers())); 50 | 51 | //Get the search manager 52 | return new SearchManager( 53 | $config, 54 | new ElasticaAdapter($client), 55 | $eventManager 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /demo/Pagination/PagerfantaAdapter.php: -------------------------------------------------------------------------------- 1 | query = $query; 14 | } 15 | 16 | public function getSlice($iOffset, $iLength) 17 | { 18 | $this->query->setFrom($iOffset); 19 | $this->query->setLimit($iLength); 20 | return $this->query->getResult(); 21 | } 22 | 23 | public function getNbResults() 24 | { 25 | return $this->query->count(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /demo/README: -------------------------------------------------------------------------------- 1 | 1. Make sure you have composer installed 2 | 2. Run `composer update` to install dependencies. Check composer.json for correct version of Elastica which corresponds to your ES installation. 3 | 3. Make sure you have ES running and modify config.php to point at it. 4 | 4. Run `php build.php` from command line to create the index and fixtures 5 | 5. Run `php index.php` from the command line or navigate to in the browser -------------------------------------------------------------------------------- /demo/build.php: -------------------------------------------------------------------------------- 1 | getClient(); 13 | $metadatas = $sm->getMetadataFactory()->getAllMetadata(); 14 | 15 | // Delete indexes 16 | foreach ($metadatas as $metadata) { 17 | if ($client->getIndex($metadata->index)->exists()) { 18 | $client->deleteIndex($metadata->index); 19 | } 20 | } 21 | 22 | // Recreate indexes and types 23 | foreach ($metadatas as $metadata) { 24 | if (!$client->getIndex($metadata->index)->exists()) { 25 | $client->createIndex($metadata->index); 26 | } 27 | $client->createType($metadata); 28 | } 29 | 30 | 31 | //Install fixtures here... can use Doctrine/data-fixtures package with 32 | //special SearchManager adapter if required. 33 | $user1 = new User(); 34 | $user1->setName('Hash'); 35 | $user1->setUsername('mrhash'); 36 | $user1->addEmail(new Email('user1@example.com')); 37 | 38 | $user2 = new User(); 39 | $user2->setName('Timothy Leary'); 40 | $user2->setUsername('timmyl'); 41 | $user2->addEmail(new Email('user2@example.com')); 42 | $user2->addEmail(new Email('user2@test.com')); 43 | $user2->addFriend($user1); 44 | 45 | $comment1 = new Comment($user1, 'comment 1 from user 1'); 46 | $comment2 = new Comment($user2, 'comment 1 from user 2'); 47 | $comment3 = new Comment($user2, 'comment 2 from user 2'); 48 | 49 | $sm->persist(array($user1, $user2, $comment1, $comment2, $comment3)); 50 | $sm->flush(); 51 | -------------------------------------------------------------------------------- /demo/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "repositories":[ 3 | { 4 | "type":"vcs", 5 | "url":"http://github.com/doctrine/search" 6 | } 7 | ], 8 | "require":{ 9 | "jms/serializer":"0.15.0", 10 | "ruflin/elastica":"v1.3.0.0", 11 | "doctrine/search":"dev-master", 12 | "pagerfanta/pagerfanta":"v1.0.1", 13 | "symfony/yaml":"@stable" 14 | }, 15 | "autoload-dev": { 16 | "psr-0": { 17 | "Doctrine\\Tests\\Models": "../tests/" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /demo/config.php: -------------------------------------------------------------------------------- 1 | 'localhost', 'port' => 9200) 12 | ); 13 | } 14 | 15 | //Entities namespace and location 16 | public static function getEntityNamespacePath() 17 | { 18 | return array( 19 | 'namespace' => 'Doctrine\Tests\Models', 20 | 'path' => __DIR__ . '/../tests/Doctrine/Tests/Models' 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demo/index.php: -------------------------------------------------------------------------------- 1 | 'timmyl')); 15 | $users = $sm->getRepository('Doctrine\Tests\Models\Comments\User')->search($query); 16 | 17 | foreach ($users as $user) { 18 | print_r($user); 19 | } 20 | 21 | 22 | 23 | //Execute a single term lookup, modify and persist 24 | echo PHP_EOL."*** Single term lookup, modify and persist ***".PHP_EOL; 25 | $user = $sm->getRepository('Doctrine\Tests\Models\Comments\User')->findOneBy(array('username' => 'mrhash')); 26 | print_r($user); 27 | $user->setName('New name'); 28 | $sm->persist($user); 29 | $sm->flush(); 30 | 31 | 32 | 33 | //Execute a single lookup with no results 34 | echo PHP_EOL."*** Single lookup with no results ***".PHP_EOL; 35 | try { 36 | $user = $sm->find('Doctrine\Tests\Models\Comments\User', 'unknownid'); 37 | } catch (Doctrine\Search\Exception\NoResultException $exception) { 38 | print_r($exception->getMessage()); 39 | echo PHP_EOL; 40 | } 41 | 42 | 43 | 44 | //Search for comments with parent user. Because of the way ES returns 45 | //results, you have to explicitly ask for the _parent or _routing field if required. 46 | //On single document query e.g. find() the _parent field is returned by ES anyway. 47 | echo PHP_EOL."*** Comments with parent user ***".PHP_EOL; 48 | $query = new Elastica\Query(); 49 | $query->setFilter(new Elastica\Filter\HasParent( 50 | new Elastica\Filter\Term(array('username' => 'mrhash')), 51 | 'users' 52 | )); 53 | $query->setFields(array('_source', '_parent')); 54 | $comments = $sm->getRepository('Doctrine\Tests\Models\Comments\Comment')->search($query); 55 | 56 | foreach ($comments as $comment) { 57 | print_r($comment); 58 | } 59 | 60 | 61 | //Paginated response with Pagerfanta library. In this case the Doctrine\Search\Query 62 | //wrapper provides a mechanism for specifying the query but it should be possible to 63 | //pass an Elastica query directly into a modified pagination adapter. 64 | echo PHP_EOL."*** Pagerfanta paginated results ***".PHP_EOL; 65 | $query = $sm->createQuery() 66 | ->from('Doctrine\Tests\Models\Comments\Comment') 67 | ->searchWith(new Elastica\Query()) 68 | ->setQuery(new Elastica\Query\MatchAll()) 69 | ->setFields(['_source', '_parent']) 70 | ->setHydrationMode(Doctrine\Search\Query::HYDRATE_INTERNAL); 71 | 72 | $pager = new Pagerfanta\Pagerfanta(new PagerfantaAdapter($query)); 73 | $pager->setAllowOutOfRangePages(true); 74 | $pager->setMaxPerPage(1); 75 | $pager->setCurrentPage(2); 76 | $comments = $pager->getCurrentPageResults(); 77 | 78 | foreach ($comments as $comment) { 79 | print_r($comment); 80 | } 81 | 82 | echo "Total comments found by query: ".$pager->getNbResults().PHP_EOL; 83 | -------------------------------------------------------------------------------- /demo/yaml/Doctrine.Tests.Models.Comments.Comment.yml: -------------------------------------------------------------------------------- 1 | Doctrine\Tests\Models\Comments\Comment: 2 | exclusion_policy: none -------------------------------------------------------------------------------- /demo/yaml/Doctrine.Tests.Models.Comments.User.yml: -------------------------------------------------------------------------------- 1 | Doctrine\Tests\Models\Comments\User: 2 | exclusion_policy: none -------------------------------------------------------------------------------- /lib/Doctrine/Search/Configuration.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search; 21 | 22 | use Doctrine\Common\Cache\Cache; 23 | use Doctrine\Common\Cache\ArrayCache; 24 | use Doctrine\Search\Mapping\ClassMetadataFactory; 25 | use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver; 26 | use Doctrine\Search\Serializer\CallbackSerializer; 27 | use Doctrine\Common\Persistence\ObjectManager; 28 | 29 | /** 30 | * Configuration SearchManager 31 | * 32 | * @author Mike Lohmann 33 | */ 34 | class Configuration 35 | { 36 | /** 37 | * @var array 38 | */ 39 | private $attributes; 40 | 41 | /** 42 | * Gets the cache driver implementation that is used for the mapping metadata. 43 | * (Annotation is the default) 44 | * 45 | * @return MappingDriver 46 | */ 47 | public function getMetadataDriverImpl() 48 | { 49 | if (!isset($this->attributes['concreteMetadataDriver'])) { 50 | $this->attributes['concreteMetadataDriver'] = $this->newDefaultAnnotationDriver(); 51 | } 52 | 53 | return $this->attributes['concreteMetadataDriver']; 54 | } 55 | 56 | /** 57 | * Sets the driver that is used to store the class metadata . 58 | * 59 | * @param MappingDriver $concreteDriver 60 | */ 61 | public function setMetadataDriverImpl(MappingDriver $concreteDriver) 62 | { 63 | $this->attributes['concreteMetadataDriver'] = $concreteDriver; 64 | } 65 | 66 | /** 67 | * Sets the cache driver that is used to cache the metadata. 68 | * 69 | * @param Cache $concreteCache 70 | */ 71 | public function setMetadataCacheImpl(Cache $concreteCache) 72 | { 73 | $this->attributes['metadataCacheImpl'] = $concreteCache; 74 | } 75 | 76 | /** 77 | * Returns the cache driver that is used to cache the metadata. 78 | * 79 | * @return Cache 80 | */ 81 | public function getMetadataCacheImpl() 82 | { 83 | return isset($this->attributes['metadataCacheImpl']) 84 | ? $this->attributes['metadataCacheImpl'] 85 | : new ArrayCache(); 86 | } 87 | 88 | /** 89 | * Add a new default annotation driver with a correctly configured annotation reader. 90 | * 91 | * @param array $paths 92 | * 93 | * @return Mapping\Driver\AnnotationDriver 94 | */ 95 | public function newDefaultAnnotationDriver(array $paths = array()) 96 | { 97 | $reader = new \Doctrine\Common\Annotations\AnnotationReader(); 98 | 99 | return new \Doctrine\Search\Mapping\Driver\AnnotationDriver($reader, $paths); 100 | } 101 | 102 | /** 103 | * Set the class metadata factory class name. 104 | * 105 | * @param string $cmfName 106 | */ 107 | public function setClassMetadataFactoryName($cmfName) 108 | { 109 | $this->attributes['classMetadataFactoryName'] = $cmfName; 110 | } 111 | 112 | /** 113 | * Gets the class metadata factory class name. 114 | * 115 | * @return string 116 | */ 117 | public function getClassMetadataFactoryName() 118 | { 119 | if (!isset($this->attributes['classMetadataFactoryName'])) { 120 | $this->attributes['classMetadataFactoryName'] = 'Doctrine\Search\Mapping\ClassMetadataFactory'; 121 | } 122 | 123 | return $this->attributes['classMetadataFactoryName']; 124 | } 125 | 126 | /** 127 | * Gets the class metadata factory. 128 | * 129 | * @return ClassMetadataFactory 130 | */ 131 | public function getClassMetadataFactory() 132 | { 133 | if (!isset($this->attributes['classMetadataFactory'])) { 134 | $classMetaDataFactoryName = $this->getClassMetadataFactoryName(); 135 | $this->attributes['classMetadataFactory'] = new $classMetaDataFactoryName; 136 | } 137 | return $this->attributes['classMetadataFactory']; 138 | } 139 | 140 | /** 141 | * Sets an entity serializer 142 | * 143 | * @param SerializerInterface $serializer 144 | */ 145 | public function setEntitySerializer(SerializerInterface $serializer) 146 | { 147 | $this->attributes['serializer'] = $serializer; 148 | } 149 | 150 | /** 151 | * Gets the entity serializer or provides a default if not set 152 | * 153 | * @return SerializerInterface 154 | */ 155 | public function getEntitySerializer() 156 | { 157 | if (isset($this->attributes['serializer'])) { 158 | return $this->attributes['serializer']; 159 | } 160 | return new CallbackSerializer(); 161 | } 162 | 163 | /** 164 | * @param ObjectManager $entityManager 165 | */ 166 | public function setEntityManager(ObjectManager $entityManager) 167 | { 168 | $this->attributes['entityManager'] = $entityManager; 169 | } 170 | 171 | /** 172 | * @return ObjectManager 173 | */ 174 | public function getEntityManager() 175 | { 176 | if (isset($this->attributes['entityManager'])) { 177 | return $this->attributes['entityManager']; 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/ElasticSearch/Client.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\ElasticSearch; 21 | 22 | use Doctrine\Search\SearchClientInterface; 23 | use Doctrine\Search\Mapping\ClassMetadata; 24 | use Doctrine\Search\Exception\NoResultException; 25 | use Elastica\Client as ElasticaClient; 26 | use Elastica\Type\Mapping; 27 | use Elastica\Document; 28 | use Elastica\Index; 29 | use Elastica\Query\MatchAll; 30 | use Elastica\Filter\Term; 31 | use Elastica\Exception\NotFoundException; 32 | use Elastica\Search; 33 | use Doctrine\Common\Collections\ArrayCollection; 34 | use Elastica\Query; 35 | use Elastica\Query\Filtered; 36 | 37 | /** 38 | * SearchManager for ElasticSearch-Backend 39 | * 40 | * @author Mike Lohmann 41 | * @author Markus Bachmann 42 | */ 43 | class Client implements SearchClientInterface 44 | { 45 | /** 46 | * @var ElasticaClient 47 | */ 48 | private $client; 49 | 50 | /** 51 | * @param ElasticaClient $client 52 | */ 53 | public function __construct(ElasticaClient $client) 54 | { 55 | $this->client = $client; 56 | } 57 | 58 | /** 59 | * @return ElasticaClient 60 | */ 61 | public function getClient() 62 | { 63 | return $this->client; 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | */ 69 | public function addDocuments(ClassMetadata $class, array $documents) 70 | { 71 | $type = $this->getIndex($class->index)->getType($class->type); 72 | 73 | $parameters = $this->getParameters($class->parameters); 74 | 75 | $bulk = array(); 76 | foreach ($documents as $id => $document) { 77 | $elasticaDoc = new Document($id); 78 | foreach ($parameters as $name => $value) { 79 | if (isset($document[$value])) { 80 | if (method_exists($elasticaDoc, "set{$name}")) { 81 | $elasticaDoc->{"set{$name}"}($document[$value]); 82 | } else { 83 | $elasticaDoc->setParam($name, $document[$value]); 84 | } 85 | unset($document[$value]); 86 | } 87 | } 88 | $elasticaDoc->setData($document); 89 | $bulk[] = $elasticaDoc; 90 | } 91 | 92 | if (count($bulk) > 1) { 93 | $type->addDocuments($bulk); 94 | } else { 95 | $type->addDocument($bulk[0]); 96 | } 97 | } 98 | 99 | /** 100 | * {@inheritDoc} 101 | */ 102 | public function removeDocuments(ClassMetadata $class, array $documents) 103 | { 104 | $type = $this->getIndex($class->index)->getType($class->type); 105 | $type->deleteIds(array_keys($documents)); 106 | } 107 | 108 | /** 109 | * {@inheritDoc} 110 | */ 111 | public function removeAll(ClassMetadata $class, $query = null) 112 | { 113 | $type = $this->getIndex($class->index)->getType($class->type); 114 | $query = $query ?: new MatchAll(); 115 | $type->deleteByQuery($query); 116 | } 117 | 118 | /** 119 | * {@inheritDoc} 120 | */ 121 | public function find(ClassMetadata $class, $id, $options = array()) 122 | { 123 | try { 124 | $type = $this->getIndex($class->index)->getType($class->type); 125 | $document = $type->getDocument($id, $options); 126 | } catch (NotFoundException $ex) { 127 | throw new NoResultException(); 128 | } 129 | 130 | return $document; 131 | } 132 | 133 | public function findOneBy(ClassMetadata $class, $field, $value) 134 | { 135 | $filter = new Term(array($field => $value)); 136 | 137 | $query = new Query(new Filtered(null, $filter)); 138 | $query->setVersion(true); 139 | $query->setSize(1); 140 | 141 | $results = $this->search($query, array($class)); 142 | 143 | if (!$results->count()) { 144 | throw new NoResultException(); 145 | } 146 | 147 | return $results[0]; 148 | } 149 | 150 | /** 151 | * {@inheritDoc} 152 | */ 153 | public function findAll(array $classes) 154 | { 155 | return $this->buildQuery($classes)->search(); 156 | } 157 | 158 | protected function buildQuery(array $classes) 159 | { 160 | $searchQuery = new Search($this->client); 161 | $searchQuery->setOption(Search::OPTION_VERSION, true); 162 | foreach ($classes as $class) { 163 | if ($class->index) { 164 | $indexObject = $this->getIndex($class->index); 165 | $searchQuery->addIndex($indexObject); 166 | if ($class->type) { 167 | $searchQuery->addType($indexObject->getType($class->type)); 168 | } 169 | } 170 | } 171 | return $searchQuery; 172 | } 173 | 174 | /** 175 | * {@inheritDoc} 176 | */ 177 | public function search($query, array $classes) 178 | { 179 | return $this->buildQuery($classes)->search($query); 180 | } 181 | 182 | /** 183 | * {@inheritDoc} 184 | */ 185 | public function createIndex($name, array $config = array()) 186 | { 187 | $index = $this->getIndex($name); 188 | $index->create($config, true); 189 | return $index; 190 | } 191 | 192 | /** 193 | * {@inheritDoc} 194 | */ 195 | public function getIndex($name) 196 | { 197 | return $this->client->getIndex($name); 198 | } 199 | 200 | /** 201 | * {@inheritDoc} 202 | */ 203 | public function deleteIndex($index) 204 | { 205 | $this->getIndex($index)->delete(); 206 | } 207 | 208 | /** 209 | * {@inheritDoc} 210 | */ 211 | public function refreshIndex($index) 212 | { 213 | $this->getIndex($index)->refresh(); 214 | } 215 | 216 | /** 217 | * {@inheritDoc} 218 | */ 219 | public function createType(ClassMetadata $metadata) 220 | { 221 | $type = $this->getIndex($metadata->index)->getType($metadata->type); 222 | $properties = $this->getMapping($metadata->fieldMappings); 223 | $rootProperties = $this->getRootMapping($metadata->rootMappings); 224 | 225 | $mapping = new Mapping($type, $properties); 226 | $mapping->disableSource($metadata->source); 227 | if (isset($metadata->boost)) { 228 | $mapping->setParam('_boost', array('name' => '_boost', 'null_value' => $metadata->boost)); 229 | } 230 | if (isset($metadata->parent)) { 231 | $mapping->setParent($metadata->parent); 232 | } 233 | foreach ($rootProperties as $key => $value) { 234 | $mapping->setParam($key, $value); 235 | } 236 | 237 | $mapping->send(); 238 | 239 | return $type; 240 | } 241 | 242 | /** 243 | * {@inheritDoc} 244 | */ 245 | public function deleteType(ClassMetadata $metadata) 246 | { 247 | $type = $this->getIndex($metadata->index)->getType($metadata->type); 248 | return $type->delete(); 249 | } 250 | 251 | /** 252 | * Generates property mapping from entity annotations 253 | * 254 | * @param array $mappings 255 | */ 256 | protected function getMapping($mappings) 257 | { 258 | $properties = array(); 259 | 260 | foreach ($mappings as $propertyName => $fieldMapping) { 261 | if (isset($fieldMapping['fieldName'])) { 262 | $propertyName = $fieldMapping['fieldName']; 263 | } 264 | 265 | if (isset($fieldMapping['type'])) { 266 | $properties[$propertyName]['type'] = $fieldMapping['type']; 267 | 268 | if ($fieldMapping['type'] == 'attachment' && isset($fieldMapping['fields'])) { 269 | $callback = function ($field) { 270 | unset($field['type']); 271 | return $field; 272 | }; 273 | $properties[$propertyName]['fields'] = array_map($callback, $this->getMapping($fieldMapping['fields'])); 274 | } 275 | 276 | if ($fieldMapping['type'] == 'multi_field' && isset($fieldMapping['fields'])) { 277 | $properties[$propertyName]['fields'] = $this->getMapping($fieldMapping['fields']); 278 | } 279 | 280 | if (in_array($fieldMapping['type'], array('nested', 'object')) && isset($fieldMapping['properties'])) { 281 | $properties[$propertyName]['properties'] = $this->getMapping($fieldMapping['properties']); 282 | } 283 | } 284 | 285 | if (isset($fieldMapping['path'])) { 286 | $properties[$propertyName]['path'] = $fieldMapping['path']; 287 | } 288 | 289 | if (isset($fieldMapping['includeInAll'])) { 290 | $properties[$propertyName]['include_in_all'] = $fieldMapping['includeInAll']; 291 | } 292 | 293 | if (isset($fieldMapping['nullValue'])) { 294 | $properties[$propertyName]['null_value'] = $fieldMapping['nullValue']; 295 | } 296 | 297 | if (isset($fieldMapping['store'])) { 298 | $properties[$propertyName]['store'] = $fieldMapping['store']; 299 | } 300 | 301 | if (isset($fieldMapping['index'])) { 302 | $properties[$propertyName]['index'] = $fieldMapping['index']; 303 | } 304 | 305 | if (isset($fieldMapping['boost'])) { 306 | $properties[$propertyName]['boost'] = $fieldMapping['boost']; 307 | } 308 | 309 | if (isset($fieldMapping['analyzer'])) { 310 | $properties[$propertyName]['analyzer'] = $fieldMapping['analyzer']; 311 | } 312 | 313 | if (isset($fieldMapping['indexName'])) { 314 | $properties[$propertyName]['index_name'] = $fieldMapping['indexName']; 315 | } 316 | 317 | if (isset($fieldMapping['geohash'])) { 318 | $properties[$propertyName]['geohash'] = $fieldMapping['geohash']; 319 | } 320 | 321 | if (isset($fieldMapping['geohash_precision'])) { 322 | $properties[$propertyName]['geohash_precision'] = $fieldMapping['geohash_precision']; 323 | } 324 | 325 | if (isset($fieldMapping['geohash_prefix'])) { 326 | $properties[$propertyName]['geohash_prefix'] = $fieldMapping['geohash_prefix']; 327 | } 328 | } 329 | 330 | return $properties; 331 | } 332 | 333 | /** 334 | * Generates parameter mapping from entity annotations 335 | * 336 | * @param array $paramMapping 337 | */ 338 | protected function getParameters($paramMapping) 339 | { 340 | $parameters = array(); 341 | foreach ($paramMapping as $propertyName => $mapping) { 342 | $paramName = isset($mapping['fieldName']) ? $mapping['fieldName'] : $propertyName; 343 | $parameters[$paramName] = $propertyName; 344 | } 345 | return $parameters; 346 | } 347 | 348 | /** 349 | * Generates root mapping from entity annotations 350 | * 351 | * @param array $mappings 352 | */ 353 | protected function getRootMapping($mappings) 354 | { 355 | $properties = array(); 356 | 357 | foreach ($mappings as $rootMapping) { 358 | $propertyName = $rootMapping['fieldName']; 359 | $mapping = array(); 360 | 361 | if (isset($rootMapping['value'])) { 362 | $mapping = $rootMapping['value']; 363 | } 364 | 365 | if (isset($rootMapping['match'])) { 366 | $mapping['match'] = $rootMapping['match']; 367 | } 368 | 369 | if (isset($rootMapping['pathMatch'])) { 370 | $mapping['path_match'] = $rootMapping['pathMatch']; 371 | } 372 | 373 | if (isset($rootMapping['unmatch'])) { 374 | $mapping['unmatch'] = $rootMapping['unmatch']; 375 | } 376 | 377 | if (isset($rootMapping['pathUnmatch'])) { 378 | $mapping['path_unmatch'] = $rootMapping['pathUnmatch']; 379 | } 380 | 381 | if (isset($rootMapping['matchPattern'])) { 382 | $mapping['match_pattern'] = $rootMapping['matchPattern']; 383 | } 384 | 385 | if (isset($rootMapping['matchMappingType'])) { 386 | $mapping['match_mapping_type'] = $rootMapping['matchMappingType']; 387 | } 388 | 389 | if (isset($rootMapping['mapping'])) { 390 | $mapping['mapping'] = current($this->getMapping(array($rootMapping['mapping']))); 391 | } 392 | 393 | if (isset($rootMapping['id'])) { 394 | $properties[$propertyName][][$rootMapping['id']] = $mapping; 395 | } else { 396 | $properties[$propertyName] = $mapping; 397 | } 398 | } 399 | 400 | return $properties; 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/EntityRepository.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search; 21 | 22 | use Doctrine\Common\Persistence\ObjectRepository; 23 | use Doctrine\Search\SearchManager; 24 | use Doctrine\Search\Mapping\ClassMetadata; 25 | use Doctrine\Search\Exception\DoctrineSearchException; 26 | 27 | class EntityRepository implements ObjectRepository 28 | { 29 | /** 30 | * @var string 31 | */ 32 | protected $_entityName; 33 | 34 | /** 35 | * @var \Doctrine\Search\Mapping\ClassMetadata 36 | */ 37 | private $_class; 38 | 39 | /** 40 | * @var \Doctrine\Search\SearchManager 41 | */ 42 | private $_sm; 43 | 44 | public function __construct(SearchManager $sm, ClassMetadata $class) 45 | { 46 | $this->_sm = $sm; 47 | $this->_entityName = $class->className; 48 | $this->_class = $class; 49 | } 50 | 51 | /** 52 | * Finds an object by its primary key / identifier. 53 | * 54 | * @param mixed $id The identifier. 55 | * @return object The object. 56 | */ 57 | public function find($id) 58 | { 59 | return $this->_sm->find($this->_entityName, $id); 60 | } 61 | 62 | /** 63 | * Finds all objects in the repository. 64 | * 65 | * @return mixed The objects. 66 | */ 67 | public function findAll() 68 | { 69 | throw new DoctrineSearchException('Not yet implemented.'); 70 | } 71 | 72 | /** 73 | * Finds objects by a set of criteria. 74 | * 75 | * Optionally sorting and limiting details can be passed. An implementation may throw 76 | * an UnexpectedValueException if certain values of the sorting or limiting details are 77 | * not supported. 78 | * 79 | * @throws \UnexpectedValueException 80 | * @param array $criteria 81 | * @param array|null $orderBy 82 | * @param int|null $limit 83 | * @param int|null $offset 84 | * @return mixed The objects. 85 | */ 86 | public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) 87 | { 88 | throw new DoctrineSearchException('Not yet implemented.'); 89 | } 90 | 91 | /** 92 | * Finds a single object by a set of criteria. 93 | * 94 | * @param array $criteria 95 | * @return object The object. 96 | */ 97 | public function findOneBy(array $criteria) 98 | { 99 | $options = array('field' => key($criteria)); 100 | $value = current($criteria); 101 | return $this->_sm->getUnitOfWork()->load($this->_class, $value, $options); 102 | } 103 | 104 | /** 105 | * Execute a direct search query on the associated index and type 106 | * 107 | * @param object $query 108 | */ 109 | public function search($query) 110 | { 111 | return $this->_sm->getUnitOfWork()->loadCollection(array($this->_class), $query); 112 | } 113 | 114 | /** 115 | * Execute a direct delete by query on the associated index and type 116 | * 117 | * @param object $query 118 | */ 119 | public function delete($query) 120 | { 121 | $this->_sm->getClient()->removeAll($this->_class, $query); 122 | } 123 | 124 | /** 125 | * Returns the class name of the object managed by the repository 126 | * 127 | * @return string 128 | */ 129 | public function getClassName() 130 | { 131 | return $this->_entityName; 132 | } 133 | 134 | /** 135 | * Returns the class metadata managed by the repository 136 | * 137 | * @return string 138 | */ 139 | public function getClassMetadata() 140 | { 141 | return $this->_class; 142 | } 143 | 144 | /** 145 | * Returns the search manager 146 | * 147 | * @return \Doctrine\Search\SearchManager 148 | */ 149 | public function getSearchManager() 150 | { 151 | return $this->_sm; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/EntityRepositoryCollection.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search; 21 | 22 | use Doctrine\Common\Persistence\ObjectRepository; 23 | use Doctrine\Search\Exception\DoctrineSearchException; 24 | 25 | class EntityRepositoryCollection implements ObjectRepository 26 | { 27 | /** 28 | * @var array|EntityRepository[] 29 | */ 30 | private $_repositories = array(); 31 | 32 | /** 33 | * @var \Doctrine\Search\SearchManager 34 | */ 35 | private $_sm; 36 | 37 | public function __construct(SearchManager $sm) 38 | { 39 | $this->_sm = $sm; 40 | } 41 | 42 | /** 43 | * Add a repository to the collection 44 | * 45 | * @param EntityRepository $repository 46 | */ 47 | public function addRepository(EntityRepository $repository) 48 | { 49 | $this->_repositories[] = $repository; 50 | } 51 | 52 | /** 53 | * Finds an object by its primary key / identifier. 54 | * 55 | * @param mixed $id The identifier. 56 | * @return object The object. 57 | */ 58 | public function find($id) 59 | { 60 | return $this->_sm->find($this->_entityName, $id); 61 | } 62 | 63 | /** 64 | * Finds all objects in the repository. 65 | * 66 | * @return mixed The objects. 67 | */ 68 | public function findAll() 69 | { 70 | throw new DoctrineSearchException('Not yet implemented.'); 71 | } 72 | 73 | /** 74 | * Finds objects by a set of criteria. 75 | * 76 | * Optionally sorting and limiting details can be passed. An implementation may throw 77 | * an UnexpectedValueException if certain values of the sorting or limiting details are 78 | * not supported. 79 | * 80 | * @throws \UnexpectedValueException 81 | * @param array $criteria 82 | * @param array|null $orderBy 83 | * @param int|null $limit 84 | * @param int|null $offset 85 | * @return mixed The objects. 86 | */ 87 | public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) 88 | { 89 | throw new DoctrineSearchException('Not yet implemented.'); 90 | } 91 | 92 | /** 93 | * Finds a single object by a set of criteria. 94 | * 95 | * @param array $criteria 96 | * @return object The object. 97 | */ 98 | public function findOneBy(array $criteria) 99 | { 100 | throw new DoctrineSearchException('Not yet implemented.'); 101 | } 102 | 103 | /** 104 | * Execute a direct search query on the associated index and type 105 | * 106 | * @param object $query 107 | */ 108 | public function search($query) 109 | { 110 | $classes = $this->getClassMetadata(); 111 | return $this->_sm->getUnitOfWork()->loadCollection($classes, $query); 112 | } 113 | 114 | /** 115 | * Execute a direct delete by query on the associated index and type 116 | * 117 | * @param object $query 118 | */ 119 | public function delete($query) 120 | { 121 | $classes = $this->getClassMetadata(); 122 | foreach ($classes as $class) { 123 | $this->_sm->getClient()->removeAll($class, $query); 124 | } 125 | } 126 | 127 | /** 128 | * Returns the class names of the objects managed by the repository 129 | * 130 | * @return string 131 | */ 132 | public function getClassName() 133 | { 134 | $classNames = array(); 135 | foreach ($this->_repositories as $repository) { 136 | $classNames[] = $repository->getClassName(); 137 | } 138 | return $classNames; 139 | } 140 | 141 | /** 142 | * Returns the class metadata of the objects managed by the repository 143 | * 144 | * @return array 145 | */ 146 | protected function getClassMetadata() 147 | { 148 | $classes = array(); 149 | foreach ($this->_repositories as $repository) { 150 | $classes[] = $repository->getClassMetadata(); 151 | } 152 | return $classes; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Event/LifecycleEventArgs.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Event; 21 | 22 | use Doctrine\Common\EventArgs; 23 | use Doctrine\Search\SearchManager; 24 | 25 | /** 26 | * Lifecycle Events are triggered by the UnitOfWork during lifecycle transitions 27 | * of entities. 28 | * 29 | * @link www.doctrine-project.org 30 | * @since 2.0 31 | * @author Roman Borschel 32 | * @author Benjamin Eberlei 33 | */ 34 | class LifecycleEventArgs extends EventArgs 35 | { 36 | /** 37 | * @var \Doctrine\Search\SearchManager 38 | */ 39 | private $sm; 40 | 41 | /** 42 | * @var object 43 | */ 44 | private $entity; 45 | 46 | /** 47 | * Constructor 48 | * 49 | * @param object $entity 50 | * @param \Doctrine\Search\SearchManager $em 51 | */ 52 | public function __construct($entity, SearchManager $sm) 53 | { 54 | $this->entity = $entity; 55 | $this->sm = $sm; 56 | } 57 | 58 | /** 59 | * Retrieve associated Entity. 60 | * 61 | * @return object 62 | */ 63 | public function getEntity() 64 | { 65 | return $this->entity; 66 | } 67 | 68 | /** 69 | * Retrieve associated SearchManager. 70 | * 71 | * @return \Doctrine\Search\SearchManager 72 | */ 73 | public function getSearchManager() 74 | { 75 | return $this->sm; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Event/LoadClassMetadataEventArgs.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Event; 21 | 22 | use Doctrine\Common\EventArgs; 23 | use Doctrine\Search\SearchManager; 24 | use Doctrine\Search\Mapping\ClassMetadata; 25 | 26 | /** 27 | * Class that holds event arguments for a loadMetadata event. 28 | * 29 | * @link www.doctrine-project.com 30 | * @since 1.0 31 | * @author Mike Lohmann 32 | */ 33 | class LoadClassMetadataEventArgs extends EventArgs 34 | { 35 | /** 36 | * @var \Doctrine\Search\SearchManager 37 | */ 38 | private $sm; 39 | 40 | /** 41 | * @var \Doctrine\Search\Mapping\ClassMetadata 42 | */ 43 | private $classMetadata; 44 | 45 | /** 46 | * Constructor. 47 | * 48 | * @param \Doctrine\Search\Mapping\ClassMetadata $classMetadata 49 | * @param \Doctrine\Search\EntityManager $sm 50 | */ 51 | public function __construct(ClassMetadata $classMetadata, SearchManager $sm) 52 | { 53 | $this->classMetadata = $classMetadata; 54 | $this->sm = $sm; 55 | } 56 | 57 | /** 58 | * Retrieve associated ClassMetadata. 59 | * 60 | * @return \Doctrine\Search\Mapping\ClassMetadata 61 | */ 62 | public function getClassMetadata() 63 | { 64 | return $this->classMetadata; 65 | } 66 | 67 | /** 68 | * Retrieve associated SearchManager. 69 | * 70 | * @return \Doctrine\Search\SearchManager 71 | */ 72 | public function getSearchManager() 73 | { 74 | return $this->sm; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Event/OnClearEventArgs.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Event; 21 | 22 | use Doctrine\Common\EventArgs; 23 | use Doctrine\Search\SearchManager; 24 | 25 | /** 26 | * Provides event arguments for the onClear event. 27 | * 28 | * @link www.doctrine-project.org 29 | * @since 2.0 30 | * @author Roman Borschel 31 | * @author Benjamin Eberlei 32 | */ 33 | class OnClearEventArgs extends EventArgs 34 | { 35 | /** 36 | * @var \Doctrine\Search\SearchManager 37 | */ 38 | private $sm; 39 | 40 | /** 41 | * @var string 42 | */ 43 | private $entityClass; 44 | 45 | /** 46 | * Constructor. 47 | * 48 | * @param \Doctrine\Search\SearchManager $sm 49 | * @param string $entityClass Optional entity class 50 | */ 51 | public function __construct(SearchManager $sm, $entityClass = null) 52 | { 53 | $this->sm = $sm; 54 | $this->entityClass = $entityClass; 55 | } 56 | 57 | /** 58 | * Retrieve associated SearchManager. 59 | * 60 | * @return \Doctrine\Search\SearchManager 61 | */ 62 | public function getSearchManager() 63 | { 64 | return $this->sm; 65 | } 66 | 67 | /** 68 | * Name of the entity class that is cleared, or empty if all are cleared. 69 | * 70 | * @return string 71 | */ 72 | public function getEntityClass() 73 | { 74 | return $this->entityClass; 75 | } 76 | 77 | /** 78 | * Check if event clears all entities. 79 | * 80 | * @return bool 81 | */ 82 | public function clearsAllEntities() 83 | { 84 | return ($this->entityClass === null); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Event/PostFlushEventArgs.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Event; 21 | 22 | use Doctrine\Search\SearchManager; 23 | use Doctrine\Common\EventArgs; 24 | 25 | /** 26 | * Provides event arguments for the postFlush event. 27 | * 28 | * @link www.doctrine-project.org 29 | * @since 2.0 30 | * @author Daniel Freudenberger 31 | */ 32 | class PostFlushEventArgs extends EventArgs 33 | { 34 | /** 35 | * @var \Doctrine\Search\SearchManager 36 | */ 37 | private $sm; 38 | 39 | /** 40 | * Constructor. 41 | * 42 | * @param \Doctrine\Search\SearchManager $sm 43 | */ 44 | public function __construct(SearchManager $sm) 45 | { 46 | $this->sm = $sm; 47 | } 48 | 49 | /** 50 | * Retrieve associated SearchManager. 51 | * 52 | * @return \Doctrine\Search\SearchManager 53 | */ 54 | public function getSearchManager() 55 | { 56 | return $this->sm; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Event/PreFlushEventArgs.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Event; 21 | 22 | use Doctrine\Search\SearchManager; 23 | use Doctrine\Common\EventArgs; 24 | 25 | /** 26 | * Provides event arguments for the preFlush event. 27 | * 28 | * @link www.doctrine-project.com 29 | * @since 2.0 30 | * @author Roman Borschel 31 | * @author Benjamin Eberlei 32 | */ 33 | class PreFlushEventArgs extends EventArgs 34 | { 35 | /** 36 | * @var \Doctrine\Search\SearchManager 37 | */ 38 | private $sm; 39 | 40 | /** 41 | * Constructor. 42 | * 43 | * @param \Doctrine\Search\SearchManager $sm 44 | */ 45 | public function __construct(SearchManager $sm) 46 | { 47 | $this->sm = $sm; 48 | } 49 | 50 | /** 51 | * Retrieve associated SearchManager. 52 | * 53 | * @return \Doctrine\Search\SearchManager 54 | */ 55 | public function getSearchManager() 56 | { 57 | return $this->sm; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Events.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search; 21 | 22 | /** 23 | * Container for all ORM events. 24 | * 25 | * This class cannot be instantiated. 26 | * 27 | * @author Roman Borschel 28 | * @since 2.0 29 | */ 30 | final class Events 31 | { 32 | private function __construct() 33 | { 34 | } 35 | 36 | /** 37 | * The preRemove event occurs for a given entity before the respective 38 | * EntityManager remove operation for that entity is executed. 39 | * 40 | * This is an entity lifecycle event. 41 | * 42 | * @var string 43 | */ 44 | const preRemove = 'preRemove'; 45 | /** 46 | * The postRemove event occurs for an entity after the entity has 47 | * been deleted. It will be invoked after the database delete operations. 48 | * 49 | * This is an entity lifecycle event. 50 | * 51 | * @var string 52 | */ 53 | const postRemove = 'postRemove'; 54 | /** 55 | * The prePersist event occurs for a given entity before the respective 56 | * EntityManager persist operation for that entity is executed. 57 | * 58 | * This is an entity lifecycle event. 59 | * 60 | * @var string 61 | */ 62 | const prePersist = 'prePersist'; 63 | /** 64 | * The postPersist event occurs for an entity after the entity has 65 | * been made persistent. It will be invoked after the database insert operations. 66 | * Generated primary key values are available in the postPersist event. 67 | * 68 | * This is an entity lifecycle event. 69 | * 70 | * @var string 71 | */ 72 | const postPersist = 'postPersist'; 73 | /** 74 | * The postLoad event occurs for an entity after the entity has been loaded 75 | * into the current EntityManager from the database or after the refresh operation 76 | * has been applied to it. 77 | * 78 | * Note that the postLoad event occurs for an entity before any associations have been 79 | * initialized. Therefore it is not safe to access associations in a postLoad callback 80 | * or event handler. 81 | * 82 | * This is an entity lifecycle event. 83 | * 84 | * @var string 85 | */ 86 | const postLoad = 'postLoad'; 87 | /** 88 | * The loadClassMetadata event occurs after the mapping metadata for a class 89 | * has been loaded from a mapping source (annotations/xml/yaml). 90 | * 91 | * @var string 92 | */ 93 | const loadClassMetadata = 'loadClassMetadata'; 94 | /** 95 | * The preFlush event occurs when the EntityManager#flush() operation is invoked, 96 | * but before any changes to managed entites have been calculated. This event is 97 | * always raised right after EntityManager#flush() call. 98 | */ 99 | const preFlush = 'preFlush'; 100 | /** 101 | * The postFlush event occurs when the EntityManager#flush() operation is invoked and 102 | * after all actual database operations are executed successfully. The event is only raised if there is 103 | * actually something to do for the underlying UnitOfWork. If nothing needs to be done, 104 | * the postFlush event is not raised. The event won't be raised if an error occurs during the 105 | * flush operation. 106 | * 107 | * @var string 108 | */ 109 | const postFlush = 'postFlush'; 110 | 111 | /** 112 | * The onClear event occurs when the EntityManager#clear() operation is invoked, 113 | * after all references to entities have been removed from the unit of work. 114 | * 115 | * @var string 116 | */ 117 | const onClear = 'onClear'; 118 | } 119 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Exception/DoctrineSearchException.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Exception; 21 | 22 | class DoctrineSearchException extends \Exception 23 | { 24 | } 25 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Exception/Driver/ClassIsNotAValidDocumentException.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Exception\Driver; 21 | 22 | use Doctrine\Search\Exception\DoctrineSearchException; 23 | 24 | class ClassIsNotAValidDocumentException extends DoctrineSearchException 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Exception/Driver/PropertyDoesNotExistsInMetadataException.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Exception\Driver; 21 | 22 | use Doctrine\Search\Exception\DoctrineSearchException; 23 | 24 | class PropertyDoesNotExistsInMetadataException extends DoctrineSearchException 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Exception/NoResultException.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Exception; 21 | 22 | /** 23 | * Exception thrown when an search query unexpectedly does not return any results. 24 | * 25 | * @author robo 26 | * @since 2.0 27 | */ 28 | class NoResultException extends DoctrineSearchException 29 | { 30 | public function __construct($message = null) 31 | { 32 | parent::__construct($message ?: 'No result was found for query although at least one row was expected.'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Exception/UnexpectedTypeException.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Exception; 21 | 22 | class UnexpectedTypeException extends DoctrineSearchException 23 | { 24 | 25 | public function __construct($value, $expected) 26 | { 27 | parent::__construct( 28 | sprintf( 29 | 'Expected argument of type "%s", "%s" given', 30 | $expected, 31 | (is_object($value) ? get_class($value) : gettype($value)) 32 | ) 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Mapping/Annotations/DoctrineAnnotations.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | require_once __DIR__ . '/ElasticField.php'; 20 | require_once __DIR__ . '/ElasticRoot.php'; 21 | require_once __DIR__ . '/ElasticSearchable.php'; 22 | require_once __DIR__ . '/Field.php'; 23 | require_once __DIR__ . '/Id.php'; 24 | require_once __DIR__ . '/Parameter.php'; 25 | require_once __DIR__ . '/Searchable.php'; 26 | require_once __DIR__ . '/SolrField.php'; 27 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Mapping/Annotations/ElasticField.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Mapping\Annotations; 21 | 22 | use Doctrine\Common\Annotations\Annotation; 23 | 24 | /** 25 | * @Annotation 26 | * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) 27 | */ 28 | final class ElasticField extends Field 29 | { 30 | /** 31 | * @var boolean 32 | */ 33 | public $includeInAll; 34 | 35 | /** 36 | * @var string 37 | */ 38 | public $index; 39 | 40 | /** 41 | * @var array 42 | */ 43 | public $fields; 44 | 45 | /** 46 | * @var array 47 | */ 48 | public $properties; 49 | 50 | /** 51 | * @var string 52 | */ 53 | public $analyzer; 54 | 55 | /** 56 | * @var string 57 | */ 58 | public $path; 59 | 60 | /** 61 | * @var string 62 | */ 63 | public $indexName; 64 | 65 | /** 66 | * @var boolean 67 | */ 68 | public $store; 69 | 70 | /** 71 | * @var mixed 72 | */ 73 | public $nullValue; 74 | 75 | /** 76 | * @var boolean 77 | */ 78 | public $geohash; 79 | 80 | /** 81 | * @var string 82 | */ 83 | public $geohash_precision; 84 | 85 | /** 86 | * @var boolean 87 | */ 88 | public $geohash_prefix; 89 | } 90 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Mapping/Annotations/ElasticRoot.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Mapping\Annotations; 21 | 22 | use Doctrine\Common\Annotations\Annotation; 23 | 24 | /** 25 | * @Annotation 26 | * @Target({"CLASS"}) 27 | */ 28 | final class ElasticRoot extends Annotation 29 | { 30 | /** 31 | * @var string 32 | */ 33 | public $name; 34 | 35 | /** 36 | * @var string 37 | */ 38 | public $id; 39 | 40 | /** 41 | * @var string 42 | */ 43 | public $match; 44 | 45 | /** 46 | * @var string 47 | */ 48 | public $unmatch; 49 | 50 | /** 51 | * @var string 52 | */ 53 | public $pathMatch; 54 | 55 | /** 56 | * @var string 57 | */ 58 | public $pathUnmatch; 59 | 60 | /** 61 | * @var string 62 | */ 63 | public $matchPattern; 64 | 65 | /** 66 | * @var string 67 | */ 68 | public $matchMappingType; 69 | 70 | /** 71 | * @var mixed 72 | */ 73 | public $value; 74 | 75 | /** 76 | * @var array 77 | */ 78 | public $mapping; 79 | } 80 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Mapping/Annotations/ElasticSearchable.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Mapping\Annotations; 21 | 22 | use Doctrine\Common\Annotations\Annotation; 23 | 24 | /** 25 | * @Annotation 26 | * @Target("CLASS") 27 | */ 28 | final class ElasticSearchable extends Searchable 29 | { 30 | /** 31 | * @var int $numberOfShards 32 | */ 33 | public $numberOfShards; 34 | 35 | /** 36 | * @var int $numnberOfReplicas 37 | */ 38 | public $numberOfReplicas; 39 | 40 | /** 41 | * @var string $parent 42 | */ 43 | public $parent; 44 | 45 | /** 46 | * TTL in milliseconds 47 | * @var int $timeToLive 48 | */ 49 | public $timeToLive; 50 | 51 | /** 52 | * @var float 53 | */ 54 | public $boost; 55 | 56 | /** 57 | * @var boolean 58 | */ 59 | public $source; 60 | } 61 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Mapping/Annotations/Field.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Mapping\Annotations; 21 | 22 | use Doctrine\Common\Annotations\Annotation; 23 | 24 | /** 25 | * @Annotation 26 | * @Target("PROPERTY") 27 | */ 28 | class Field extends Annotation 29 | { 30 | /** 31 | * @var float 32 | */ 33 | public $boost; 34 | 35 | /** 36 | * @var string 37 | */ 38 | public $type; 39 | 40 | /** 41 | * @var string 42 | */ 43 | public $name; 44 | 45 | /** 46 | * @var string 47 | */ 48 | public $fieldName; 49 | } 50 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Mapping/Annotations/Id.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Mapping\Annotations; 21 | 22 | use Doctrine\Common\Annotations\Annotation; 23 | 24 | /** 25 | * @Annotation 26 | * @Target({"PROPERTY"}) 27 | */ 28 | final class Id extends Annotation 29 | { 30 | } 31 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Mapping/Annotations/Parameter.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Mapping\Annotations; 21 | 22 | use Doctrine\Common\Annotations\Annotation; 23 | 24 | /** 25 | * @Annotation 26 | * @Target({"PROPERTY"}) 27 | */ 28 | final class Parameter extends Annotation 29 | { 30 | /** 31 | * @var string 32 | */ 33 | public $parameterName; 34 | 35 | /** 36 | * @var string 37 | */ 38 | public $name; 39 | 40 | /** 41 | * @var string 42 | */ 43 | public $type = 'string'; 44 | } 45 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Mapping/Annotations/Searchable.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Mapping\Annotations; 21 | 22 | use Doctrine\Common\Annotations\Annotation; 23 | 24 | /** 25 | * @Annotation 26 | * @Target("CLASS") 27 | */ 28 | class Searchable extends Annotation 29 | { 30 | /** 31 | * @var string $index 32 | */ 33 | public $index; 34 | 35 | /** 36 | * @var string $type 37 | */ 38 | public $type; 39 | } 40 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Mapping/Annotations/SolrField.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Mapping\Annotations; 21 | 22 | use Doctrine\Common\Annotations\Annotation; 23 | 24 | /** 25 | * @Annotation 26 | * @Target("PROPERTY") 27 | */ 28 | final class SolrField extends Field 29 | { 30 | /* configuration */ 31 | } 32 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Mapping/ClassMetadata.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Mapping; 21 | 22 | use Doctrine\Common\Persistence\Mapping\ClassMetadata as ClassMetadataInterface; 23 | use Doctrine\Search\Mapping\Annotations\ElasticField; 24 | use Doctrine\Search\Mapping\Annotations\ElasticRoot; 25 | use Doctrine\Search\Mapping\MappingException; 26 | 27 | /** 28 | * A ClassMetadata instance holds all the object-document mapping metadata 29 | * of a document and it's references. 30 | * 31 | * Once populated, ClassMetadata instances are usually cached in a serialized form. 32 | * 33 | * IMPORTANT NOTE: 34 | * 35 | * The fields of this class are only public for 2 reasons: 36 | * 1) To allow fast READ access. 37 | * 2) To drastically reduce the size of a serialized instance (public/protected members 38 | * get the whole class name, namespace inclusive, prepended to every property in 39 | * the serialized representation). 40 | * 41 | * @link www.doctrine-project.com 42 | * @since 1.0 43 | * @author Mike Lohmann 44 | */ 45 | class ClassMetadata implements ClassMetadataInterface 46 | { 47 | /** 48 | * @var string 49 | */ 50 | public $index; 51 | 52 | /** 53 | * @var string 54 | */ 55 | public $type; 56 | 57 | /** 58 | * @var int 59 | */ 60 | public $numberOfShards = 1; 61 | 62 | /** 63 | * @var int 64 | */ 65 | public $numberOfReplicas = 0; 66 | 67 | /** 68 | * @var string 69 | */ 70 | public $parent; 71 | 72 | /** 73 | * @var int 74 | */ 75 | public $timeToLive; 76 | 77 | /** 78 | * @var int 79 | */ 80 | public $value; 81 | 82 | /** 83 | * @var boolean 84 | */ 85 | public $source = true; 86 | 87 | /** 88 | * @var float 89 | */ 90 | public $boost; 91 | 92 | /** 93 | * @var string 94 | */ 95 | public $className; 96 | 97 | /** 98 | * The ReflectionProperty instances of the mapped class. 99 | * 100 | * @var array|ElasticField[] 101 | */ 102 | public $fieldMappings = array(); 103 | 104 | /** 105 | * Additional root annotations of the mapped class. 106 | * 107 | * @var array|ElasticRoot[] 108 | */ 109 | public $rootMappings = array(); 110 | 111 | /** 112 | * The ReflectionProperty parameters of the mapped class. 113 | * 114 | * @var array 115 | */ 116 | public $parameters = array(); 117 | 118 | /** 119 | * The ReflectionClass instance of the mapped class. 120 | * 121 | * @var \ReflectionClass 122 | */ 123 | public $reflClass; 124 | 125 | /** 126 | * The ReflectionClass instance of the mapped class. 127 | * 128 | * @var \ReflectionClass 129 | */ 130 | public $reflFields; 131 | 132 | /** 133 | * The field name of the identifier 134 | * 135 | * @var string 136 | */ 137 | public $identifier; 138 | 139 | 140 | public function __construct($documentName) 141 | { 142 | $this->className = $documentName; 143 | $this->reflClass = new \ReflectionClass($documentName); 144 | } 145 | 146 | /** Determines which fields get serialized. 147 | * 148 | * It is only serialized what is necessary for best unserialization performance. 149 | * 150 | * Parts that are also NOT serialized because they can not be properly unserialized: 151 | * - reflClass (ReflectionClass) 152 | * - reflFields (ReflectionProperty array) 153 | * 154 | * @return array The names of all the fields that should be serialized. 155 | */ 156 | public function __sleep() 157 | { 158 | // This metadata is always serialized/cached. 159 | return array( 160 | 'boost', 161 | 'className', 162 | 'fieldMappings', 163 | 'parameters', 164 | 'index', 165 | 'numberOfReplicas', 166 | 'numberOfShards', 167 | 'parent', 168 | 'timeToLive', 169 | 'type', 170 | 'value', 171 | 'identifier', 172 | 'rootMappings' 173 | ); 174 | } 175 | 176 | /** 177 | * Get fully-qualified class name of this persistent class. 178 | * 179 | * @return string 180 | */ 181 | public function getName() 182 | { 183 | return $this->className; 184 | 185 | } 186 | 187 | /** 188 | * Gets the mapped identifier field name. 189 | * 190 | * The returned structure is an array of the identifier field names. 191 | * 192 | * @return array 193 | */ 194 | public function getIdentifier() 195 | { 196 | return $this->identifier; 197 | } 198 | 199 | /** 200 | * INTERNAL: 201 | * Sets the mapped identifier key field of this class. 202 | * Mainly used by the ClassMetadataFactory to assign inherited identifiers. 203 | * 204 | * @param mixed $identifier 205 | */ 206 | public function setIdentifier($identifier) 207 | { 208 | $this->identifier = $identifier; 209 | } 210 | 211 | /** 212 | * Gets the ReflectionClass instance for this mapped class. 213 | * 214 | * @return \ReflectionClass 215 | */ 216 | public function getReflectionClass() 217 | { 218 | return $this->reflClass; 219 | } 220 | 221 | /** 222 | * Checks if the given field name is a mapped identifier for this class. 223 | * 224 | * @param string $fieldName 225 | * @return boolean 226 | */ 227 | public function isIdentifier($fieldName) 228 | { 229 | return $this->identifier === $fieldName; 230 | } 231 | 232 | /** 233 | * Checks if the given field is a mapped property for this class. 234 | * 235 | * @param string $fieldName 236 | * @return boolean 237 | */ 238 | public function hasField($fieldName) 239 | { 240 | return isset($this->fieldMappings[$fieldName]); 241 | } 242 | 243 | /** 244 | * Adds a mapped field to the class. 245 | * 246 | * @param array $mapping The field mapping. 247 | * @throws MappingException 248 | * @return void 249 | */ 250 | public function mapField(array $mapping) 251 | { 252 | if (isset($this->fieldMappings[$mapping['fieldName']])) { 253 | throw MappingException::duplicateFieldMapping($this->className, $mapping['fieldName']); 254 | } 255 | $this->fieldMappings[$mapping['fieldName']] = $mapping; 256 | } 257 | 258 | /** 259 | * Adds a root mapping to the class. 260 | * 261 | * @param array $mapping 262 | */ 263 | public function mapRoot($mapping = array()) 264 | { 265 | $this->rootMappings[] = $mapping; 266 | } 267 | 268 | /** 269 | * Adds a mapped parameter to the class. 270 | * 271 | * @param array $mapping The parameter mapping. 272 | * @throws MappingException 273 | * @return void 274 | */ 275 | public function mapParameter(array $mapping) 276 | { 277 | if (isset($this->fieldMappings[$mapping['parameterName']])) { 278 | throw MappingException::duplicateParameterMapping($this->className, $mapping['parameterName']); 279 | } 280 | $this->parameters[$mapping['parameterName']] = $mapping; 281 | } 282 | 283 | /** 284 | * Checks if the given field is a mapped association for this class. 285 | * 286 | * @param string $fieldName 287 | * @return boolean 288 | */ 289 | public function hasAssociation($fieldName) 290 | { 291 | return false; 292 | } 293 | 294 | /** 295 | * Checks if the given field is a mapped single valued association for this class. 296 | * 297 | * @param string $fieldName 298 | * @return boolean 299 | */ 300 | public function isSingleValuedAssociation($fieldName) 301 | { 302 | return false; 303 | } 304 | 305 | /** 306 | * Checks if the given field is a mapped collection valued association for this class. 307 | * 308 | * @param string $fieldName 309 | * @return boolean 310 | */ 311 | public function isCollectionValuedAssociation($fieldName) 312 | { 313 | return false; 314 | } 315 | 316 | /** 317 | * A numerically indexed list of field names of this persistent class. 318 | * 319 | * This array includes identifier fields if present on this class. 320 | * 321 | * @return array 322 | */ 323 | public function getFieldNames() 324 | { 325 | return array_keys($this->reflFields); 326 | } 327 | 328 | /** 329 | * Currently not necessary but needed by Interface 330 | * 331 | * @return array 332 | */ 333 | public function getAssociationNames() 334 | { 335 | return array(); 336 | } 337 | 338 | /** 339 | * Returns a type name of this field. 340 | * 341 | * This type names can be implementation specific but should at least include the php types: 342 | * integer, string, boolean, float/double, datetime. 343 | * 344 | * @param string $fieldName 345 | * @return string 346 | */ 347 | public function getTypeOfField($fieldName) 348 | { 349 | //@todo: check if $field exists 350 | return $this->fieldMappings[$fieldName]['type']; 351 | } 352 | 353 | /** 354 | * Currently not necessary but needed by Interface 355 | * 356 | * 357 | * @param string $assocName 358 | * @return string 359 | */ 360 | public function getAssociationTargetClass($assocName) 361 | { 362 | return ''; 363 | } 364 | 365 | public function isAssociationInverseSide($assocName) 366 | { 367 | return ''; 368 | } 369 | 370 | public function getAssociationMappedByTargetField($assocName) 371 | { 372 | return ''; 373 | } 374 | 375 | /** 376 | * Return the identifier of this object as an array with field name as key. 377 | * 378 | * Has to return an empty array if no identifier isset. 379 | * 380 | * @param object $object 381 | * @return array 382 | */ 383 | public function getIdentifierValues($object) 384 | { 385 | // TODO: Implement getIdentifierValues() method. 386 | } 387 | 388 | /** 389 | * Returns an array of identifier field names numerically indexed. 390 | * 391 | * @return array 392 | */ 393 | public function getIdentifierFieldNames() 394 | { 395 | // TODO: Implement getIdentifierFieldNames() method. 396 | } 397 | 398 | /** 399 | * Restores some state that can not be serialized/unserialized. 400 | * 401 | * @param \Doctrine\Common\Persistence\Mapping\ReflectionService $reflService 402 | * 403 | * @return void 404 | */ 405 | public function wakeupReflection($reflService) 406 | { 407 | // Restore ReflectionClass and properties 408 | $this->reflClass = $reflService->getClass($this->className); 409 | 410 | foreach ($this->fieldMappings as $field => $mapping) { 411 | $this->reflFields[$field] = $reflService->getAccessibleProperty($this->className, $field); 412 | } 413 | } 414 | 415 | /** 416 | * Initializes a new ClassMetadata instance that will hold the object-relational mapping 417 | * metadata of the class with the given name. 418 | * 419 | * @param \Doctrine\Common\Persistence\Mapping\ReflectionService $reflService The reflection service. 420 | * 421 | * @return void 422 | */ 423 | public function initializeReflection($reflService) 424 | { 425 | $this->reflClass = $reflService->getClass($this->className); 426 | $this->className = $this->reflClass->getName(); // normalize classname 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Mapping/ClassMetadataFactory.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Mapping; 21 | 22 | use Doctrine\Search\SearchManager; 23 | use Doctrine\Search\Configuration; 24 | use Doctrine\Common\Persistence\Mapping\AbstractClassMetadataFactory; 25 | use Doctrine\Common\Persistence\Mapping\ReflectionService; 26 | use Doctrine\Common\Persistence\Mapping\ClassMetadata as ClassMetadataInterface; 27 | use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver; 28 | use Doctrine\Search\Events; 29 | use Doctrine\Search\Event\LoadClassMetadataEventArgs; 30 | 31 | /** 32 | * The ClassMetadataFactory is used to create ClassMetadata objects that contain all the 33 | * metadata mapping informations of a class which describes how a search backend should be configured. 34 | * 35 | * @link www.doctrine-project.com 36 | * @since 1.0 37 | * @author Mike Lohmann 38 | */ 39 | class ClassMetadataFactory extends AbstractClassMetadataFactory 40 | { 41 | /** 42 | * @var SearchManager 43 | */ 44 | private $sm; 45 | 46 | /** 47 | * @var Configuration 48 | */ 49 | private $config; 50 | 51 | /** 52 | * @var MappingDriver 53 | */ 54 | private $driver; 55 | 56 | /** 57 | * @var \Doctrine\Common\EventManager 58 | */ 59 | private $evm; 60 | 61 | /** 62 | * {@inheritDoc} 63 | */ 64 | protected function initialize() 65 | { 66 | $this->driver = $this->config->getMetadataDriverImpl(); 67 | $this->evm = $this->sm->getEventManager(); 68 | $this->initialized = true; 69 | } 70 | 71 | /** 72 | * Sets the SearchManager instance for this class. 73 | * 74 | * @param SearchManager $sm The SearchManager instance 75 | */ 76 | public function setSearchManager(SearchManager $sm) 77 | { 78 | $this->sm = $sm; 79 | } 80 | 81 | /** 82 | * Sets the Configuration instance 83 | * 84 | * @param Configuration $config 85 | */ 86 | public function setConfiguration(Configuration $config) 87 | { 88 | $this->config = $config; 89 | } 90 | 91 | /** 92 | * Get the fully qualified class-name from the namespace alias. 93 | * 94 | * @param string $namespaceAlias 95 | * @param string $simpleClassName 96 | * 97 | * @return string 98 | */ 99 | protected function getFqcnFromAlias($namespaceAlias, $simpleClassName) 100 | { 101 | // TODO: Implement getFqcnFromAlias() method. 102 | } 103 | 104 | /** 105 | * Return the mapping driver implementation. 106 | * 107 | * @return MappingDriver 108 | */ 109 | protected function getDriver() 110 | { 111 | return $this->driver; 112 | } 113 | 114 | /** 115 | * Actually load the metadata from the underlying metadata 116 | * 117 | * @param ClassMetadataInterface|ClassMetadata $class 118 | * @param ClassMetadataInterface|ClassMetadata $parent 119 | * @param bool $rootEntityFound 120 | * 121 | * @return void 122 | */ 123 | protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonSuperclassParents) 124 | { 125 | //Manipulates $classMetadata; 126 | $this->driver->loadMetadataForClass($class->getName(), $class); 127 | 128 | if ($this->evm->hasListeners(Events::loadClassMetadata)) { 129 | $eventArgs = new LoadClassMetadataEventArgs($class, $this->sm); 130 | $this->evm->dispatchEvent(Events::loadClassMetadata, $eventArgs); 131 | } 132 | } 133 | 134 | /** 135 | * Creates a new ClassMetadata instance for the given class name. 136 | * 137 | * @param string $className 138 | * @return ClassMetadataInterface|ClassMetadata 139 | */ 140 | protected function newClassMetadataInstance($className) 141 | { 142 | return new ClassMetadata($className); 143 | } 144 | 145 | /** 146 | * Wakeup reflection after ClassMetadata gets unserialized from cache. 147 | * 148 | * @param ClassMetadataInterface|ClassMetadata $class 149 | * @param ReflectionService $reflService 150 | * 151 | * @return void 152 | */ 153 | protected function wakeupReflection(ClassMetadataInterface $class, ReflectionService $reflService) 154 | { 155 | $class->wakeupReflection($reflService); 156 | } 157 | 158 | /** 159 | * Initialize Reflection after ClassMetadata was constructed. 160 | * 161 | * @param ClassMetadataInterface|ClassMetadata $class 162 | * @param ReflectionService $reflService 163 | * @return void 164 | */ 165 | protected function initializeReflection(ClassMetadataInterface $class, ReflectionService $reflService) 166 | { 167 | $class->initializeReflection($reflService); 168 | } 169 | 170 | /** 171 | * @param ClassMetadataInterface|ClassMetadata $class 172 | * @return bool 173 | */ 174 | protected function isEntity(ClassMetadataInterface $class) 175 | { 176 | 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Mapping/Driver/AnnotationDriver.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Mapping\Driver; 21 | 22 | use Doctrine\Common\Persistence\Mapping\Driver\AnnotationDriver as AbstractAnnotationDriver; 23 | use Doctrine\Common\Persistence\Mapping\ClassMetadata; 24 | use Doctrine\Common\Annotations\AnnotationRegistry; 25 | use Doctrine\Search\Mapping\MappingException; 26 | 27 | /** 28 | * The AnnotationDriver reads the mapping metadata from docblock annotations. 29 | * 30 | * @link www.doctrine-project.org 31 | * @since 1.0 32 | * @author Mike Lohmann 33 | */ 34 | class AnnotationDriver extends AbstractAnnotationDriver 35 | { 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | protected $entityAnnotationClasses = array( 40 | 'Doctrine\Search\Mapping\Annotations\Searchable' => 1, 41 | 'Doctrine\Search\Mapping\Annotations\ElasticSearchable' => 2 42 | ); 43 | 44 | /** 45 | * Registers annotation classes to the common registry. 46 | * 47 | * This method should be called when bootstrapping your application. 48 | */ 49 | public static function registerAnnotationClasses() 50 | { 51 | AnnotationRegistry::registerFile(__DIR__ . '/../Annotations/DoctrineAnnotations.php'); 52 | } 53 | 54 | /** 55 | * @param string $className 56 | * @param ClassMetadata|\Doctrine\Search\Mapping\ClassMetadata $metadata 57 | * 58 | * @throws \ReflectionException 59 | */ 60 | public function loadMetadataForClass($className, ClassMetadata $metadata) 61 | { 62 | $class = $metadata->getReflectionClass(); 63 | 64 | if (!$class) { 65 | $class = new \ReflectionClass((string) $className); 66 | } 67 | 68 | $classAnnotations = $this->reader->getClassAnnotations($class); 69 | 70 | $classMapping = array(); 71 | $validMapping = false; 72 | foreach ($classAnnotations as $annotation) { 73 | switch(get_class($annotation)) { 74 | case 'Doctrine\Search\Mapping\Annotations\ElasticSearchable': 75 | $classMapping = (array) $annotation; 76 | $classMapping['class'] = 'ElasticSearchable'; 77 | $validMapping = true; 78 | break; 79 | case 'Doctrine\Search\Mapping\Annotations\Searchable': 80 | $classMapping = (array) $annotation; 81 | $classMapping['class'] = 'Searchable'; 82 | $validMapping = true; 83 | break; 84 | case 'Doctrine\Search\Mapping\Annotations\ElasticRoot': 85 | $rootMapping = (array) $annotation; 86 | $metadata->mapRoot($this->rootToArray($rootMapping)); 87 | break; 88 | } 89 | } 90 | 91 | if (!$validMapping) { 92 | throw MappingException::classIsNotAValidDocument($className); 93 | } 94 | 95 | $this->annotateClassMetadata($classMapping, $metadata); 96 | 97 | $properties = $class->getProperties(); 98 | foreach ($properties as $property) { 99 | $propertyAnnotations = $this->reader->getPropertyAnnotations($property); 100 | foreach ($propertyAnnotations as $annotation) { 101 | switch (get_class($annotation)) { 102 | case 'Doctrine\Search\Mapping\Annotations\Id': 103 | $metadata->identifier = $property->getName(); 104 | break; 105 | case 'Doctrine\Search\Mapping\Annotations\Parameter': 106 | $mapping = $this->parameterToArray($property->getName(), (array) $annotation); 107 | $metadata->mapParameter($mapping); 108 | break; 109 | case 'Doctrine\Search\Mapping\Annotations\Field': 110 | case 'Doctrine\Search\Mapping\Annotations\ElasticField': 111 | case 'Doctrine\Search\Mapping\Annotations\SolrField': 112 | $mapping = $this->fieldToArray($property->getName(), (array) $annotation); 113 | $metadata->mapField($mapping); 114 | break; 115 | } 116 | } 117 | } 118 | } 119 | 120 | private function annotateClassMetadata($classMapping, $metadata) 121 | { 122 | $className = $classMapping['class']; 123 | switch ($className) { 124 | case 'ElasticSearchable': 125 | if (isset($classMapping['numberOfShards'])) { 126 | $metadata->numberOfShards = $classMapping['numberOfShards']; 127 | } 128 | if (isset($classMapping['numberOfReplicas'])) { 129 | $metadata->numberOfReplicas = $classMapping['numberOfReplicas']; 130 | } 131 | if (isset($classMapping['parent'])) { 132 | $metadata->parent = $classMapping['parent']; 133 | } 134 | if (isset($classMapping['timeToLive'])) { 135 | $metadata->timeToLive = $classMapping['timeToLive']; 136 | } 137 | if (isset($classMapping['boost'])) { 138 | $metadata->boost = $classMapping['boost']; 139 | } 140 | if (isset($classMapping['source'])) { 141 | $metadata->source = $classMapping['source']; 142 | } 143 | // no break 144 | case 'Searchable': 145 | if (isset($classMapping['index'])) { 146 | $metadata->index = $classMapping['index']; 147 | } 148 | if (isset($classMapping['type'])) { 149 | $metadata->type = $classMapping['type']; 150 | } 151 | break; 152 | } 153 | } 154 | 155 | private function fieldToArray($name, $fieldMapping) 156 | { 157 | $mapping = array(); 158 | if (isset($fieldMapping['name'])) { 159 | $mapping['fieldName'] = $fieldMapping['name']; 160 | } else { 161 | $mapping['fieldName'] = $name; 162 | } 163 | 164 | if (isset($fieldMapping['type'])) { 165 | $mapping['type'] = $fieldMapping['type']; 166 | 167 | if ($fieldMapping['type'] == 'multi_field' && isset($fieldMapping['fields'])) { 168 | foreach ($fieldMapping['fields'] as $name => $subFieldMapping) { 169 | $subFieldMapping = (array) $subFieldMapping; 170 | $mapping['fields'][] = $this->fieldToArray($name, $subFieldMapping); 171 | } 172 | } 173 | 174 | if (in_array($fieldMapping['type'], array('nested', 'object')) && isset($fieldMapping['properties'])) { 175 | foreach ($fieldMapping['properties'] as $name => $subFieldMapping) { 176 | $subFieldMapping = (array) $subFieldMapping; 177 | $mapping['properties'][] = $this->fieldToArray($name, $subFieldMapping); 178 | } 179 | } 180 | } 181 | if (isset($fieldMapping['boost'])) { 182 | $mapping['boost'] = $fieldMapping['boost']; 183 | } 184 | if (isset($fieldMapping['includeInAll'])) { 185 | $mapping['includeInAll'] = (bool) $fieldMapping['includeInAll']; 186 | } 187 | if (isset($fieldMapping['index'])) { 188 | $mapping['index'] = $fieldMapping['index']; 189 | } 190 | if (isset($fieldMapping['analyzer'])) { 191 | $mapping['analyzer'] = $fieldMapping['analyzer']; 192 | } 193 | if (isset($fieldMapping['path'])) { 194 | $mapping['path'] = $fieldMapping['path']; 195 | } 196 | if (isset($fieldMapping['indexName'])) { 197 | $mapping['indexName'] = $fieldMapping['indexName']; 198 | } 199 | if (isset($fieldMapping['store'])) { 200 | $mapping['store'] = (bool) $fieldMapping['store']; 201 | } 202 | if (isset($fieldMapping['nullValue'])) { 203 | $mapping['nullValue'] = $fieldMapping['nullValue']; 204 | } 205 | 206 | return $mapping; 207 | } 208 | 209 | private function rootToArray($rootMapping) 210 | { 211 | $mapping = array(); 212 | if (isset($rootMapping['name'])) { 213 | $mapping['fieldName'] = $rootMapping['name']; 214 | } 215 | if (isset($rootMapping['id'])) { 216 | $mapping['id'] = $rootMapping['id']; 217 | } 218 | if (isset($rootMapping['match'])) { 219 | $mapping['match'] = $rootMapping['match']; 220 | } 221 | if (isset($rootMapping['unmatch'])) { 222 | $mapping['unmatch'] = $rootMapping['unmatch']; 223 | } 224 | if (isset($rootMapping['pathMatch'])) { 225 | $mapping['pathMatch'] = $rootMapping['pathMatch']; 226 | } 227 | if (isset($rootMapping['pathUnmatch'])) { 228 | $mapping['pathUnmatch'] = $rootMapping['pathUnmatch']; 229 | } 230 | if (isset($rootMapping['matchPattern'])) { 231 | $mapping['matchPattern'] = $rootMapping['matchPattern']; 232 | } 233 | if (isset($rootMapping['matchMappingType'])) { 234 | $mapping['matchMappingType'] = $rootMapping['matchMappingType']; 235 | } 236 | if (isset($rootMapping['value'])) { 237 | $mapping['value'] = $rootMapping['value']; 238 | } 239 | if (isset($rootMapping['mapping'])) { 240 | $subFieldMapping = (array) $rootMapping['mapping']; 241 | $field = $this->fieldToArray(null, $subFieldMapping); 242 | unset($field['fieldName']); 243 | $mapping['mapping'] = $field; 244 | } 245 | 246 | return $mapping; 247 | } 248 | 249 | private function parameterToArray($name, $parameterMapping) 250 | { 251 | $mapping = array(); 252 | if (isset($parameterMapping['name'])) { 253 | $mapping['parameterName'] = $parameterMapping['name']; 254 | } else { 255 | $mapping['parameterName'] = $name; 256 | } 257 | 258 | if (isset($parameterMapping['type'])) { 259 | $mapping['type'] = $parameterMapping['type']; 260 | } 261 | 262 | return $mapping; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Mapping/Driver/YamlDriver.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Mapping\Driver; 21 | 22 | use Doctrine\Common\Persistence\Mapping\ClassMetadata; 23 | use Doctrine\Common\Persistence\Mapping\Driver\FileDriver; 24 | use Doctrine\Search\Mapping\MappingException; 25 | use Doctrine\Common\Persistence\Mapping\MappingException as CommonMappingException; 26 | use Symfony\Component\Yaml\Yaml; 27 | 28 | /** 29 | * The YamlDriver reads the mapping metadata from yaml schema files. 30 | * 31 | * @since 1.0 32 | * @author Jonathan H. Wage 33 | * @author Roman Borschel 34 | */ 35 | class YamlDriver extends FileDriver 36 | { 37 | const DEFAULT_FILE_EXTENSION = '.dcm.yml'; 38 | 39 | /** 40 | * {@inheritDoc} 41 | */ 42 | protected $entityAnnotationClasses = array( 43 | 'Doctrine\Search\Mapping\Annotations\Searchable' => 1, 44 | 'Doctrine\Search\Mapping\Annotations\ElasticSearchable' => 2 45 | ); 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | public function __construct($locator, $fileExtension = self::DEFAULT_FILE_EXTENSION) 51 | { 52 | parent::__construct($locator, $fileExtension); 53 | } 54 | 55 | /** 56 | * {@inheritDoc} 57 | */ 58 | public function loadMetadataForClass($className, ClassMetadata $metadata) 59 | { 60 | /* @var $metadata \Doctrine\Search\Mapping\ClassMetadata */ 61 | $hierarchy = array_merge(array($className), class_parents($className)); 62 | 63 | // Look for mappings in the class heirarchy and merge 64 | $element = array(); 65 | foreach (array_reverse($hierarchy) as $subClassName) { 66 | try { 67 | $element = array_merge($element, $this->getElement($subClassName)); 68 | } catch (CommonMappingException $e) { 69 | } 70 | } 71 | 72 | if (empty($element)) { 73 | throw MappingException::mappingFileNotFound($className); 74 | } 75 | 76 | $this->annotateClassMetadata($element, $metadata); 77 | 78 | // Evaluate root mappings 79 | if (isset($element['root'])) { 80 | foreach ($element['root'] as $rootMapping) { 81 | $metadata->mapRoot($this->rootToArray($rootMapping)); 82 | } 83 | } 84 | 85 | // Evaluate id 86 | if (isset($element['id'])) { 87 | $metadata->identifier = $element['id']; 88 | } 89 | 90 | // Evaluate field mappings 91 | if (isset($element['fields'])) { 92 | foreach ($element['fields'] as $name => $fieldMapping) { 93 | $mapping = $this->fieldToArray($name, $fieldMapping); 94 | $metadata->mapField($mapping); 95 | } 96 | } 97 | 98 | // Evaluate parameter mappings 99 | if (isset($element['parameters'])) { 100 | foreach ($element['parameters'] as $name => $parameterMapping) { 101 | $mapping = $this->parameterToArray($name, $parameterMapping); 102 | $metadata->mapParameter($mapping); 103 | } 104 | } 105 | } 106 | 107 | private function annotateClassMetadata($classMapping, $metadata) 108 | { 109 | $className = $classMapping['class']; 110 | switch ($className) { 111 | case 'ElasticSearchable': 112 | if (isset($classMapping['numberOfShards'])) { 113 | $metadata->numberOfShards = $classMapping['numberOfShards']; 114 | } 115 | if (isset($classMapping['numberOfReplicas'])) { 116 | $metadata->numberOfReplicas = $classMapping['numberOfReplicas']; 117 | } 118 | if (isset($classMapping['parent'])) { 119 | $metadata->parent = $classMapping['parent']; 120 | } 121 | if (isset($classMapping['timeToLive'])) { 122 | $metadata->timeToLive = $classMapping['timeToLive']; 123 | } 124 | if (isset($classMapping['boost'])) { 125 | $metadata->boost = $classMapping['boost']; 126 | } 127 | if (isset($classMapping['source'])) { 128 | $metadata->source = $classMapping['source']; 129 | } 130 | // no break 131 | case 'Searchable': 132 | if (isset($classMapping['index'])) { 133 | $metadata->index = $classMapping['index']; 134 | } 135 | if (isset($classMapping['type'])) { 136 | $metadata->type = $classMapping['type']; 137 | } 138 | break; 139 | default: 140 | throw MappingException::classIsNotAValidDocument($className); 141 | } 142 | } 143 | 144 | private function fieldToArray($name, $fieldMapping) 145 | { 146 | $mapping = array(); 147 | if (isset($fieldMapping['name'])) { 148 | $mapping['fieldName'] = $fieldMapping['name']; 149 | } else { 150 | $mapping['fieldName'] = $name; 151 | } 152 | 153 | if (isset($fieldMapping['type'])) { 154 | $mapping['type'] = $fieldMapping['type']; 155 | 156 | if ($fieldMapping['type'] == 'multi_field' && isset($fieldMapping['fields'])) { 157 | foreach ($fieldMapping['fields'] as $name => $subFieldMapping) { 158 | $subFieldMapping = (array) $subFieldMapping; 159 | $mapping['fields'][] = $this->fieldToArray($name, $subFieldMapping); 160 | } 161 | } 162 | 163 | if (in_array($fieldMapping['type'], array('nested', 'object')) && isset($fieldMapping['properties'])) { 164 | foreach ($fieldMapping['properties'] as $name => $subFieldMapping) { 165 | $subFieldMapping = (array) $subFieldMapping; 166 | $mapping['properties'][] = $this->fieldToArray($name, $subFieldMapping); 167 | } 168 | } 169 | } 170 | if (isset($fieldMapping['boost'])) { 171 | $mapping['boost'] = $fieldMapping['boost']; 172 | } 173 | if (isset($fieldMapping['includeInAll'])) { 174 | $mapping['includeInAll'] = (bool) $fieldMapping['includeInAll']; 175 | } 176 | if (isset($fieldMapping['index'])) { 177 | $mapping['index'] = $fieldMapping['index']; 178 | } 179 | if (isset($fieldMapping['analyzer'])) { 180 | $mapping['analyzer'] = $fieldMapping['analyzer']; 181 | } 182 | if (isset($fieldMapping['path'])) { 183 | $mapping['path'] = $fieldMapping['path']; 184 | } 185 | if (isset($fieldMapping['indexName'])) { 186 | $mapping['indexName'] = $fieldMapping['indexName']; 187 | } 188 | if (isset($fieldMapping['store'])) { 189 | $mapping['store'] = (bool) $fieldMapping['store']; 190 | } 191 | if (isset($fieldMapping['nullValue'])) { 192 | $mapping['nullValue'] = $fieldMapping['nullValue']; 193 | } 194 | if (isset($fieldMapping['geohash'])) { 195 | $mapping['geohash'] = (bool) $fieldMapping['geohash']; 196 | } 197 | if (isset($fieldMapping['geohash_precision'])) { 198 | $mapping['geohash_precision'] = $fieldMapping['geohash_precision']; 199 | } 200 | if (isset($fieldMapping['geohash_prefix'])) { 201 | $mapping['geohash_prefix'] = (bool) $fieldMapping['geohash_prefix']; 202 | } 203 | 204 | return $mapping; 205 | } 206 | 207 | private function rootToArray($rootMapping) 208 | { 209 | $mapping = array(); 210 | if (isset($rootMapping['name'])) { 211 | $mapping['fieldName'] = $rootMapping['name']; 212 | } 213 | if (isset($rootMapping['id'])) { 214 | $mapping['id'] = $rootMapping['id']; 215 | } 216 | if (isset($rootMapping['match'])) { 217 | $mapping['match'] = $rootMapping['match']; 218 | } 219 | if (isset($rootMapping['unmatch'])) { 220 | $mapping['unmatch'] = $rootMapping['unmatch']; 221 | } 222 | if (isset($rootMapping['pathMatch'])) { 223 | $mapping['pathMatch'] = $rootMapping['pathMatch']; 224 | } 225 | if (isset($rootMapping['pathUnmatch'])) { 226 | $mapping['pathUnmatch'] = $rootMapping['pathUnmatch']; 227 | } 228 | if (isset($rootMapping['matchPattern'])) { 229 | $mapping['matchPattern'] = $rootMapping['matchPattern']; 230 | } 231 | if (isset($rootMapping['matchMappingType'])) { 232 | $mapping['matchMappingType'] = $rootMapping['matchMappingType']; 233 | } 234 | if (isset($rootMapping['value'])) { 235 | $mapping['value'] = $rootMapping['value']; 236 | } 237 | if (isset($rootMapping['mapping'])) { 238 | $subFieldMapping = (array) $rootMapping['mapping']; 239 | $field = $this->fieldToArray(null, $subFieldMapping); 240 | unset($field['fieldName']); 241 | $mapping['mapping'] = $field; 242 | } 243 | 244 | return $mapping; 245 | } 246 | 247 | private function parameterToArray($name, $parameterMapping) 248 | { 249 | $mapping = array(); 250 | if (isset($parameterMapping['name'])) { 251 | $mapping['parameterName'] = $parameterMapping['name']; 252 | } else { 253 | $mapping['parameterName'] = $name; 254 | } 255 | 256 | if (isset($parameterMapping['type'])) { 257 | $mapping['type'] = $parameterMapping['type']; 258 | } 259 | 260 | return $mapping; 261 | } 262 | 263 | /** 264 | * {@inheritDoc} 265 | */ 266 | protected function loadMappingFile($file) 267 | { 268 | return Yaml::parse($file); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Mapping/MappingException.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Mapping; 21 | 22 | use Exception; 23 | 24 | /** 25 | * A MappingException indicates that something is wrong with the mapping setup. 26 | */ 27 | class MappingException extends Exception 28 | { 29 | public static function classIsNotAValidDocument($className) 30 | { 31 | return new self(sprintf( 32 | 'Class "%s" is not a valid searchable document.', 33 | $className 34 | )); 35 | } 36 | 37 | public static function duplicateFieldMapping($mapping, $fieldName) 38 | { 39 | return new self('Field "'.$fieldName.'" in "'.$mapping.'" was already declared, but it must be declared only once'); 40 | } 41 | 42 | public static function duplicateParameterMapping($mapping, $parameterName) 43 | { 44 | return new self('Parameter "'.$parameterName.'" in "'.$mapping.'" was already declared, but it must be declared only once'); 45 | } 46 | 47 | /** 48 | * @param string $entityName 49 | * @return MappingException 50 | */ 51 | public static function mappingFileNotFound($entityName) 52 | { 53 | return new self("No class or parent mapping file found for class '$entityName'."); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Query.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search; 21 | 22 | use Doctrine\Search\Exception\DoctrineSearchException; 23 | use Doctrine\Search\Exception\NoResultException; 24 | use Elastica; 25 | 26 | class Query 27 | { 28 | const HYDRATE_BYPASS = -1; 29 | 30 | const HYDRATE_INTERNAL = -2; 31 | 32 | const HYDRATION_PARAMETER = 'ids'; 33 | 34 | /** 35 | * @var SearchManager 36 | */ 37 | protected $sm; 38 | 39 | /** 40 | * @var object 41 | */ 42 | protected $query; 43 | 44 | /** 45 | * @var object 46 | */ 47 | protected $hydrationQuery; 48 | 49 | /** 50 | * @var string 51 | */ 52 | protected $hydrationParameter = self::HYDRATION_PARAMETER; 53 | 54 | /** 55 | * @var array 56 | */ 57 | protected $entityClasses; 58 | 59 | /** 60 | * @var integer 61 | */ 62 | protected $hydrationMode; 63 | 64 | /** 65 | * @var boolean 66 | */ 67 | protected $useResultCache; 68 | 69 | /** 70 | * @var integer 71 | */ 72 | protected $cacheLifetime; 73 | 74 | /** 75 | * @var integer 76 | */ 77 | protected $count; 78 | 79 | /** 80 | * @var array 81 | */ 82 | protected $facets; 83 | 84 | public function __construct(SearchManager $sm) 85 | { 86 | $this->sm = $sm; 87 | } 88 | 89 | /** 90 | * Magic method to pass query building to the underlying query 91 | * object, saving the need to abstract. 92 | * 93 | * @param string $method 94 | * @param array $arguments 95 | */ 96 | public function __call($method, $arguments) 97 | { 98 | if (!$this->query) { 99 | throw new DoctrineSearchException('No client query has been provided using Query#searchWith().'); 100 | } 101 | 102 | call_user_func_array(array($this->query, $method), $arguments); 103 | return $this; 104 | } 105 | 106 | /** 107 | * Specifies the searchable entity class to search against. 108 | * 109 | * @param mixed $entityClasses 110 | */ 111 | public function from($entityClasses) 112 | { 113 | $this->entityClasses = (array)$entityClasses; 114 | return $this; 115 | } 116 | 117 | /** 118 | * Add a searchable entity class to search against. 119 | * 120 | * @param string $entityClass 121 | */ 122 | public function addFrom($entityClass) 123 | { 124 | $this->entityClasses[] = $entityClass; 125 | return $this; 126 | } 127 | 128 | /** 129 | * Set the query object to be executed on the search engine 130 | * 131 | * @param mixed $query 132 | */ 133 | public function searchWith($query) 134 | { 135 | $this->query = $query; 136 | return $this; 137 | } 138 | 139 | protected function getSearchManager() 140 | { 141 | return $this->sm; 142 | } 143 | 144 | /** 145 | * Set the hydration mode from the underlying query modes 146 | * or bypass and return search result directly from the client 147 | * 148 | * @param integer $mode 149 | */ 150 | public function setHydrationMode($mode) 151 | { 152 | $this->hydrationMode = $mode; 153 | return $this; 154 | } 155 | 156 | /** 157 | * If hydrating with Doctrine then you can use the result cache 158 | * on the default or provided query 159 | * 160 | * @param boolean $useCache 161 | * @param integer $cacheLifetime 162 | */ 163 | public function useResultCache($useCache, $cacheLifetime = null) 164 | { 165 | $this->useResultCache = $useCache; 166 | $this->cacheLifetime = $cacheLifetime; 167 | return $this; 168 | } 169 | 170 | /** 171 | * Return the total hit count for the given query as provided by 172 | * the search engine. 173 | */ 174 | public function count() 175 | { 176 | return $this->count; 177 | } 178 | 179 | /** 180 | * @return array 181 | */ 182 | public function getFacets() 183 | { 184 | return $this->facets; 185 | } 186 | 187 | /** 188 | * Set a custom Doctrine Query to execute in order to hydrate the search 189 | * engine results into required entities. The assumption is made the the 190 | * search engine result id is correlated to the entity id. An optional 191 | * query parameter override can be specified. 192 | * 193 | * @param object $hydrationQuery 194 | * @param string $parameter 195 | */ 196 | public function hydrateWith($hydrationQuery, $parameter = null) 197 | { 198 | $this->hydrationQuery = $hydrationQuery; 199 | if ($parameter) { 200 | $this->hydrationParameter = $parameter; 201 | } 202 | return $this; 203 | } 204 | 205 | /** 206 | * Return a provided hydration query 207 | * 208 | * @return object 209 | */ 210 | protected function getHydrationQuery() 211 | { 212 | if (!$this->hydrationQuery) { 213 | throw new DoctrineSearchException('A hydration query is required for hydrating results to entities.'); 214 | } 215 | 216 | return $this->hydrationQuery; 217 | } 218 | 219 | /** 220 | * Execute search for single result and hydrate results if required. 221 | * 222 | * @param integer $hydrationMode 223 | * @throws NoResultException 224 | * @return mixed 225 | */ 226 | public function getSingleResult($hydrationMode = null) 227 | { 228 | $this->query->setSize(1); 229 | $results = $this->getResult($hydrationMode); 230 | if (count($results) < 1) { 231 | throw new NoResultException('No results found'); 232 | } 233 | return $results[0]; 234 | } 235 | 236 | /** 237 | * Execute search and hydrate results if required. 238 | * 239 | * @param integer $hydrationMode 240 | * @throws DoctrineSearchException 241 | * @return mixed 242 | */ 243 | public function getResult($hydrationMode = null) 244 | { 245 | if ($hydrationMode) { 246 | $this->hydrationMode = $hydrationMode; 247 | } 248 | 249 | $classes = array(); 250 | foreach (array_unique($this->entityClasses) as $entityClass) { 251 | $classes[] = $this->sm->getClassMetadata($entityClass); 252 | } 253 | 254 | $resultSet = $this->getSearchManager()->getClient()->search($this->query, $classes); 255 | 256 | // TODO: abstraction of support for different result sets 257 | if ($resultSet instanceof Elastica\ResultSet) { 258 | $this->count = $resultSet->getTotalHits(); 259 | $this->facets = $resultSet->getFacets(); 260 | $results = $resultSet->getResults(); 261 | 262 | } else { 263 | $resultClass = get_class($resultSet); 264 | throw new DoctrineSearchException("Unexpected result set class '$resultClass'"); 265 | } 266 | 267 | // Return results depending on hydration mode 268 | if ($this->hydrationMode == self::HYDRATE_BYPASS) { 269 | return $resultSet; 270 | } elseif ($this->hydrationMode == self::HYDRATE_INTERNAL) { 271 | return $this->sm->getUnitOfWork()->hydrateCollection($classes, $resultSet); 272 | } 273 | 274 | // Document ids are used to lookup dbms results 275 | $fn = function ($result) { 276 | return (string) $result->getId(); 277 | }; 278 | $ids = array_map($fn, $results); 279 | 280 | return $this->getHydrationQuery() 281 | ->setParameter($this->hydrationParameter, $ids ?: null) 282 | ->useResultCache($this->useResultCache, $this->cacheLifetime) 283 | ->getResult($this->hydrationMode); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/SearchClientInterface.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search; 21 | 22 | use Doctrine\Search\Mapping\ClassMetadata; 23 | 24 | /** 25 | * Interface for a Doctrine SearchManager class to implement. 26 | * 27 | * @author Mike Lohmann 28 | */ 29 | interface SearchClientInterface 30 | { 31 | /** 32 | * Finds document by id. 33 | * 34 | * @param ClassMetadata $class 35 | * @param mixed $id 36 | * @param array $options 37 | * @throws \Doctrine\Search\Exception\NoResultException 38 | */ 39 | public function find(ClassMetadata $class, $id, $options = array()); 40 | 41 | /** 42 | * Finds document by specified field and value. 43 | * 44 | * @param ClassMetadata $class 45 | * @param string $field 46 | * @param mixed $value 47 | * @throws \Doctrine\Search\Exception\NoResultException 48 | */ 49 | public function findOneBy(ClassMetadata $class, $field, $value); 50 | 51 | /** 52 | * Finds all documents 53 | * 54 | * @param array $classes 55 | */ 56 | public function findAll(array $classes); 57 | 58 | /** 59 | * Finds documents by a specific query. 60 | * 61 | * @param object $query 62 | * @param array $classes 63 | */ 64 | public function search($query, array $classes); 65 | 66 | /** 67 | * Creates a document index 68 | * 69 | * @param string $name The name of the index. 70 | * @param string $config The configuration of the index. 71 | */ 72 | public function createIndex($name, array $config = array()); 73 | 74 | /** 75 | * Gets a document index reference 76 | * 77 | * @param string $name The name of the index. 78 | */ 79 | public function getIndex($name); 80 | 81 | /** 82 | * Deletes an index and its types and documents 83 | * 84 | * @param string $index 85 | */ 86 | public function deleteIndex($index); 87 | 88 | /** 89 | * Refresh the index to make documents available for search 90 | * 91 | * @param string $index 92 | */ 93 | public function refreshIndex($index); 94 | 95 | /** 96 | * Create a document type mapping as defined in the 97 | * class annotations 98 | * 99 | * @param ClassMetadata $metadata 100 | */ 101 | public function createType(ClassMetadata $metadata); 102 | 103 | /** 104 | * Delete a document type 105 | * 106 | * @param ClassMetadata $metadata 107 | */ 108 | public function deleteType(ClassMetadata $metadata); 109 | 110 | /** 111 | * Adds documents of a given type to the specified index 112 | * 113 | * @param ClassMetadata $class 114 | * @param array $documents Indexed by document id 115 | */ 116 | public function addDocuments(ClassMetadata $class, array $documents); 117 | 118 | /** 119 | * Remove documents of a given type from the specified index 120 | * 121 | * @param ClassMetadata $class 122 | * @param array $documents Indexed by document id 123 | */ 124 | public function removeDocuments(ClassMetadata $class, array $documents); 125 | 126 | /** 127 | * Remove all documents of a given type from the specified index 128 | * without deleting the index itself 129 | * 130 | * @param ClassMetadata $class 131 | * @param object $query 132 | */ 133 | public function removeAll(ClassMetadata $class, $query = null); 134 | } 135 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/SearchManager.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search; 21 | 22 | use Doctrine\Common\Persistence\ObjectManager; 23 | use Doctrine\Search\Exception\UnexpectedTypeException; 24 | use Doctrine\Common\EventManager; 25 | 26 | /** 27 | * Interface for a Doctrine SearchManager class to implement. 28 | * 29 | * @author Mike Lohmann 30 | */ 31 | class SearchManager implements ObjectManager 32 | { 33 | /** 34 | * @var SearchClientInterface 35 | */ 36 | private $client; 37 | 38 | /** 39 | * @var Configuration $configuration 40 | */ 41 | private $configuration; 42 | 43 | /** 44 | * @var \Doctrine\Search\Mapping\ClassMetadataFactory 45 | */ 46 | private $metadataFactory; 47 | 48 | /** 49 | * @var SerializerInterface 50 | */ 51 | private $serializer; 52 | 53 | /** 54 | * @var ObjectManager 55 | */ 56 | private $entityManager; 57 | 58 | /** 59 | * The event manager that is the central point of the event system. 60 | * 61 | * @var \Doctrine\Common\EventManager 62 | */ 63 | private $eventManager; 64 | 65 | /** 66 | * The EntityRepository instances. 67 | * 68 | * @var array 69 | */ 70 | private $repositories = array(); 71 | 72 | /** 73 | * The UnitOfWork used to coordinate object-level transactions. 74 | * 75 | * @var \Doctrine\Search\UnitOfWork 76 | */ 77 | private $unitOfWork; 78 | 79 | /** 80 | * Constructor 81 | * 82 | * @param Configuration $config 83 | * @param SearchClientInterface $client 84 | * @param EventManager $eventManager 85 | */ 86 | public function __construct(Configuration $config, SearchClientInterface $client, EventManager $eventManager) 87 | { 88 | $this->configuration = $config; 89 | $this->client = $client; 90 | $this->eventManager = $eventManager; 91 | 92 | $this->metadataFactory = $this->configuration->getClassMetadataFactory(); 93 | $this->metadataFactory->setSearchManager($this); 94 | $this->metadataFactory->setConfiguration($this->configuration); 95 | $this->metadataFactory->setCacheDriver($this->configuration->getMetadataCacheImpl()); 96 | 97 | $this->serializer = $this->configuration->getEntitySerializer(); 98 | $this->entityManager = $this->configuration->getEntityManager(); 99 | 100 | $this->unitOfWork = new UnitOfWork($this); 101 | } 102 | 103 | /** 104 | * Inject a Doctrine 2 object manager 105 | * 106 | * @param ObjectManager $om 107 | */ 108 | public function setEntityManager(ObjectManager $om) 109 | { 110 | $this->entityManager = $om; 111 | } 112 | 113 | /** 114 | * @return ObjectManager|\Doctrine\ORM\EntityManager 115 | */ 116 | public function getEntityManager() 117 | { 118 | return $this->entityManager; 119 | } 120 | 121 | /** 122 | * @return Configuration 123 | */ 124 | public function getConfiguration() 125 | { 126 | return $this->configuration; 127 | } 128 | 129 | /** 130 | * Gets the UnitOfWork used by the SearchManager to coordinate operations. 131 | * 132 | * @return \Doctrine\Search\UnitOfWork 133 | */ 134 | public function getUnitOfWork() 135 | { 136 | return $this->unitOfWork; 137 | } 138 | 139 | /** 140 | * Gets the EventManager used by the SearchManager. 141 | * 142 | * @return \Doctrine\Common\EventManager 143 | */ 144 | public function getEventManager() 145 | { 146 | return $this->eventManager; 147 | } 148 | 149 | /** 150 | * Loads class metadata for the given class 151 | * 152 | * @param string $className 153 | * 154 | * @return \Doctrine\Search\Mapping\ClassMetadata 155 | */ 156 | public function getClassMetadata($className) 157 | { 158 | return $this->metadataFactory->getMetadataFor($className); 159 | } 160 | 161 | /** 162 | * @return Mapping\ClassMetadataFactory 163 | */ 164 | public function getClassMetadataFactory() 165 | { 166 | return $this->metadataFactory; 167 | } 168 | 169 | /** 170 | * @return SearchClientInterface 171 | */ 172 | public function getClient() 173 | { 174 | return $this->client; 175 | } 176 | 177 | /** 178 | * @return SerializerInterface 179 | */ 180 | public function getSerializer() 181 | { 182 | return $this->serializer; 183 | } 184 | 185 | /** 186 | * @return \Doctrine\Search\Mapping\ClassMetadataFactory 187 | */ 188 | public function getMetadataFactory() 189 | { 190 | return $this->metadataFactory; 191 | } 192 | 193 | /** 194 | * {@inheritDoc} 195 | */ 196 | public function find($entityName, $id) 197 | { 198 | $options = array(); 199 | if (is_array($id)) { 200 | if (!isset($id['id'])) { 201 | throw new \InvalidArgumentException('An "id" field is required'); 202 | } 203 | $options = $id; 204 | $id = $options['id']; 205 | unset($options['id']); 206 | } 207 | 208 | $class = $this->getClassMetadata($entityName); 209 | return $this->unitOfWork->load($class, $id, $options); 210 | } 211 | 212 | /** 213 | * Adds the object to the index 214 | * 215 | * @param array|object $objects 216 | * 217 | * @throws UnexpectedTypeException 218 | */ 219 | public function persist($objects) 220 | { 221 | if (!is_array($objects) && !$objects instanceof \Traversable) { 222 | $objects = array($objects); 223 | } 224 | foreach ($objects as $object) { 225 | if (!is_object($object)) { 226 | throw new UnexpectedTypeException($object, 'object'); 227 | } 228 | $this->unitOfWork->persist($object); 229 | } 230 | } 231 | 232 | /** 233 | * Remove the object from the index 234 | * 235 | * @param array|object $objects 236 | * 237 | * @throws UnexpectedTypeException 238 | */ 239 | public function remove($objects) 240 | { 241 | if (!is_array($objects) && !$objects instanceof \Traversable) { 242 | $objects = array($objects); 243 | } 244 | foreach ($objects as $object) { 245 | if (!is_object($object)) { 246 | throw new UnexpectedTypeException($object, 'object'); 247 | } 248 | $this->unitOfWork->remove($object); 249 | } 250 | } 251 | 252 | /** 253 | * Commit all changes 254 | */ 255 | public function flush($object = null) 256 | { 257 | $this->unitOfWork->commit($object); 258 | } 259 | 260 | /** 261 | * Gets the repository for an entity class. 262 | * 263 | * @param string $entityName The name of the entity. 264 | * @return EntityRepository The repository class. 265 | */ 266 | public function getRepository($entityName) 267 | { 268 | if (isset($this->repositories[$entityName])) { 269 | return $this->repositories[$entityName]; 270 | } 271 | 272 | $metadata = $this->getClassMetadata($entityName); 273 | $repository = new EntityRepository($this, $metadata); 274 | $this->repositories[$entityName] = $repository; 275 | 276 | return $repository; 277 | } 278 | 279 | /** 280 | * Gets a collection of entity repositories. 281 | * 282 | * @param array $entityNames The names of the entities. 283 | * @return EntityRepositoryCollection The repository class. 284 | */ 285 | public function getRepositories(array $entityNames) 286 | { 287 | $repositoryCollection = new EntityRepositoryCollection($this); 288 | foreach ($entityNames as $entityName) { 289 | $repositoryCollection->addRepository($this->getRepository($entityName)); 290 | } 291 | return $repositoryCollection; 292 | } 293 | 294 | /** 295 | * Returns a search engine Query wrapper which can be executed 296 | * to retrieve results. 297 | * 298 | * @return Query 299 | */ 300 | public function createQuery() 301 | { 302 | return new Query($this); 303 | } 304 | 305 | public function initializeObject($obj) 306 | { 307 | } 308 | 309 | public function contains($object) 310 | { 311 | } 312 | 313 | public function merge($object) 314 | { 315 | } 316 | 317 | /** 318 | * Clears the SearchManager. All entities that are currently managed 319 | * by this EntityManager become detached. 320 | * 321 | * @param string $objectName if given, only entities of this type will get detached 322 | */ 323 | public function clear($objectName = null) 324 | { 325 | $this->unitOfWork->clear($objectName); 326 | } 327 | 328 | public function detach($object) 329 | { 330 | } 331 | 332 | public function refresh($object) 333 | { 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Serializer/AnnotationSerializer.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Serializer; 21 | 22 | use Doctrine\Search\SerializerInterface; 23 | 24 | class AnnotationSerializer implements SerializerInterface 25 | { 26 | public function serialize($object) 27 | { 28 | } 29 | 30 | public function deserialize($entityName, $data) 31 | { 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Serializer/CallbackSerializer.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Serializer; 21 | 22 | use Doctrine\Search\SerializerInterface; 23 | use ReflectionClass; 24 | 25 | class CallbackSerializer implements SerializerInterface 26 | { 27 | protected $serializerCallback; 28 | protected $deserializerCallback; 29 | 30 | public function __construct($serializerCallback = 'toArray', $deserializerCallback = 'fromArray') 31 | { 32 | $this->serializerCallback = $serializerCallback; 33 | $this->deserializerCallback = $deserializerCallback; 34 | } 35 | 36 | public function serialize($object) 37 | { 38 | return $object->{$this->serializerCallback}(); 39 | } 40 | 41 | public function deserialize($entityName, $data) 42 | { 43 | $reflection = new ReflectionClass($entityName); 44 | $entity = $reflection->newInstanceWithoutConstructor(); 45 | $entity->{$this->deserializerCallback}(json_decode($data, true)); 46 | return $entity; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Serializer/JMSSerializer.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Serializer; 21 | 22 | use Doctrine\Search\SerializerInterface; 23 | use JMS\Serializer\SerializerBuilder; 24 | use JMS\Serializer\SerializationContext; 25 | use JMS\Serializer\Naming\SerializedNameAnnotationStrategy; 26 | use JMS\Serializer\Naming\IdenticalPropertyNamingStrategy; 27 | use JMS\Serializer\Serializer; 28 | 29 | class JMSSerializer implements SerializerInterface 30 | { 31 | protected $serializer; 32 | protected $context; 33 | 34 | public function __construct(SerializationContext $context = null, Serializer $serializer = null) 35 | { 36 | $this->context = $context; 37 | $this->serializer = $serializer ?: SerializerBuilder::create() 38 | ->setPropertyNamingStrategy(new SerializedNameAnnotationStrategy(new IdenticalPropertyNamingStrategy())) 39 | ->addDefaultHandlers() 40 | ->build(); 41 | } 42 | 43 | public function serialize($object) 44 | { 45 | $context = $this->context ? clone $this->context : null; 46 | return json_decode($this->serializer->serialize($object, 'json', $context), true); 47 | } 48 | 49 | public function deserialize($entityName, $data) 50 | { 51 | return $this->serializer->deserialize($data, $entityName, 'json'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/SerializerInterface.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search; 21 | 22 | interface SerializerInterface 23 | { 24 | public function serialize($object); 25 | public function deserialize($entityName, $data); 26 | } 27 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Solr/Client.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Solr; 21 | 22 | use Doctrine\Search\SearchClientInterface; 23 | use Doctrine\Search\Mapping\ClassMetadata; 24 | use Doctrine\Common\Persistence\ObjectManager; 25 | 26 | /** 27 | * SearchManager for Solr-Backend 28 | * 29 | * @author Mike Lohmann 30 | */ 31 | class Client implements SearchClientInterface 32 | { 33 | 34 | private $config; 35 | 36 | private $connection; 37 | 38 | /* 39 | * @param Connection $conn 40 | * @param Configuration $config 41 | */ 42 | public function __construct(Connection $conn = null, Configuration $config = null) 43 | { 44 | $this->connection = $conn; 45 | $this->config = $config; 46 | } 47 | 48 | public function find($index, $type, $query) 49 | { 50 | 51 | } 52 | 53 | public function createIndex($index, array $data) 54 | { 55 | 56 | } 57 | 58 | public function createType(ClassMetadata $metadata) 59 | { 60 | 61 | } 62 | 63 | public function deleteType(ClassMetadata $metadata) 64 | { 65 | 66 | } 67 | 68 | public function getIndex($index) 69 | { 70 | 71 | } 72 | 73 | public function deleteIndex($index) 74 | { 75 | 76 | } 77 | 78 | public function refreshIndex($index) 79 | { 80 | 81 | } 82 | 83 | public function addDocuments($index, $type, array $documents) 84 | { 85 | 86 | } 87 | 88 | public function removeDocuments($index, $type, array $documents) 89 | { 90 | 91 | } 92 | 93 | public function removeAll($index, $type) 94 | { 95 | 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Solr/Configuration.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Solr; 21 | 22 | /** 23 | * Configuration handler for Solr-Backend 24 | * 25 | * @author Mike Lohmann 26 | */ 27 | class Configuration 28 | { 29 | 30 | 31 | public function __construct() 32 | { 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Solr/Connection.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search\Solr; 21 | 22 | use Doctrine\Search\Http\ClientInterface as HttpClientInterface; 23 | 24 | /** 25 | * Connections handler for Solr-Backend 26 | * 27 | * @author Mike Lohmann 28 | */ 29 | class Connection 30 | { 31 | private $host; 32 | 33 | private $port; 34 | 35 | private $path; 36 | 37 | private $httpClient; 38 | 39 | public function __construct(HttpClientInterface $httpClient, $host = null, $port = null, $path = null) 40 | { 41 | $this->host = $host; 42 | $this->port = $port; 43 | $this->path = $path; 44 | $this->httpClient = $httpClient; 45 | } 46 | 47 | public function initialize() 48 | { 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/UnitOfWork.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search; 21 | 22 | use Doctrine\Search\SearchManager; 23 | use Doctrine\Search\Exception\DoctrineSearchException; 24 | use Doctrine\Common\Collections\ArrayCollection; 25 | use Doctrine\Search\Mapping\ClassMetadata; 26 | use Traversable; 27 | 28 | class UnitOfWork 29 | { 30 | /** 31 | * The SearchManager that "owns" this UnitOfWork instance. 32 | * 33 | * @var \Doctrine\Search\SearchManager 34 | */ 35 | private $sm; 36 | 37 | /** 38 | * The EventManager used for dispatching events. 39 | * 40 | * @var \Doctrine\Common\EventManager 41 | */ 42 | private $evm; 43 | 44 | /** 45 | * @var array 46 | */ 47 | private $scheduledForPersist = array(); 48 | 49 | /** 50 | * @var array 51 | */ 52 | private $scheduledForDelete = array(); 53 | 54 | /** 55 | * @var array 56 | */ 57 | private $updatedIndexes = array(); 58 | 59 | /** 60 | * Initializes a new UnitOfWork instance, bound to the given SearchManager. 61 | * 62 | * @param \Doctrine\Search\EntityManager $sm 63 | */ 64 | public function __construct(SearchManager $sm) 65 | { 66 | $this->sm = $sm; 67 | $this->evm = $sm->getEventManager(); 68 | } 69 | 70 | /** 71 | * Persists an entity as part of the current unit of work. 72 | * 73 | * @param object $entity The entity to persist. 74 | */ 75 | public function persist($entity) 76 | { 77 | if ($this->evm->hasListeners(Events::prePersist)) { 78 | $this->evm->dispatchEvent(Events::prePersist, new Event\LifecycleEventArgs($entity, $this->sm)); 79 | } 80 | 81 | $oid = spl_object_hash($entity); 82 | $this->scheduledForPersist[$oid] = $entity; 83 | 84 | if ($this->evm->hasListeners(Events::postPersist)) { 85 | $this->evm->dispatchEvent(Events::postPersist, new Event\LifecycleEventArgs($entity, $this->sm)); 86 | } 87 | } 88 | 89 | /** 90 | * Deletes an entity as part of the current unit of work. 91 | * 92 | * @param object $entity The entity to remove. 93 | */ 94 | public function remove($entity) 95 | { 96 | if ($this->evm->hasListeners(Events::preRemove)) { 97 | $this->evm->dispatchEvent(Events::preRemove, new Event\LifecycleEventArgs($entity, $this->sm)); 98 | } 99 | 100 | $oid = spl_object_hash($entity); 101 | $this->scheduledForDelete[$oid] = $entity; 102 | 103 | if ($this->evm->hasListeners(Events::postRemove)) { 104 | $this->evm->dispatchEvent(Events::postRemove, new Event\LifecycleEventArgs($entity, $this->sm)); 105 | } 106 | } 107 | 108 | /** 109 | * Clears the UnitOfWork. 110 | * 111 | * @param string $entityName if given, only entities of this type will get detached 112 | */ 113 | public function clear($entityName = null) 114 | { 115 | if ($entityName === null) { 116 | $this->scheduledForDelete = 117 | $this->scheduledForPersist = 118 | $this->updatedIndexes = array(); 119 | } else { 120 | //TODO: implement for named entity classes 121 | } 122 | 123 | if ($this->evm->hasListeners(Events::onClear)) { 124 | $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->sm, $entityName)); 125 | } 126 | } 127 | 128 | /** 129 | * Commits the UnitOfWork, executing all operations that have been postponed 130 | * up to this point. 131 | * 132 | * The operations are executed in the following order: 133 | * 134 | * 1) All entity inserts 135 | * 2) All entity deletions 136 | * 137 | * @param null|object|array $entity 138 | */ 139 | public function commit($entity = null) 140 | { 141 | if ($this->evm->hasListeners(Events::preFlush)) { 142 | $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->sm)); 143 | } 144 | 145 | //TODO: single/array entity commit handling 146 | $this->commitRemoved(); 147 | $this->commitPersisted(); 148 | 149 | //Force refresh of updated indexes 150 | if ($entity === true) { 151 | $client = $this->sm->getClient(); 152 | foreach (array_unique($this->updatedIndexes) as $index) { 153 | $client->refreshIndex($index); 154 | } 155 | } 156 | 157 | $this->clear(); 158 | 159 | if ($this->evm->hasListeners(Events::postFlush)) { 160 | $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->sm)); 161 | } 162 | } 163 | 164 | /** 165 | * Commit persisted entities to the database 166 | */ 167 | private function commitPersisted() 168 | { 169 | $sortedDocuments = $this->sortObjects($this->scheduledForPersist); 170 | $client = $this->sm->getClient(); 171 | 172 | foreach ($sortedDocuments as $entityName => $documents) { 173 | $classMetadata = $this->sm->getClassMetadata($entityName); 174 | $this->updatedIndexes[] = $classMetadata->index; 175 | $client->addDocuments($classMetadata, $documents); 176 | } 177 | } 178 | 179 | /** 180 | * Commit deleted entities to the database 181 | */ 182 | private function commitRemoved() 183 | { 184 | $documents = $this->sortObjects($this->scheduledForDelete, false); 185 | $client = $this->sm->getClient(); 186 | 187 | foreach ($documents as $entityName => $documents) { 188 | $classMetadata = $this->sm->getClassMetadata($entityName); 189 | $this->updatedIndexes[] = $classMetadata->index; 190 | $client->removeDocuments($classMetadata, $documents); 191 | } 192 | } 193 | 194 | /** 195 | * Prepare entities for commit. Entities scheduled for deletion do not need 196 | * to be serialized. 197 | * 198 | * @param array $objects 199 | * @param boolean $serialize 200 | * @throws DoctrineSearchException 201 | * @return array 202 | */ 203 | private function sortObjects(array $objects, $serialize = true) 204 | { 205 | $documents = array(); 206 | $serializer = $this->sm->getSerializer(); 207 | 208 | foreach ($objects as $object) { 209 | $document = $serialize ? $serializer->serialize($object) : $object; 210 | 211 | $id = (string) $object->getId(); 212 | if (!$id) { 213 | throw new DoctrineSearchException('Entity must have an id to be indexed'); 214 | } 215 | 216 | $documents[get_class($object)][$id] = $document; 217 | } 218 | 219 | return $documents; 220 | } 221 | 222 | /** 223 | * Load and hydrate a document model 224 | * 225 | * @param ClassMetadata $class 226 | * @param mixed $value 227 | * @param array $options 228 | */ 229 | public function load(ClassMetadata $class, $value, $options = array()) 230 | { 231 | $client = $this->sm->getClient(); 232 | 233 | if (isset($options['field'])) { 234 | $document = $client->findOneBy($class, $options['field'], $value); 235 | } else { 236 | $document = $client->find($class, $value, $options); 237 | } 238 | 239 | return $this->hydrateEntity($class, $document); 240 | } 241 | 242 | /** 243 | * Load and hydrate a document collection 244 | * 245 | * @param array $classes 246 | * @param unknown $query 247 | */ 248 | public function loadCollection(array $classes, $query) 249 | { 250 | $results = $this->sm->getClient()->search($query, $classes); 251 | return $this->hydrateCollection($classes, $results); 252 | } 253 | 254 | /** 255 | * Construct an entity collection 256 | * 257 | * @param array $classes 258 | * @param Traversable $resultSet 259 | */ 260 | public function hydrateCollection(array $classes, Traversable $resultSet) 261 | { 262 | $collection = new ArrayCollection(); 263 | foreach ($resultSet as $document) { 264 | foreach ($classes as $class) { 265 | if ($document->getIndex() == $class->index && $document->getType() == $class->type) { 266 | break; 267 | } 268 | } 269 | $collection[] = $this->hydrateEntity($class, $document); 270 | } 271 | 272 | return $collection; 273 | } 274 | 275 | /** 276 | * Construct an entity object 277 | * 278 | * @param ClassMetadata $class 279 | * @param object $document 280 | */ 281 | public function hydrateEntity(ClassMetadata $class, $document) 282 | { 283 | // TODO: add support for different result set types from different clients 284 | // perhaps by wrapping documents in a layer of abstraction 285 | $data = $document->getData(); 286 | $fields = array_merge( 287 | $document->hasFields() ? $document->getFields() : array(), 288 | array('_version' => $document->getVersion()) 289 | ); 290 | 291 | foreach ($fields as $name => $value) { 292 | if (isset($class->parameters[$name])) { 293 | $data[$name] = $value; 294 | } else { 295 | foreach ($class->parameters as $param => $mapping) { 296 | if (isset($mapping['fieldName']) && $mapping['fieldName'] == $name) { 297 | $data[$param] = $value; 298 | break; 299 | } 300 | } 301 | } 302 | } 303 | 304 | $data[$class->getIdentifier()] = $document->getId(); 305 | 306 | $entity = $this->sm->getSerializer()->deserialize($class->className, json_encode($data)); 307 | 308 | if ($this->evm->hasListeners(Events::postLoad)) { 309 | $this->evm->dispatchEvent(Events::postLoad, new Event\LifecycleEventArgs($entity, $this->sm)); 310 | } 311 | 312 | return $entity; 313 | } 314 | 315 | /** 316 | * Checks whether an entity is registered in the identity map of this UnitOfWork. 317 | * 318 | * @param object $entity 319 | * 320 | * @return boolean 321 | */ 322 | public function isInIdentityMap($entity) 323 | { 324 | $oid = spl_object_hash($entity); 325 | return isset($this->scheduledForPersist[$oid]) || isset($this->scheduledForDelete[$oid]); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /lib/Doctrine/Search/Version.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace Doctrine\Search; 21 | 22 | final class Version 23 | { 24 | /** 25 | * @var string 26 | */ 27 | public static $version = '0.1-alpha'; 28 | 29 | /** 30 | * Compare a given version with the current version 31 | * 32 | * @param string $version 33 | * 34 | * @return integer Return -1 if it older, 0 if it the same, 1 if $version is newer 35 | */ 36 | public static function compare($version) 37 | { 38 | return version_compare($version, static::$version); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | ./tests/Doctrine/Tests/Search 17 | 18 | 19 | 20 | 21 | 22 | ./lib/Doctrine/Search 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/Doctrine/Tests/Models/Blog/BlogPost.php: -------------------------------------------------------------------------------- 1 | name = $name; 29 | } 30 | 31 | public function getName() 32 | { 33 | return $this->name; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Doctrine/Tests/Models/Comments/Comment.php: -------------------------------------------------------------------------------- 1 | setParent($user->getId()); 37 | $this->comment = $comment; 38 | } 39 | 40 | public function getId() 41 | { 42 | if (!$this->id) { 43 | $this->id = uniqid(); 44 | } 45 | return $this->id; 46 | } 47 | 48 | public function setParent($parent) 49 | { 50 | $this->_parent = $parent; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Doctrine/Tests/Models/Comments/Email.php: -------------------------------------------------------------------------------- 1 | email = $email; 14 | $this->createdAt = new \DateTime('now'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Doctrine/Tests/Models/Comments/User.php: -------------------------------------------------------------------------------- 1 | id) { 77 | $this->id = uniqid(); 78 | } 79 | return $this->id; 80 | } 81 | 82 | public function setName($name) 83 | { 84 | $this->name = $name; 85 | } 86 | 87 | public function setUsername($username) 88 | { 89 | $this->username = $username; 90 | } 91 | 92 | public function getUsername() 93 | { 94 | return $this->username; 95 | } 96 | 97 | public function setIp($ip) 98 | { 99 | $this->ip = $ip; 100 | } 101 | 102 | public function getIp() 103 | { 104 | return $this->ip; 105 | } 106 | 107 | public function setDescription($description) 108 | { 109 | $this->description = $description; 110 | } 111 | 112 | public function getDescription() 113 | { 114 | return $this->description; 115 | } 116 | 117 | public function getFriends() 118 | { 119 | return $this->friends; 120 | } 121 | 122 | public function addFriend(User $user) 123 | { 124 | if (!in_array($user->getId(), $this->friends)) { 125 | $this->friends[] = $user->getId(); 126 | } 127 | } 128 | 129 | public function addEmail(Email $email) 130 | { 131 | $this->emails[] = $email; 132 | } 133 | 134 | public function getRouting() 135 | { 136 | return $this->_routing; 137 | } 138 | 139 | public function setRouting($routing) 140 | { 141 | $this->_routing = $routing; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tests/Doctrine/Tests/Search/ConfigurationTest.php: -------------------------------------------------------------------------------- 1 | configuration = new Configuration(); 25 | } 26 | 27 | public function testGetMetadataDriverImpl() 28 | { 29 | $metadataDriverImpl = $this->configuration->getMetadataDriverImpl(); 30 | $this->assertInstanceOf('\Doctrine\Search\Mapping\Driver\AnnotationDriver', $metadataDriverImpl); 31 | } 32 | 33 | public function testSetMetadataDriverImpl() 34 | { 35 | $mockedMetadataDriverImpl = $this->getMockBuilder('Doctrine\Common\Persistence\Mapping\Driver\MappingDriver') 36 | ->disableOriginalConstructor() 37 | ->getMock(); 38 | 39 | $this->configuration->setMetadataDriverImpl($mockedMetadataDriverImpl); 40 | 41 | $metadataDriverImpl = $this->configuration->getMetadataDriverImpl(); 42 | $this->assertInstanceOf('Doctrine\Common\Persistence\Mapping\Driver\MappingDriver', $metadataDriverImpl); 43 | 44 | } 45 | 46 | /** 47 | * @expectedException \PHPUnit_Framework_Error 48 | */ 49 | public function testSetMetadataCacheImplWrongParameter() 50 | { 51 | $this->configuration->setMetadataCacheImpl(array()); 52 | } 53 | 54 | public function testSetMetadataCacheImpl() 55 | { 56 | $mockedMetadataCacheImpl = $this->getMock('\Doctrine\Common\Cache\Cache'); 57 | $this->configuration->setMetadataCacheImpl($mockedMetadataCacheImpl); 58 | 59 | $cacheMetadataImpl = $this->configuration->getMetadataCacheImpl(); 60 | $this->assertInstanceOf('\Doctrine\Common\Cache\Cache', $cacheMetadataImpl); 61 | } 62 | 63 | public function testNewDefaultAnnotationDriver() 64 | { 65 | $defaultAnnotationDriver = $this->configuration->newDefaultAnnotationDriver(); 66 | $this->assertInstanceOf('\Doctrine\Search\Mapping\Driver\AnnotationDriver', $defaultAnnotationDriver); 67 | } 68 | 69 | /** 70 | * @expectedException \PHPUnit_Framework_Error 71 | */ 72 | public function testNewDefaultAnnotationDriverWrongParameter() 73 | { 74 | $this->configuration->newDefaultAnnotationDriver(new \StdClass()); 75 | } 76 | 77 | public function testSetClassMetadataFactoryName() 78 | { 79 | $this->configuration = new Configuration(); 80 | $this->configuration->setClassMetadataFactoryName('test'); 81 | $this->assertEquals('test', $this->configuration->getClassMetadataFactoryName()); 82 | } 83 | 84 | 85 | public function testGetClassMetadataFactoryName() 86 | { 87 | $this->configuration = new Configuration(); 88 | $className = $this->configuration->getClassMetadataFactoryName(); 89 | $this->assertEquals($className, 'Doctrine\Search\Mapping\ClassMetadataFactory'); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/Doctrine/Tests/Search/ElasticSearch/ClientTest.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class ClientTest extends \PHPUnit_Framework_TestCase 12 | { 13 | /** @var \Elastica\Client|\PHPUnit_Framework_MockObject_MockObject */ 14 | protected $elasticaClient; 15 | 16 | /** @var \Doctrine\Search\ElasticSearch\Client */ 17 | protected $client; 18 | 19 | protected function setUp() 20 | { 21 | $this->elasticaClient = $this->getMockBuilder('Elastica\Client') 22 | ->setMethods(array('getIndex')) 23 | ->getMock(); 24 | 25 | $this->client = new Client($this->elasticaClient); 26 | } 27 | 28 | public function testFind() 29 | { 30 | $index = $this->getMockBuilder('Elastica\Index') 31 | ->disableOriginalConstructor() 32 | ->setMethods(array('getType')) 33 | ->getMock(); 34 | 35 | $result = $this->getMockBuilder('Elastica\ResultSet') 36 | ->disableOriginalConstructor() 37 | ->getMock(); 38 | 39 | $this->elasticaClient->expects($this->once()) 40 | ->method('getIndex') 41 | ->with('comments') 42 | ->will($this->returnValue($index)); 43 | 44 | $type = $this->getMockBuilder('Elastica\Type') 45 | ->disableOriginalConstructor() 46 | ->setMethods(array('getDocument')) 47 | ->getMock(); 48 | 49 | $index->expects($this->once()) 50 | ->method('getType') 51 | ->with('comment') 52 | ->will($this->returnValue($type)); 53 | 54 | $document = $this->getMockBuilder('Elastica\Document') 55 | ->disableOriginalConstructor() 56 | ->getMock(); 57 | 58 | $type->expects($this->once()) 59 | ->method('getDocument') 60 | ->with('123', array('foo' => 'bar')) 61 | ->will($this->returnValue($document)); 62 | 63 | $class = new ClassMetadata('Doctrine\Tests\Models\Comments\Comment'); 64 | $class->index = 'comments'; 65 | $class->type = 'comment'; 66 | 67 | $this->assertSame($document, $this->client->find($class, '123', array('foo' => 'bar'))); 68 | } 69 | 70 | public function testCreateIndex() 71 | { 72 | $index = $this->getMockBuilder('Elastica\Index') 73 | ->disableOriginalConstructor() 74 | ->setMethods(array('create')) 75 | ->getMock(); 76 | 77 | $index->expects($this->once()) 78 | ->method('create') 79 | ->with(array('foo' => 'bar'), true); 80 | 81 | $this->elasticaClient->expects($this->once()) 82 | ->method('getIndex') 83 | ->with('comments') 84 | ->will($this->returnValue($index)); 85 | 86 | $this->client->createIndex('comments', array('foo' => 'bar')); 87 | } 88 | 89 | public function testDeleteIndex() 90 | { 91 | $index = $this->getMockBuilder('Elastica\Index') 92 | ->disableOriginalConstructor() 93 | ->setMethods(array('delete')) 94 | ->getMock(); 95 | 96 | $index->expects($this->once()) 97 | ->method('delete'); 98 | 99 | $this->elasticaClient->expects($this->once()) 100 | ->method('getIndex') 101 | ->with('comments') 102 | ->will($this->returnValue($index)); 103 | 104 | $this->client->deleteIndex('comments'); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/Doctrine/Tests/Search/Mapping/ClassMetadataFactoryTest.php: -------------------------------------------------------------------------------- 1 | classMetadataFactory = new ClassMetadataFactory(); 21 | } 22 | 23 | /** 24 | * @expectedException PHPUnit_Framework_Error 25 | */ 26 | public function testSetSearchManagerWrongParameter() 27 | { 28 | $this->classMetadataFactory->setSearchManager(array()); 29 | } 30 | 31 | public function testSetSearchManager() 32 | { 33 | $smMock = $this->getMock('Doctrine\\Search\\SearchManager', array(), array(), '', false); 34 | $this->classMetadataFactory->setSearchManager($smMock); 35 | 36 | $reflClass = new \ReflectionClass($this->classMetadataFactory); 37 | $reflProperty = $reflClass->getProperty('sm'); 38 | $reflProperty->setAccessible(true); 39 | $sm = $reflProperty->getValue($this->classMetadataFactory); 40 | 41 | $this->assertInstanceOf('Doctrine\Search\SearchManager', $sm); 42 | } 43 | 44 | /** 45 | * @expectedException PHPUnit_Framework_Error 46 | */ 47 | public function testSetConfigurationWrongParameter() 48 | { 49 | $this->classMetadataFactory->setConfiguration(array()); 50 | } 51 | 52 | public function testSetConfiguration() 53 | { 54 | $configMock = $this->getMock('Doctrine\\Search\\Configuration'); 55 | 56 | $this->classMetadataFactory->setConfiguration($configMock); 57 | 58 | $reflClass = new \ReflectionClass($this->classMetadataFactory); 59 | $reflProperty = $reflClass->getProperty('config'); 60 | $reflProperty->setAccessible(true); 61 | $config = $reflProperty->getValue($this->classMetadataFactory); 62 | 63 | $this->assertInstanceOf('Doctrine\Search\Configuration', $config); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Doctrine/Tests/Search/Mapping/ClassMetadataTest.php: -------------------------------------------------------------------------------- 1 | classMetadata = new ClassMetadata('Doctrine\Tests\Models\Blog\BlogPost'); 23 | $this->reflectionService = new RuntimeReflectionService(); 24 | } 25 | 26 | public function testSleep() 27 | { 28 | // removed className, because it is set by constructor 29 | $fields = array( 30 | 'boost', 31 | 'index', 32 | 'numberOfReplicas', 33 | 'numberOfShards', 34 | 'parent', 35 | 'timeToLive', 36 | 'type', 37 | 'value', 38 | ); 39 | 40 | //fill the metadata fields 41 | foreach ($fields as $field) { 42 | $this->classMetadata->$field = 1; 43 | } 44 | 45 | $this->classMetadata->fieldMappings = array(); 46 | 47 | $serializedClass = serialize($this->classMetadata); 48 | $unserializedClass = unserialize($serializedClass); 49 | $unserializedClass->wakeupReflection($this->reflectionService); 50 | 51 | $this->assertEquals($unserializedClass, $this->classMetadata); 52 | } 53 | 54 | public function testWakeup() 55 | { 56 | $serializedClass = serialize($this->classMetadata); 57 | $unserializedClass = unserialize($serializedClass); 58 | $unserializedClass->wakeupReflection($this->reflectionService); 59 | 60 | $this->assertEquals($unserializedClass, $this->classMetadata); 61 | } 62 | 63 | public function testGetName() 64 | { 65 | $this->assertEquals('Doctrine\Tests\Models\Blog\BlogPost', $this->classMetadata->getName()); 66 | } 67 | 68 | public function testGetIdentifier() 69 | { 70 | $this->assertNull($this->classMetadata->getIdentifier()); 71 | } 72 | 73 | public function testGetReflectionClass() 74 | { 75 | $this->assertInstanceOf('ReflectionClass', $this->classMetadata->getReflectionClass()); 76 | } 77 | 78 | public function testIsIdentifier() 79 | { 80 | $this->assertFalse($this->classMetadata->isIdentifier('test')); 81 | } 82 | 83 | public function testHasField() 84 | { 85 | $this->assertFalse($this->classMetadata->hasField('testtestasdf')); 86 | } 87 | 88 | public function testHasAssociation() 89 | { 90 | $this->assertFalse($this->classMetadata->hasAssociation('testtestasdf')); 91 | } 92 | 93 | public function testIsSingleValuedAssociation() 94 | { 95 | $this->assertFalse($this->classMetadata->isSingleValuedAssociation('testtestasdf')); 96 | } 97 | 98 | public function testIsCollectionValuedAssociation() 99 | { 100 | $this->assertFalse($this->classMetadata->isCollectionValuedAssociation('testtestasdf')); 101 | } 102 | 103 | 104 | public function testGetAssociationNames() 105 | { 106 | $this->assertEquals(array(), $this->classMetadata->getAssociationNames()); 107 | } 108 | 109 | public function testGetTypeOfField() 110 | { 111 | $this->markTestIncomplete(); 112 | } 113 | 114 | public function testGetAssociationTargetClass() 115 | { 116 | $this->assertInternalType('string', $this->classMetadata->getAssociationTargetClass('name')); 117 | } 118 | 119 | public function testIsAssociationInverseSide() 120 | { 121 | $this->assertInternalType('string', $this->classMetadata->isAssociationInverseSide('name')); 122 | } 123 | 124 | public function testGetAssociationMappedByTargetField() 125 | { 126 | $this->assertInternalType('string', $this->classMetadata->getAssociationMappedByTargetField('name')); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/Doctrine/Tests/Search/Mapping/Driver/AbstractDriverTest.php: -------------------------------------------------------------------------------- 1 | type = $type; 13 | $expected->identifier = 'id'; 14 | $expected->index = 'searchdemo'; 15 | $expected->numberOfShards = 2; 16 | $expected->numberOfReplicas = 1; 17 | $expected->timeToLive = 180; 18 | $expected->boost = 2.0; 19 | $expected->source = true; 20 | 21 | $expected->mapRoot(array( 22 | 'fieldName' => 'dynamic_templates', 23 | 'id' => 'template_2', 24 | 'match' => 'description*', 25 | 'mapping' => array( 26 | 'type' => 'multi_field', 27 | 'fields' => array( 28 | array( 29 | 'fieldName' => '{name}', 30 | 'type' => 'string', 31 | 'includeInAll' => false 32 | ), 33 | array( 34 | 'fieldName' => 'untouched', 35 | 'type' => 'string', 36 | 'index' => 'not_analyzed' 37 | ) 38 | ) 39 | ) 40 | )); 41 | 42 | $expected->mapRoot(array( 43 | 'fieldName' => 'date_detection', 44 | 'value' => 'false' 45 | )); 46 | 47 | $expected->mapField(array( 48 | 'fieldName' => 'name', 49 | 'type' => 'string', 50 | 'includeInAll' => false, 51 | 'index' => 'no', 52 | 'boost' => 2.0 53 | )); 54 | 55 | $expected->mapField(array( 56 | 'fieldName' => 'username', 57 | 'type' => 'multi_field', 58 | 'fields' => array( 59 | array( 60 | 'fieldName' => 'username', 61 | 'type' => 'string', 62 | 'includeInAll' => true, 63 | 'analyzer' => 'whitespace' 64 | ), 65 | array( 66 | 'fieldName' => 'username.term', 67 | 'type' => 'string', 68 | 'includeInAll' => false, 69 | 'index' => 'not_analyzed' 70 | ) 71 | ) 72 | )); 73 | 74 | $expected->mapField(array( 75 | 'fieldName' => 'ip', 76 | 'type' => 'ip', 77 | 'includeInAll' => false, 78 | 'index' => 'no', 79 | 'store' => true, 80 | 'nullValue' => '127.0.0.1' 81 | )); 82 | 83 | $expected->mapField(array( 84 | 'fieldName' => 'emails', 85 | 'type' => 'nested', 86 | 'properties' => array( 87 | array( 88 | 'fieldName' => 'email', 89 | 'type' => 'string', 90 | 'includeInAll' => false, 91 | 'index' => 'not_analyzed' 92 | ), 93 | array( 94 | 'fieldName' => 'createdAt', 95 | 'type' => 'date' 96 | ) 97 | ) 98 | )); 99 | 100 | $expected->mapField(array( 101 | 'fieldName' => 'friends', 102 | 'type' => 'string', 103 | 'includeInAll' => false, 104 | 'index' => 'not_analyzed' 105 | )); 106 | 107 | $expected->mapField(array( 108 | 'fieldName' => 'active', 109 | 'type' => 'boolean', 110 | 'nullValue' => false 111 | )); 112 | 113 | $expected->mapParameter(array( 114 | 'parameterName' => '_routing', 115 | 'type' => 'string' 116 | )); 117 | 118 | return $expected; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /tests/Doctrine/Tests/Search/Mapping/Driver/AnnotationDriverTest.php: -------------------------------------------------------------------------------- 1 | reader = new AnnotationReader(); 27 | 28 | $this->annotationDriver = new AnnotationDriver($this->reader); 29 | } 30 | 31 | public function testLoadMetadataForClass() 32 | { 33 | $className = 'Doctrine\\Tests\\Models\\Comments\\User'; 34 | $metadata = new ClassMetadata($className); 35 | 36 | $this->annotationDriver->loadMetadataForClass($className, $metadata); 37 | 38 | $expected = $this->loadExpectedMetadataFor($className, 'users'); 39 | 40 | $this->assertEquals($expected, $metadata); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Doctrine/Tests/Search/Mapping/Driver/YamlDriverTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('This test requires the Symfony YAML component'); 24 | } 25 | 26 | $this->yamlDriver = new YamlDriver(__DIR__ . DIRECTORY_SEPARATOR . 'files'); 27 | } 28 | 29 | public function testLoadMetadataForClass() 30 | { 31 | $className = __NAMESPACE__.'\YamlUser'; 32 | $metadata = new ClassMetadata($className); 33 | $this->yamlDriver->loadMetadataForClass($className, $metadata); 34 | 35 | $expected = $this->loadExpectedMetadataFor($className, 'users'); 36 | 37 | $this->assertEquals($expected, $metadata); 38 | } 39 | 40 | public function testLoadMetadataForSubClass() 41 | { 42 | $className = __NAMESPACE__.'\YamlSuperUser'; 43 | $metadata = new ClassMetadata($className); 44 | $this->yamlDriver->loadMetadataForClass($className, $metadata); 45 | 46 | $expected = $this->loadExpectedMetadataFor($className, 'superusers'); 47 | 48 | $this->assertEquals($expected, $metadata); 49 | } 50 | } 51 | 52 | class YamlUser extends User 53 | { 54 | } 55 | 56 | class YamlSuperUser extends YamlUser 57 | { 58 | } 59 | -------------------------------------------------------------------------------- /tests/Doctrine/Tests/Search/Mapping/Driver/files/Doctrine.Tests.Search.Mapping.Driver.YamlSuperUser.dcm.yml: -------------------------------------------------------------------------------- 1 | Doctrine\Tests\Search\Mapping\Driver\YamlSuperUser: 2 | class: ElasticSearchable 3 | type: superusers 4 | index: searchdemo 5 | -------------------------------------------------------------------------------- /tests/Doctrine/Tests/Search/Mapping/Driver/files/Doctrine.Tests.Search.Mapping.Driver.YamlUser.dcm.yml: -------------------------------------------------------------------------------- 1 | Doctrine\Tests\Search\Mapping\Driver\YamlUser: 2 | class: ElasticSearchable 3 | type: users 4 | index: searchdemo 5 | numberOfShards: 2 6 | numberOfReplicas: 1 7 | timeToLive: 180 8 | boost: 2.0 9 | source: true 10 | root: 11 | - 12 | name: dynamic_templates 13 | id: template_2 14 | match: 'description*' 15 | mapping: 16 | type: multi_field 17 | fields: 18 | - 19 | name: '{name}' 20 | type: string 21 | includeInAll: false 22 | - 23 | name: untouched 24 | type: string 25 | index: not_analyzed 26 | - 27 | name: date_detection 28 | value: 'false' 29 | id: id 30 | fields: 31 | name: 32 | type: string 33 | includeInAll: false 34 | index: no 35 | boost: 2.0 36 | username: 37 | type: multi_field 38 | fields: 39 | - 40 | name: username 41 | type: string 42 | includeInAll: true 43 | analyzer: whitespace 44 | - 45 | name: username.term 46 | type: string 47 | includeInAll: false 48 | index: not_analyzed 49 | ip: 50 | type: ip 51 | includeInAll: false 52 | index: no 53 | store: true 54 | nullValue: 127.0.0.1 55 | friends: 56 | type: string 57 | includeInAll: false 58 | index: not_analyzed 59 | emails: 60 | type: nested 61 | properties: 62 | - 63 | name: email 64 | type: string 65 | includeInAll: false 66 | index: not_analyzed 67 | - 68 | name: createdAt 69 | type: date 70 | active: 71 | type: boolean 72 | nullValue: false 73 | parameters: 74 | _routing: 75 | type: string 76 | -------------------------------------------------------------------------------- /tests/Doctrine/Tests/Search/SearchManagerTest.php: -------------------------------------------------------------------------------- 1 | metadataFactory = $this->getMock('Doctrine\\Search\\Mapping\\ClassMetadataFactory'); 44 | 45 | $this->searchClient = $this->getMockBuilder('Doctrine\\Search\\ElasticSearch\\Client') 46 | ->disableOriginalConstructor() 47 | ->getMock(); 48 | 49 | $this->configuration = $this->getMock('Doctrine\\Search\\Configuration'); 50 | $this->configuration->expects($this->once()) 51 | ->method('getClassMetadataFactory') 52 | ->will($this->returnValue($this->metadataFactory)); 53 | 54 | $this->configuration->expects($this->once()) 55 | ->method('getMetadataCacheImpl') 56 | ->will($this->returnValue($this->getMock('Doctrine\\Common\\Cache\\ArrayCache'))); 57 | 58 | $this->evm = new EventManager(); 59 | 60 | $this->sm = new SearchManager($this->configuration, $this->searchClient, $this->evm); 61 | } 62 | 63 | /** 64 | * Tests if the returned configuration is a Doctrine\\Search\\Configuration 65 | */ 66 | public function testGetConfiguration() 67 | { 68 | $this->assertInstanceOf('Doctrine\\Search\\Configuration', $this->sm->getConfiguration()); 69 | } 70 | 71 | public function testGetClassMetadata() 72 | { 73 | $classMetadata = new ClassMetadata(BlogPost::CLASSNAME); 74 | 75 | $this->metadataFactory->expects($this->once()) 76 | ->method('getMetadataFor') 77 | ->with('Some\Class') 78 | ->will($this->returnValue($classMetadata)); 79 | 80 | $this->assertEquals($classMetadata, $this->sm->getClassMetadata('Some\Class')); 81 | } 82 | 83 | public function testGetClassMetadataFactory() 84 | { 85 | $mdf = $this->sm->getClassMetadataFactory(); 86 | $this->assertInstanceOf('Doctrine\\Search\\Mapping\\ClassMetadataFactory', $mdf); 87 | } 88 | 89 | /** 90 | * @todo Implement testFind(). 91 | */ 92 | public function testFind() 93 | { 94 | // Remove the following lines when you implement this test. 95 | $this->markTestIncomplete( 96 | 'This test has not been implemented yet.' 97 | ); 98 | } 99 | 100 | /** 101 | * @todo Implement testPersist(). 102 | */ 103 | public function testPersist() 104 | { 105 | // Remove the following lines when you implement this test. 106 | $this->markTestIncomplete( 107 | 'This test has not been implemented yet.' 108 | ); 109 | } 110 | 111 | /** 112 | * @todo Implement testRemove(). 113 | */ 114 | public function testRemove() 115 | { 116 | // Remove the following lines when you implement this test. 117 | $this->markTestIncomplete( 118 | 'This test has not been implemented yet.' 119 | ); 120 | } 121 | 122 | /** 123 | * @todo Implement testBulk(). 124 | */ 125 | public function testBulk() 126 | { 127 | // Remove the following lines when you implement this test. 128 | $this->markTestIncomplete( 129 | 'This test has not been implemented yet.' 130 | ); 131 | } 132 | 133 | /** 134 | * @todo Implement testCommit(). 135 | */ 136 | public function testCommit() 137 | { 138 | // Remove the following lines when you implement this test. 139 | $this->markTestIncomplete( 140 | 'This test has not been implemented yet.' 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tests/Doctrine/Tests/Search/VersionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, Version::compare($version)); 15 | } 16 | 17 | public static function getVersions() 18 | { 19 | return array( 20 | array('1.0', 1), 21 | array('0', -1), 22 | array('-1', -1), 23 | array('0.1-alpha', 0), 24 | array('0.1', 1), 25 | array('0.1-beta', 1), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |