├── SolrException.php ├── Tests ├── Fixtures │ ├── NotIndexedEntity.php │ ├── InvalidEntityRepository.php │ ├── ValidEntityRepository.php │ ├── ValidTestEntityNoBoost.php │ ├── ValidTestEntityFloatBoost.php │ ├── ValidTestEntityWithInvalidBoost.php │ ├── EntityWithRepository.php │ ├── InvalidTestEntityFiltered.php │ ├── EntityWithInvalidRepository.php │ ├── ValidTestEntityNoTypes.php │ ├── ValidTestEntityIndexProperty.php │ ├── ValidTestEntityIndexHandler.php │ ├── EntityWithCustomId.php │ ├── PartialUpdateEntity.php │ ├── ValidTestEntityNumericFields.php │ ├── ValidTestEntityFiltered.php │ ├── EntityCore0.php │ ├── EntityCore1.php │ ├── NestedEntity.php │ ├── ValidTestEntityWithRelation.php │ ├── EntityNestedProperty.php │ ├── ValidTestEntityWithCollection.php │ ├── ValidTestEntityAllCores.php │ ├── ValidOdmTestDocument.php │ └── ValidTestEntity.php ├── Doctrine │ ├── Mapper │ │ ├── SolrDocumentStub.php │ │ └── MetaInformationTest.php │ ├── Hydration │ │ ├── NoDatabaseValueHydratorTest.php │ │ ├── DoctrineValueHydratorTest.php │ │ └── ValueHydratorTest.php │ ├── Annotation │ │ └── FieldTest.php │ ├── ClassnameResolver │ │ ├── KnownNamespaceAliasesTest.php │ │ └── ClassnameResolverTest.php │ └── ORM │ │ └── Listener │ │ └── EntityIndexerSubscriberTest.php ├── Integration │ ├── SolariumClientFake.php │ └── Entity │ │ ├── Tag.php │ │ └── Category.php ├── Util │ ├── EntityIdentifier.php │ ├── CommandFactoryStub.php │ └── MetaTestInformationFactory.php ├── bootstrap.php ├── ResultFake.php ├── SolrResponseFake.php ├── DocumentStub.php ├── Query │ ├── FindByIdentifierQueryTest.php │ └── FindByDocumentNameQueryTest.php ├── SolrClientFake.php ├── Client │ └── Solarium │ │ └── SolariumClientBuilderTest.php ├── DependencyInjection │ └── FSSolrExtensionTest.php ├── MulticoreSolrTest.php └── AbstractSolrTest.php ├── Query ├── Exception │ ├── QueryException.php │ └── UnknownFieldException.php ├── DeleteDocumentQuery.php ├── FindByIdentifierQuery.php ├── FindByDocumentNameQuery.php ├── AbstractQuery.php └── QueryBuilderInterface.php ├── Doctrine ├── Mapper │ ├── SolrMappingException.php │ ├── EntityMapperInterface.php │ ├── EntityMapper.php │ └── MetaInformationInterface.php ├── Annotation │ ├── AnnotationReaderException.php │ ├── Nested.php │ ├── SynchronizationFilter.php │ ├── Id.php │ ├── Document.php │ └── Field.php ├── Hydration │ ├── PropertyAccessor │ │ ├── PropertyAccessorInterface.php │ │ ├── MethodCallPropertyAccessor.php │ │ └── PrivatePropertyAccessor.php │ ├── HydrationModes.php │ ├── NoDatabaseValueHydrator.php │ ├── DoctrineValueHydrator.php │ ├── HydratorInterface.php │ ├── IndexHydrator.php │ ├── DoctrineHydratorFactory.php │ ├── DoctrineHydrator.php │ └── ValueHydrator.php ├── ClassnameResolver │ ├── ClassnameResolverException.php │ ├── KnownNamespaceAliases.php │ └── ClassnameResolver.php ├── ODM │ └── Listener │ │ └── DocumentIndexerSubscriber.php ├── AbstractIndexingListener.php └── ORM │ └── Listener │ └── EntityIndexerSubscriber.php ├── Client ├── Builder.php └── Solarium │ ├── Plugin │ ├── RequestDebugger.php │ └── LoggerPlugin.php │ ├── SolariumMulticoreClient.php │ └── SolariumClientBuilder.php ├── .gitignore ├── Logging ├── SolrLoggerInterface.php └── DebugLogger.php ├── Event ├── Listener │ ├── ClearIndexLogListener.php │ ├── InsertLogListener.php │ ├── DeleteLogListener.php │ ├── UpdateLogListener.php │ ├── ErrorLogListener.php │ └── AbstractLogListener.php ├── Events.php ├── ErrorEvent.php └── Event.php ├── phpunit.xml.dist ├── FSSolrBundle.php ├── Repository ├── RepositoryInterface.php └── Repository.php ├── .travis.yml ├── Resources ├── views │ └── Profiler │ │ └── icon.svg ├── config │ ├── event_listener.xml │ └── log_listener.xml └── doc │ ├── index_relations.md │ └── indexing.md ├── LICENSE ├── DependencyInjection ├── Compiler │ └── AddSolariumPluginsPass.php ├── Configuration.php └── FSSolrExtension.php ├── Command ├── ClearIndexCommand.php └── ShowSchemaCommand.php ├── composer.json ├── SolrInterface.php ├── Helper └── DocumentHelper.php └── DataCollector └── RequestCollector.php /SolrException.php: -------------------------------------------------------------------------------- 1 | logger->debug(sprintf('clear index')); 18 | } 19 | } -------------------------------------------------------------------------------- /Doctrine/Hydration/NoDatabaseValueHydrator.php: -------------------------------------------------------------------------------- 1 | id; 30 | } 31 | } -------------------------------------------------------------------------------- /Tests/ResultFake.php: -------------------------------------------------------------------------------- 1 | data = $data; 13 | } 14 | 15 | public function getIterator() 16 | { 17 | return new \ArrayIterator($this->data); 18 | } 19 | 20 | public function count() 21 | { 22 | return count($this->data); 23 | } 24 | 25 | public function getNumFound() 26 | { 27 | return $this->count(); 28 | } 29 | } -------------------------------------------------------------------------------- /Tests/SolrResponseFake.php: -------------------------------------------------------------------------------- 1 | response = $response; 11 | } 12 | 13 | /** 14 | * @return array 15 | */ 16 | public function getResponse() 17 | { 18 | return $this->response; 19 | } 20 | 21 | /** 22 | * @param array 23 | */ 24 | public function setResponse($response) 25 | { 26 | $this->response = $response; 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Event/Events.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | ./Tests 18 | 19 | 20 | -------------------------------------------------------------------------------- /Doctrine/Hydration/PropertyAccessor/MethodCallPropertyAccessor.php: -------------------------------------------------------------------------------- 1 | setterName = $setterName; 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function setValue($targetObject, $value) 24 | { 25 | $targetObject->{$this->setterName}($value); 26 | } 27 | } -------------------------------------------------------------------------------- /Tests/Fixtures/PartialUpdateEntity.php: -------------------------------------------------------------------------------- 1 | subtitle; 22 | } 23 | 24 | /** 25 | * @param string $subtitle 26 | */ 27 | public function setSubtitle($subtitle) 28 | { 29 | $this->subtitle = $subtitle; 30 | } 31 | } -------------------------------------------------------------------------------- /FSSolrBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new AddSolariumPluginsPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Repository/RepositoryInterface.php: -------------------------------------------------------------------------------- 1 | getMetaInformation(); 18 | 19 | $nameWithId = $this->createDocumentNameWithId($metaInformation); 20 | $fieldList = $this->createFieldList($metaInformation); 21 | 22 | $this->logger->debug( 23 | sprintf('document %s with fields %s was added', $nameWithId, $fieldList) 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Doctrine/Hydration/DoctrineValueHydrator.php: -------------------------------------------------------------------------------- 1 | getField($fieldName) && $metaInformation->getField($fieldName)->getter) { 21 | return false; 22 | } 23 | 24 | return true; 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /Event/Listener/DeleteLogListener.php: -------------------------------------------------------------------------------- 1 | getMetaInformation(); 19 | 20 | $nameWithId = $this->createDocumentNameWithId($metaInformation); 21 | $fieldList = $this->createFieldList($metaInformation); 22 | 23 | $this->logger->debug( 24 | sprintf('document %s with fields %s was deleted', $nameWithId, $fieldList) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Event/Listener/UpdateLogListener.php: -------------------------------------------------------------------------------- 1 | getMetaInformation(); 19 | 20 | $nameWithId = $this->createDocumentNameWithId($metaInformation); 21 | $fieldList = $this->createFieldList($metaInformation); 22 | 23 | $this->logger->debug( 24 | sprintf('document %s with fields %s was updated', $nameWithId, $fieldList) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | sudo: false 3 | 4 | php: 5 | - 7.0 6 | - 7.1 7 | - 7.2 8 | - 7.3 9 | 10 | before_script: 11 | - curl -sSL https://raw.githubusercontent.com/floriansemm/travis-solr/master/travis-solr.sh | SOLR_CORE=core0 DEBUG=1 SOLR_VERSION=4.8.0 SOLR_CONFS="Tests/Resources/config/schema.xml" bash 12 | 13 | install: 14 | - composer self-update 15 | - if [[ ${TRAVIS_PHP_VERSION:0:2} == "5." ]]; then yes '' | pecl -q install -f mongo; fi 16 | - if [[ ${TRAVIS_PHP_VERSION:0:2} == "7." ]]; then pecl install -f mongodb; fi 17 | - if [[ ${TRAVIS_PHP_VERSION:0:2} == "7." ]]; then composer config "platform.ext-mongo" "1.6.16" && composer require alcaeus/mongo-php-adapter; fi 18 | - composer update 19 | 20 | script: 21 | - ./bin/phpunit 22 | -------------------------------------------------------------------------------- /Doctrine/ClassnameResolver/ClassnameResolverException.php: -------------------------------------------------------------------------------- 1 | classProperty = $classProperty; 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function setValue($targetObject, $value) 25 | { 26 | $this->classProperty->setAccessible(true); 27 | $this->classProperty->setValue($targetObject, $value); 28 | } 29 | } -------------------------------------------------------------------------------- /Tests/Fixtures/ValidTestEntityFiltered.php: -------------------------------------------------------------------------------- 1 | shouldBeIndexedWasCalled = true; 27 | 28 | return $this->shouldIndex; 29 | } 30 | 31 | public function getShouldBeIndexedWasCalled() 32 | { 33 | return $this->shouldBeIndexedWasCalled; 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Event/ErrorEvent.php: -------------------------------------------------------------------------------- 1 | exception; 22 | } 23 | 24 | /** 25 | * @param \Exception $exception 26 | */ 27 | public function setException($exception) 28 | { 29 | $this->exception = $exception; 30 | } 31 | 32 | /** 33 | * @return string 34 | */ 35 | public function getExceptionMessage() 36 | { 37 | if (!$this->exception) { 38 | return ''; 39 | } 40 | 41 | return $this->exception->getMessage(); 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /Doctrine/Annotation/Document.php: -------------------------------------------------------------------------------- 1 | boost; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getIndex() 45 | { 46 | return $this->index; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Doctrine/Hydration/HydratorInterface.php: -------------------------------------------------------------------------------- 1 | getExceptionMessage(); 21 | } 22 | 23 | $this->logger->error( 24 | sprintf('the error "%s" occure while executing event %s', $exceptionMessage, $event->getSolrAction()) 25 | ); 26 | 27 | if ($event->hasSourceEvent()) { 28 | $event->getSourceEvent()->stopPropagation(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Query/DeleteDocumentQuery.php: -------------------------------------------------------------------------------- 1 | documentKey = $documentKey; 20 | } 21 | 22 | /** 23 | * @return string 24 | * 25 | * @throws QueryException when id or document_name is null 26 | */ 27 | public function getQuery() 28 | { 29 | $idField = $this->documentKey; 30 | 31 | if ($idField == null) { 32 | throw new QueryException('id should not be null'); 33 | } 34 | 35 | $this->setQuery(sprintf('id:%s', $idField)); 36 | 37 | return parent::getQuery(); 38 | } 39 | } -------------------------------------------------------------------------------- /Client/Solarium/Plugin/RequestDebugger.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 27 | 28 | parent::__construct(); 29 | } 30 | 31 | /** 32 | * @param PreExecuteRequest $event 33 | */ 34 | public function preExecuteRequest(PreExecuteRequest $event) 35 | { 36 | $this->logger->info(sprintf('run request: %s', urldecode($event->getRequest()->getUri()))); 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /Resources/views/Profiler/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /Doctrine/Mapper/EntityMapperInterface.php: -------------------------------------------------------------------------------- 1 | add(new MapAllFieldsCommand(new MetaInformationFactory($reader)), 'all'); 22 | $commandFactory->add(new MapIdentifierCommand(), 'identifier'); 23 | 24 | return $commandFactory; 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Tests/Fixtures/EntityCore0.php: -------------------------------------------------------------------------------- 1 | id; 30 | } 31 | 32 | /** 33 | * @return string 34 | */ 35 | public function getText() 36 | { 37 | return $this->text; 38 | } 39 | 40 | /** 41 | * @param mixed $id 42 | */ 43 | public function setId($id) 44 | { 45 | $this->id = $id; 46 | } 47 | 48 | /** 49 | * @param string $text 50 | */ 51 | public function setText($text) 52 | { 53 | $this->text = $text; 54 | } 55 | 56 | 57 | } -------------------------------------------------------------------------------- /Tests/Fixtures/EntityCore1.php: -------------------------------------------------------------------------------- 1 | id; 30 | } 31 | 32 | /** 33 | * @return string 34 | */ 35 | public function getText() 36 | { 37 | return $this->text; 38 | } 39 | 40 | /** 41 | * @param mixed $id 42 | */ 43 | public function setId($id) 44 | { 45 | $this->id = $id; 46 | } 47 | 48 | /** 49 | * @param string $text 50 | */ 51 | public function setText($text) 52 | { 53 | $this->text = $text; 54 | } 55 | 56 | 57 | } -------------------------------------------------------------------------------- /Doctrine/Hydration/IndexHydrator.php: -------------------------------------------------------------------------------- 1 | valueHydrator = $valueHydrator; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function hydrate($document, MetaInformationInterface $metaInformation) 29 | { 30 | $sourceTargetEntity = $metaInformation->getEntity(); 31 | $targetEntity = clone $sourceTargetEntity; 32 | 33 | $metaInformation->setEntity($targetEntity); 34 | 35 | return $this->valueHydrator->hydrate($document, $metaInformation); 36 | } 37 | } -------------------------------------------------------------------------------- /Tests/Doctrine/Mapper/MetaInformationTest.php: -------------------------------------------------------------------------------- 1 | name = $name; 16 | $value->value = $value; 17 | 18 | return $value; 19 | } 20 | 21 | public function testHasCallback_CallbackSet() 22 | { 23 | $information = new MetaInformation(); 24 | $information->setSynchronizationCallback('function'); 25 | 26 | $this->assertTrue($information->hasSynchronizationFilter(), 'has callback'); 27 | } 28 | 29 | public function testHasCallback_NoCallbackSet() 30 | { 31 | $information = new MetaInformation(); 32 | 33 | $this->assertFalse($information->hasSynchronizationFilter(), 'has no callback'); 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Tests/DocumentStub.php: -------------------------------------------------------------------------------- 1 | $this->id, 'document_name' => $this->document_name_s); 31 | 32 | return $fields[$fieldName]; 33 | } 34 | 35 | /** 36 | * @return array 37 | */ 38 | public function getFields() 39 | { 40 | return array('id' => $this->id, 'document_name' => $this->document_name_s); 41 | } 42 | } -------------------------------------------------------------------------------- /Tests/Fixtures/NestedEntity.php: -------------------------------------------------------------------------------- 1 | id; 32 | } 33 | 34 | /** 35 | * @param mixed $id 36 | */ 37 | public function setId($id) 38 | { 39 | $this->id = $id; 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getName() 46 | { 47 | return $this->name; 48 | } 49 | 50 | /** 51 | * @param string $name 52 | */ 53 | public function setName($name) 54 | { 55 | $this->name = $name; 56 | } 57 | 58 | 59 | } -------------------------------------------------------------------------------- /Query/FindByIdentifierQuery.php: -------------------------------------------------------------------------------- 1 | documentKey = $documentKey; 20 | } 21 | 22 | /** 23 | * @return string 24 | * 25 | * @throws QueryException when id or document_name is null 26 | */ 27 | public function getQuery() 28 | { 29 | $idField = $this->documentKey; 30 | 31 | if ($idField == null) { 32 | throw new QueryException('id should not be null'); 33 | } 34 | 35 | $documentLimitation = $this->createFilterQuery('id')->setQuery(sprintf('id:%s', $idField)); 36 | $this->addFilterQuery($documentLimitation); 37 | 38 | $this->setQuery('*:*'); 39 | 40 | return parent::getQuery(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Florian Semm 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do 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 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/AddSolariumPluginsPass.php: -------------------------------------------------------------------------------- 1 | findTaggedServiceIds('solarium.client.plugin'); 20 | 21 | $clientBuilder = $container->getDefinition('solr.client.adapter.builder'); 22 | foreach ($plugins as $service => $definition) { 23 | $clientBuilder->addMethodCall( 24 | 'addPlugin', 25 | array( 26 | $definition[0]['plugin-name'], 27 | new Reference($service) 28 | ) 29 | ); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /Doctrine/Hydration/DoctrineHydratorFactory.php: -------------------------------------------------------------------------------- 1 | container = $container; 20 | } 21 | 22 | /** 23 | * @return DoctrineHydrator 24 | */ 25 | public function factory() 26 | { 27 | $valueHydrator = $this->container->get('solr.doctrine.hydration.doctrine_value_hydrator'); 28 | 29 | $hydrator = new DoctrineHydrator($valueHydrator); 30 | if ($this->container->has('doctrine')) { 31 | $hydrator->setOrmManager($this->container->get('doctrine')); 32 | } 33 | 34 | if ($this->container->has('doctrine_mongodb')) { 35 | $hydrator->setOdmManager($this->container->get('doctrine_mongodb')); 36 | } 37 | 38 | return $hydrator; 39 | } 40 | } -------------------------------------------------------------------------------- /Logging/DebugLogger.php: -------------------------------------------------------------------------------- 1 | queries; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function startRequest(array $request) 37 | { 38 | $this->start = microtime(true); 39 | $this->queries[++$this->currentQuery] = [ 40 | 'request' => $request, 41 | 'executionMS' => 0 42 | ]; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function stopRequest() 49 | { 50 | $this->queries[$this->currentQuery]['executionMS'] = microtime(true) - $this->start; 51 | } 52 | } -------------------------------------------------------------------------------- /Event/Listener/AbstractLogListener.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 21 | } 22 | 23 | /** 24 | * @param MetaInformationInterface $metaInformation 25 | * 26 | * @return string 27 | */ 28 | protected function createDocumentNameWithId(MetaInformationInterface $metaInformation) 29 | { 30 | return $metaInformation->getDocumentName() . ':' . $metaInformation->getEntityId(); 31 | } 32 | 33 | /** 34 | * @param MetaInformationInterface $metaInformation 35 | * 36 | * @return string 37 | */ 38 | protected function createFieldList(MetaInformationInterface $metaInformation) 39 | { 40 | return implode(', ', $metaInformation->getFields()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/Query/FindByIdentifierQueryTest.php: -------------------------------------------------------------------------------- 1 | setKey('id', 'validtestentity_1'); 18 | 19 | $query = new FindByIdentifierQuery(); 20 | $query->setDocumentKey('validtestentity_1'); 21 | $query->setDocument($document); 22 | 23 | $this->assertEquals('*:*', $query->getQuery()); 24 | $this->assertEquals('id:validtestentity_1', $query->getFilterQuery('id')->getQuery()); 25 | } 26 | 27 | /** 28 | * @expectedException FS\SolrBundle\Query\Exception\QueryException 29 | * @expectedExceptionMessage id should not be null 30 | */ 31 | public function testGetQuery_IdMissing() 32 | { 33 | $query = new FindByIdentifierQuery(); 34 | $query->setDocument(new Document()); 35 | 36 | $query->getQuery(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Query/FindByDocumentNameQuery.php: -------------------------------------------------------------------------------- 1 | documentName = $documentName; 25 | } 26 | 27 | /** 28 | * @return string 29 | * 30 | * @throws QueryException if documentName is null 31 | */ 32 | public function getQuery() 33 | { 34 | $documentName = $this->documentName; 35 | 36 | if ($documentName == null) { 37 | throw new QueryException('documentName should not be null'); 38 | } 39 | 40 | $documentLimitation = $this->createFilterQuery('id')->setQuery(sprintf('id:%s_*', $documentName)); 41 | $this->addFilterQuery($documentLimitation); 42 | 43 | $this->setQuery('*:*'); 44 | 45 | return parent::getQuery(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Resources/config/event_listener.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Command/ClearIndexCommand.php: -------------------------------------------------------------------------------- 1 | setName('solr:index:clear') 24 | ->setDescription('Clear the whole index'); 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function execute(InputInterface $input, OutputInterface $output) 31 | { 32 | $solr = $this->getContainer()->get('solr.client'); 33 | 34 | try { 35 | $solr->clearIndex(); 36 | } catch (SolrException $e) { 37 | $output->writeln(sprintf('A error occurs: %s', $e->getMessage())); 38 | } 39 | 40 | $output->writeln('Index successful cleared.'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/Query/FindByDocumentNameQueryTest.php: -------------------------------------------------------------------------------- 1 | addField('id', 'validtestentity_1'); 20 | 21 | $query = new FindByDocumentNameQuery(); 22 | $query->setDocumentName('validtestentity'); 23 | $query->setDocument($document); 24 | 25 | $this->assertEquals('*:*', $query->getQuery(), 'filter query'); 26 | $this->assertEquals('id:validtestentity_*', $query->getFilterQuery('id')->getQuery()); 27 | } 28 | 29 | /** 30 | * @expectedException FS\SolrBundle\Query\Exception\QueryException 31 | * @expectedExceptionMessage documentName should not be null 32 | */ 33 | public function testGetQuery_DocumentnameMissing() 34 | { 35 | $query = new FindByDocumentNameQuery(); 36 | $query->setDocument(new Document()); 37 | 38 | $query->getQuery(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Tests/Doctrine/Hydration/NoDatabaseValueHydratorTest.php: -------------------------------------------------------------------------------- 1 | '0003115-2231_S', 24 | 'title_t' => 'fooo_bar' 25 | )); 26 | 27 | $entity = new ValidTestEntity(); 28 | 29 | $metainformations = new MetaInformationFactory($reader); 30 | $metainformations = $metainformations->loadInformation($entity); 31 | 32 | $entity = $hydrator->hydrate($document, $metainformations); 33 | 34 | $this->assertEquals('0003115-2231_S', $entity->getId()); 35 | $this->assertEquals('fooo_bar', $entity->getTitle()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "floriansemm/solr-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Symfony Solr integration bundle", 5 | "keywords": [ 6 | "search", 7 | "index", 8 | "symfony", 9 | "solr" 10 | ], 11 | "homepage": "https://github.com/floriansemm/SolrBundle", 12 | "license": "MIT", 13 | "require": { 14 | "php": "^7.0", 15 | "solarium/solarium": "^4.0", 16 | "symfony/dependency-injection": "^2.3|^3.0|^4.0", 17 | "symfony/http-kernel": "^2.3|^3.0|^4.0", 18 | "symfony/config": "^2.3|^3.0|^4.0", 19 | "symfony/doctrine-bridge": "^2.3|^3.0|^4.0", 20 | "minimalcode/search": "^1.0", 21 | "ramsey/uuid": "^3.5", 22 | "myclabs/deep-copy": "^1.6", 23 | "doctrine/annotations": "^1.4" 24 | }, 25 | "require-dev": { 26 | "doctrine/mongodb-odm-bundle": "*", 27 | "doctrine/orm": "^2.3", 28 | "phpunit/phpunit": "^5.4" 29 | }, 30 | "minimum-stability": "RC", 31 | "autoload": { 32 | "psr-0": { 33 | "FS\\SolrBundle": "" 34 | } 35 | }, 36 | "config": { 37 | "bin-dir": "bin" 38 | }, 39 | "extras": { 40 | "branch-alias": { 41 | "dev-master": "2.0.x-dev" 42 | } 43 | }, 44 | "suggest": { 45 | "doctrine/orm": "Required if you want to use the Doctrine ORM", 46 | "doctrine/mongodb-odm-bundle": "Required if you want to use the MongoDB ODM" 47 | }, 48 | "target-dir": "FS/SolrBundle" 49 | } 50 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('fs_solr'); 18 | $rootNode->children() 19 | ->arrayNode('endpoints') 20 | ->useAttributeAsKey('name') 21 | ->prototype('array') 22 | ->children() 23 | ->scalarNode('dsn')->end() 24 | ->scalarNode('scheme')->end() 25 | ->scalarNode('host')->end() 26 | ->scalarNode('port')->end() 27 | ->scalarNode('path')->end() 28 | ->scalarNode('core')->end() 29 | ->scalarNode('timeout')->end() 30 | ->booleanNode('active')->defaultValue(true)->end() 31 | ->end() 32 | ->end() 33 | ->end() 34 | ->booleanNode('auto_index')->defaultValue(true)->end() 35 | ->end(); 36 | 37 | return $treeBuilder; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SolrInterface.php: -------------------------------------------------------------------------------- 1 | assertFalse($hydrator->mapValue('createdAt', array(), new MetaInformation())); 21 | } 22 | 23 | /** 24 | * @test 25 | */ 26 | public function skipObjects() 27 | { 28 | $hydrator = new DoctrineValueHydrator(); 29 | 30 | $field = new Field(array('type' => 'datetime')); 31 | $field->name = 'createdAt'; 32 | $field->getter = 'format(\'Y-m-d\TH:i:s.z\Z\')'; 33 | 34 | $metaInformation = new MetaInformation(); 35 | $metaInformation->setFields(array($field)); 36 | 37 | $this->assertFalse($hydrator->mapValue('createdAt', new \DateTime(), $metaInformation)); 38 | } 39 | 40 | /** 41 | * @test 42 | */ 43 | public function mapCommonType() 44 | { 45 | $hydrator = new DoctrineValueHydrator(); 46 | 47 | $field = new Field(array('type' => 'string')); 48 | $field->name = 'title'; 49 | 50 | $metaInformation = new MetaInformation(); 51 | $metaInformation->setFields(array($field)); 52 | 53 | $this->assertTrue($hydrator->mapValue('title_s', 'a title', $metaInformation)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/Util/MetaTestInformationFactory.php: -------------------------------------------------------------------------------- 1 | setId(2); 22 | 23 | $metaInformation = new MetaInformation(); 24 | 25 | $title = new Field(array('name' => 'title', 'boost' => '1.8', 'value' => 'A title')); 26 | $text = new Field(array('name' => 'text', 'type' => 'text', 'value' => 'A text')); 27 | $createdAt = new Field(array('name' => 'created_at', 'type' => 'date', 'boost' => '1', 'value' => 'A created at')); 28 | 29 | $metaInformation->setFields(array($title, $text, $createdAt)); 30 | 31 | $fieldMapping = array( 32 | 'id' => 'id', 33 | 'title_s' => 'title', 34 | 'text_t' => 'text', 35 | 'created_at_dt' => 'created_at' 36 | ); 37 | $metaInformation->setIdentifier(new Id(array())); 38 | $metaInformation->setBoost(1); 39 | $metaInformation->setFieldMapping($fieldMapping); 40 | $metaInformation->setEntity($entity); 41 | $metaInformation->setDocumentName('validtestentity'); 42 | $metaInformation->setClassName(get_class($entity)); 43 | $metaInformation->setIndex(null); 44 | 45 | return $metaInformation; 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /Event/Event.php: -------------------------------------------------------------------------------- 1 | client = $client; 46 | $this->metainformation = $metainformation; 47 | $this->solrAction = $solrAction; 48 | $this->sourceEvent = $sourceEvent; 49 | } 50 | 51 | /** 52 | * @return MetaInformationInterface 53 | */ 54 | public function getMetaInformation() 55 | { 56 | return $this->metainformation; 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function getSolrAction() 63 | { 64 | return $this->solrAction; 65 | } 66 | 67 | /** 68 | * @return Event 69 | */ 70 | public function getSourceEvent() 71 | { 72 | return $this->sourceEvent; 73 | } 74 | 75 | /** 76 | * @return bool 77 | */ 78 | public function hasSourceEvent() 79 | { 80 | return $this->sourceEvent !== null; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Doctrine/ODM/Listener/DocumentIndexerSubscriber.php: -------------------------------------------------------------------------------- 1 | getDocument(); 27 | 28 | try { 29 | $doctrineChangeSet = $args->getDocumentManager()->getUnitOfWork()->getDocumentChangeSet($document); 30 | 31 | if ($this->hasChanged($doctrineChangeSet, $document) == false) { 32 | return; 33 | } 34 | 35 | $this->solr->updateDocument($document); 36 | } catch (\RuntimeException $e) { 37 | $this->logger->debug($e->getMessage()); 38 | } 39 | } 40 | 41 | /** 42 | * @param LifecycleEventArgs $args 43 | */ 44 | public function preRemove(LifecycleEventArgs $args) 45 | { 46 | $entity = $args->getDocument(); 47 | 48 | try { 49 | $this->solr->removeDocument($entity); 50 | } catch (\RuntimeException $e) { 51 | $this->logger->debug($e->getMessage()); 52 | } 53 | } 54 | 55 | /** 56 | * @param LifecycleEventArgs $args 57 | */ 58 | public function postPersist(LifecycleEventArgs $args) 59 | { 60 | $entity = $args->getDocument(); 61 | 62 | try { 63 | $this->solr->addDocument($entity); 64 | } catch (\RuntimeException $e) { 65 | $this->logger->debug($e->getMessage()); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /Client/Solarium/Plugin/LoggerPlugin.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function initPluginType() 33 | { 34 | $dispatcher = $this->client->getEventDispatcher(); 35 | $dispatcher->addListener(Events::PRE_EXECUTE_REQUEST, [$this, 'preExecuteRequest']); 36 | $dispatcher->addListener(Events::POST_EXECUTE_REQUEST, [$this, 'postExecuteRequest']); 37 | } 38 | 39 | /** 40 | * @param PreExecuteRequest $event 41 | */ 42 | public function preExecuteRequest(PreExecuteRequest $event) 43 | { 44 | $endpoint = $event->getEndpoint(); 45 | $request = $event->getRequest(); 46 | $uri = $request->getUri(); 47 | 48 | $path = sprintf('%s://%s:%s%s/%s', $endpoint->getScheme(), $endpoint->getHost(), $endpoint->getPort(), $endpoint->getPath(), urldecode($uri)); 49 | 50 | $requestInformation = [ 51 | 'uri' => $path, 52 | 'method' => $request->getMethod(), 53 | 'raw_data' => $request->getRawData() 54 | ]; 55 | 56 | $this->logger->startRequest($requestInformation); 57 | } 58 | 59 | /** 60 | * Issue stop logger 61 | */ 62 | public function postExecuteRequest() 63 | { 64 | $this->logger->stopRequest(); 65 | } 66 | 67 | /** 68 | * @return Client 69 | */ 70 | public function getClient() 71 | { 72 | return $this->client; 73 | } 74 | } -------------------------------------------------------------------------------- /Helper/DocumentHelper.php: -------------------------------------------------------------------------------- 1 | solariumClient = $solr->getClient(); 28 | $this->metaInformationFactory = $solr->getMetaFactory(); 29 | } 30 | 31 | /** 32 | * @param mixed $entity 33 | * 34 | * @return int 35 | */ 36 | public function getLastInsertDocumentId($entity) 37 | { 38 | $metaInformation = $this->metaInformationFactory->loadInformation($entity); 39 | 40 | /** @var Query $select */ 41 | $select = $this->solariumClient->createQuery(SolariumClient::QUERY_SELECT); 42 | $select->setQuery(sprintf('id:%s*', $metaInformation->getDocumentKey())); 43 | $select->setRows($this->getNumberOfDocuments($metaInformation->getDocumentName())); 44 | $select->addFields(array('id')); 45 | 46 | $result = $this->solariumClient->select($select); 47 | 48 | if ($result->count() == 0) { 49 | return 0; 50 | } 51 | 52 | $ids = array_map(function ($document) { 53 | return substr($document->id, stripos($document->id, '_') + 1); 54 | }, $result->getIterator()->getArrayCopy()); 55 | 56 | return intval(max($ids)); 57 | } 58 | 59 | /** 60 | * @param string $documentKey 61 | * 62 | * @return int 63 | */ 64 | private function getNumberOfDocuments($documentKey) 65 | { 66 | $select = $this->solariumClient->createQuery(SolariumClient::QUERY_SELECT); 67 | $select->setQuery(sprintf('id:%s_*', $documentKey)); 68 | 69 | $result = $this->solariumClient->select($select); 70 | 71 | return $result->getNumFound(); 72 | } 73 | } -------------------------------------------------------------------------------- /Tests/Integration/Entity/Tag.php: -------------------------------------------------------------------------------- 1 | name = $name; 52 | } 53 | 54 | /** 55 | * Get id 56 | * 57 | * @return integer 58 | */ 59 | public function getId() 60 | { 61 | return $this->id; 62 | } 63 | 64 | /** 65 | * @param int $id 66 | */ 67 | public function setId(int $id) 68 | { 69 | $this->id = $id; 70 | } 71 | 72 | /** 73 | * Set name 74 | * 75 | * @param string $name 76 | * 77 | * @return Tag 78 | */ 79 | public function setName($name) 80 | { 81 | $this->name = $name; 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Get name 88 | * 89 | * @return string 90 | */ 91 | public function getName() 92 | { 93 | return $this->name; 94 | } 95 | 96 | /** 97 | * @return Post 98 | */ 99 | public function getPost() 100 | { 101 | return $this->post; 102 | } 103 | 104 | /** 105 | * @param Post $post 106 | */ 107 | public function setPost($post) 108 | { 109 | $this->post = $post; 110 | } 111 | 112 | 113 | } 114 | 115 | -------------------------------------------------------------------------------- /Doctrine/ClassnameResolver/KnownNamespaceAliases.php: -------------------------------------------------------------------------------- 1 | Full Entity/Documentnamespace 15 | */ 16 | private $knownNamespaceAlias = array(); 17 | 18 | /** 19 | * @var array 20 | */ 21 | private $entityClassnames = array(); 22 | 23 | /** 24 | * @param OdmConfiguration $configuration 25 | */ 26 | public function addDocumentNamespaces(OdmConfiguration $configuration) 27 | { 28 | $this->knownNamespaceAlias = array_merge($this->knownNamespaceAlias, $configuration->getDocumentNamespaces()); 29 | 30 | if ($configuration->getMetadataDriverImpl()) { 31 | $this->entityClassnames = array_merge($this->entityClassnames, $configuration->getMetadataDriverImpl()->getAllClassNames()); 32 | } 33 | } 34 | 35 | /** 36 | * @param OrmConfiguration $configuration 37 | */ 38 | public function addEntityNamespaces(OrmConfiguration $configuration) 39 | { 40 | $this->knownNamespaceAlias = array_merge($this->knownNamespaceAlias, $configuration->getEntityNamespaces()); 41 | 42 | if ($configuration->getMetadataDriverImpl()) { 43 | $this->entityClassnames = array_merge($this->entityClassnames, $configuration->getMetadataDriverImpl()->getAllClassNames()); 44 | } 45 | } 46 | 47 | /** 48 | * @param string $alias 49 | * 50 | * @return bool 51 | */ 52 | public function isKnownNamespaceAlias($alias) 53 | { 54 | return isset($this->knownNamespaceAlias[$alias]); 55 | } 56 | 57 | /** 58 | * @param string $alias 59 | * 60 | * @return string 61 | */ 62 | public function getFullyQualifiedNamespace($alias) 63 | { 64 | if ($this->isKnownNamespaceAlias($alias)) { 65 | return $this->knownNamespaceAlias[$alias]; 66 | } 67 | 68 | return ''; 69 | } 70 | 71 | /** 72 | * @return array 73 | */ 74 | public function getAllNamespaceAliases() 75 | { 76 | return array_keys($this->knownNamespaceAlias); 77 | } 78 | 79 | /** 80 | * @return array 81 | */ 82 | public function getEntityClassnames() 83 | { 84 | return $this->entityClassnames; 85 | } 86 | } -------------------------------------------------------------------------------- /Doctrine/Hydration/DoctrineHydrator.php: -------------------------------------------------------------------------------- 1 | valueHydrator = $valueHydrator; 38 | } 39 | 40 | /** 41 | * @param ManagerRegistry $ormManager 42 | */ 43 | public function setOrmManager($ormManager) 44 | { 45 | $this->ormManager = $ormManager; 46 | } 47 | 48 | /** 49 | * @param ManagerRegistry $odmManager 50 | */ 51 | public function setOdmManager($odmManager) 52 | { 53 | $this->odmManager = $odmManager; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function hydrate($document, MetaInformationInterface $metaInformation) 60 | { 61 | $entityId = $this->valueHydrator->removePrefixedKeyValues($document['id']); 62 | 63 | $doctrineEntity = null; 64 | if ($metaInformation->getDoctrineMapperType() == MetaInformationInterface::DOCTRINE_MAPPER_TYPE_RELATIONAL) { 65 | $doctrineEntity = $this->ormManager 66 | ->getManager() 67 | ->getRepository($metaInformation->getClassName()) 68 | ->find($entityId); 69 | } elseif ($metaInformation->getDoctrineMapperType() == MetaInformationInterface::DOCTRINE_MAPPER_TYPE_DOCUMENT) { 70 | $doctrineEntity = $this->odmManager 71 | ->getManager() 72 | ->getRepository($metaInformation->getClassName()) 73 | ->find($entityId); 74 | } 75 | 76 | if ($doctrineEntity !== null) { 77 | $metaInformation->setEntity($doctrineEntity); 78 | } 79 | 80 | return $this->valueHydrator->hydrate($document, $metaInformation); 81 | } 82 | } -------------------------------------------------------------------------------- /Tests/Integration/Entity/Category.php: -------------------------------------------------------------------------------- 1 | posts = new ArrayCollection(); 59 | } 60 | 61 | /** 62 | * @return int 63 | */ 64 | public function getId() 65 | { 66 | return $this->id; 67 | } 68 | 69 | /** 70 | * @param int $id 71 | */ 72 | public function setId($id) 73 | { 74 | $this->id = $id; 75 | } 76 | 77 | /** 78 | * @return string 79 | */ 80 | public function getTitle() 81 | { 82 | return $this->title; 83 | } 84 | 85 | /** 86 | * @param string $title 87 | */ 88 | public function setTitle($title) 89 | { 90 | $this->title = $title; 91 | } 92 | 93 | /** 94 | * @return Post[] 95 | */ 96 | public function getPosts() 97 | { 98 | return $this->posts; 99 | } 100 | 101 | /** 102 | * @param Post[] $posts 103 | */ 104 | public function setPosts($posts) 105 | { 106 | $this->posts = $posts; 107 | } 108 | 109 | public function addPost($post) 110 | { 111 | $this->posts->add($post); 112 | } 113 | 114 | /** 115 | * @param string $info 116 | */ 117 | public function setInfo($info) 118 | { 119 | $this->info = $info; 120 | } 121 | 122 | } -------------------------------------------------------------------------------- /Doctrine/AbstractIndexingListener.php: -------------------------------------------------------------------------------- 1 | solr = $solr; 36 | $this->metaInformationFactory = $metaInformationFactory; 37 | $this->logger = $logger; 38 | } 39 | 40 | /** 41 | * @param array $doctrineChangeSet 42 | * @param object $entity 43 | * 44 | * @return bool 45 | */ 46 | protected function hasChanged($doctrineChangeSet, $entity) 47 | { 48 | if (empty($doctrineChangeSet)) { 49 | return false; 50 | } 51 | 52 | $metaInformation = $this->metaInformationFactory->loadInformation($entity); 53 | 54 | $documentChangeSet = array(); 55 | 56 | /* Check all Solr fields on this entity and check if this field is in the change set */ 57 | foreach ($metaInformation->getFields() as $field) { 58 | if (array_key_exists($field->name, $doctrineChangeSet)) { 59 | $documentChangeSet[] = $field->name; 60 | } 61 | } 62 | 63 | return count($documentChangeSet) > 0; 64 | } 65 | 66 | /** 67 | * @param object $entity 68 | * 69 | * @return bool 70 | */ 71 | protected function isNested($entity) 72 | { 73 | $metaInformation = $this->metaInformationFactory->loadInformation($entity); 74 | 75 | return $metaInformation->isNested(); 76 | } 77 | 78 | /** 79 | * @param object $entity 80 | * 81 | * @return bool 82 | */ 83 | protected function isAbleToIndex($entity) 84 | { 85 | try { 86 | $metaInformation = $this->metaInformationFactory->loadInformation($entity); 87 | } catch (SolrMappingException $e) { 88 | return false; 89 | } 90 | 91 | return true; 92 | } 93 | } -------------------------------------------------------------------------------- /Tests/Doctrine/Annotation/FieldTest.php: -------------------------------------------------------------------------------- 1 | 'test', 'type' => 'string')); 15 | $this->assertEquals('test_s', $field->getNameWithAlias()); 16 | } 17 | 18 | public function testGetNameWithAlias_Text() 19 | { 20 | $field = new Field(array('name' => 'test', 'type' => 'text')); 21 | $this->assertEquals('test_t', $field->getNameWithAlias()); 22 | } 23 | 24 | public function testGetNameWithAlias_Date() 25 | { 26 | $field = new Field(array('name' => 'test', 'type' => 'date')); 27 | $this->assertEquals('test_dt', $field->getNameWithAlias()); 28 | } 29 | 30 | public function testGetNameWithAlias_Boolean() 31 | { 32 | $field = new Field(array('name' => 'test', 'type' => 'boolean')); 33 | $this->assertEquals('test_b', $field->getNameWithAlias()); 34 | } 35 | 36 | public function testGetNameWithAlias_NoFieldType() 37 | { 38 | $field = new Field(array('name' => 'title')); 39 | $this->assertEquals('title', $field->getNameWithAlias()); 40 | } 41 | 42 | public function testGetNameWithAlias_Integer() 43 | { 44 | $field = new Field(array('name' => 'test', 'type' => 'integer')); 45 | $this->assertEquals('test_i', $field->getNameWithAlias()); 46 | } 47 | 48 | public function testNormalizeName_CamelCase() 49 | { 50 | $field = new Field(array('name' => 'testCamelCase', 'type' => 'string')); 51 | 52 | $meta = new \ReflectionClass($field); 53 | $method = $meta->getMethod('normalizeName'); 54 | $method->setAccessible(true); 55 | $result = $method->invoke($field, $field->name); 56 | 57 | $this->assertEquals('test_camel_case', $result); 58 | } 59 | 60 | public function testNormalizeName_Underscore() 61 | { 62 | $field = new Field(array('name' => 'test_underscore', 'type' => 'string')); 63 | 64 | $meta = new \ReflectionClass($field); 65 | $method = $meta->getMethod('normalizeName'); 66 | $method->setAccessible(true); 67 | $result = $method->invoke($field, $field->name); 68 | 69 | $this->assertEquals('test_underscore', $result); 70 | } 71 | 72 | public function testCostomFieldType() 73 | { 74 | $field = new Field(array('name' => 'costumtype', 'type' => 'my_special_type')); 75 | 76 | $this->assertEquals('costumtype', $field->getNameWithAlias()); 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /Doctrine/ClassnameResolver/ClassnameResolver.php: -------------------------------------------------------------------------------- 1 | knownNamespaceAliases = $knownNamespaceAliases; 22 | } 23 | 24 | /** 25 | * @param string $entityAlias 26 | * 27 | * @return string 28 | * 29 | * @throws ClassnameResolverException if the entityAlias could not find in any configured namespace or the class 30 | * does not exist 31 | */ 32 | public function resolveFullQualifiedClassname($entityAlias) 33 | { 34 | $entityNamespaceAlias = $this->getNamespaceAlias($entityAlias); 35 | 36 | if ($this->knownNamespaceAliases->isKnownNamespaceAlias($entityNamespaceAlias) === false) { 37 | $e = ClassnameResolverException::fromKnownNamespaces( 38 | $entityNamespaceAlias, 39 | $this->knownNamespaceAliases->getAllNamespaceAliases() 40 | ); 41 | 42 | throw $e; 43 | } 44 | 45 | $foundNamespace = $this->knownNamespaceAliases->getFullyQualifiedNamespace($entityNamespaceAlias); 46 | 47 | $realClassName = $this->getFullyQualifiedClassname($foundNamespace, $entityAlias); 48 | if (class_exists($realClassName) === false) { 49 | throw new ClassnameResolverException(sprintf('class %s does not exist', $realClassName)); 50 | } 51 | 52 | return $realClassName; 53 | } 54 | 55 | /** 56 | * @param string $entity 57 | * 58 | * @return string 59 | */ 60 | public function getNamespaceAlias($entity) 61 | { 62 | list($namespaceAlias, $simpleClassName) = explode(':', $entity); 63 | 64 | return $namespaceAlias; 65 | } 66 | 67 | /** 68 | * @param string $entity 69 | * 70 | * @return string 71 | */ 72 | public function getClassname($entity) 73 | { 74 | list($namespaceAlias, $simpleClassName) = explode(':', $entity); 75 | 76 | return $simpleClassName; 77 | } 78 | 79 | /** 80 | * @param string $namespace 81 | * @param string $entityAlias 82 | * 83 | * @return string 84 | */ 85 | private function getFullyQualifiedClassname($namespace, $entityAlias) 86 | { 87 | $realClassName = $namespace . '\\' . $this->getClassname($entityAlias); 88 | 89 | return $realClassName; 90 | } 91 | 92 | 93 | } 94 | -------------------------------------------------------------------------------- /Client/Solarium/SolariumMulticoreClient.php: -------------------------------------------------------------------------------- 1 | solariumClient = $solariumClient; 28 | } 29 | 30 | /** 31 | * @param DocumentInterface $doc 32 | * @param string $index 33 | */ 34 | public function update(DocumentInterface $doc, $index) 35 | { 36 | $update = $this->solariumClient->createUpdate(); 37 | $update->addDocument($doc); 38 | $update->addCommit(); 39 | 40 | $this->applyQuery($update, $index); 41 | } 42 | 43 | /** 44 | * @param DocumentInterface $document 45 | * @param string $index 46 | */ 47 | public function delete(DocumentInterface $document, $index) 48 | { 49 | $documentFields = $document->getFields(); 50 | $documentKey = $documentFields[MetaInformationInterface::DOCUMENT_KEY_FIELD_NAME]; 51 | 52 | $deleteQuery = new DeleteDocumentQuery(); 53 | $deleteQuery->setDocument($document); 54 | $deleteQuery->setDocumentKey($documentKey); 55 | 56 | $delete = $this->solariumClient->createUpdate(); 57 | $delete->addDeleteQuery($deleteQuery->getQuery()); 58 | $delete->addCommit(); 59 | 60 | $this->applyQuery($delete, $index); 61 | } 62 | 63 | /** 64 | * Runs a *:* delete query on all cores 65 | */ 66 | public function clearCores() 67 | { 68 | $delete = $this->solariumClient->createUpdate(); 69 | $delete->addDeleteQuery('*:*'); 70 | $delete->addCommit(); 71 | 72 | $this->applyOnAllCores($delete); 73 | } 74 | 75 | /** 76 | * @param QueryInterface $query 77 | * @param string $index 78 | */ 79 | private function applyQuery(QueryInterface $query, $index) 80 | { 81 | if ($index == '*') { 82 | $this->applyOnAllCores($query); 83 | } else { 84 | $this->solariumClient->update($query, $index); 85 | } 86 | } 87 | 88 | /** 89 | * @param QueryInterface $query 90 | */ 91 | private function applyOnAllCores(QueryInterface $query) 92 | { 93 | foreach ($this->solariumClient->getEndpoints() as $endpointName => $endpoint) { 94 | $this->solariumClient->update($query, $endpointName); 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /Resources/config/log_listener.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | FS\SolrBundle\Event\Listener\InsertLogListener 9 | FS\SolrBundle\Event\Listener\UpdateLogListener 10 | FS\SolrBundle\Event\Listener\DeleteLogListener 11 | FS\SolrBundle\Event\Listener\ErrorLogListener 12 | FS\SolrBundle\Event\Listener\ClearIndexLogListener 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Client/Solarium/SolariumClientBuilder.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 37 | $this->eventDispatcher = $eventDispatcher; 38 | } 39 | 40 | /** 41 | * @param string $pluginName 42 | * @param AbstractPlugin $plugin 43 | */ 44 | public function addPlugin($pluginName, AbstractPlugin $plugin) 45 | { 46 | $this->plugins[$pluginName] = $plugin; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | * 52 | * @return Client 53 | */ 54 | public function build() 55 | { 56 | $settings = []; 57 | foreach ($this->settings as $name => $options) { 58 | if (isset($options['dsn'])) { 59 | unset( 60 | $options['scheme'], 61 | $options['host'], 62 | $options['port'], 63 | $options['path'] 64 | ); 65 | 66 | $parsedDsn = parse_url($options['dsn']); 67 | unset($options['dsn']); 68 | if ($parsedDsn) { 69 | $options['scheme'] = isset($parsedDsn['scheme']) ? $parsedDsn['scheme'] : 'http'; 70 | if (isset($parsedDsn['host'])) { 71 | $options['host'] = $parsedDsn['host']; 72 | } 73 | if (isset($parsedDsn['user'])) { 74 | $auth = $parsedDsn['user'] . (isset($parsedDsn['pass']) ? ':' . $parsedDsn['pass'] : ''); 75 | $options['host'] = $auth . '@' . $options['host']; 76 | } 77 | $options['port'] = isset($parsedDsn['port']) ? $parsedDsn['port'] : 80; 78 | $options['path'] = isset($parsedDsn['path']) ? $parsedDsn['path'] : ''; 79 | } 80 | } 81 | 82 | $settings[$name] = $options; 83 | } 84 | 85 | $solariumClient = new Client(array('endpoint' => $settings), $this->eventDispatcher); 86 | foreach ($this->plugins as $pluginName => $plugin) { 87 | $solariumClient->registerPlugin($pluginName, $plugin); 88 | } 89 | 90 | return $solariumClient; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /DataCollector/RequestCollector.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function collect(Request $request, Response $response, \Exception $exception = null) 31 | { 32 | $this->data = [ 33 | 'queries' => array_map(function ($query) { 34 | return $this->parseQuery($query); 35 | }, $this->logger->getQueries()) 36 | ]; 37 | } 38 | 39 | /** 40 | * @return int 41 | */ 42 | public function getQueryCount() 43 | { 44 | return count($this->data['queries']); 45 | } 46 | 47 | /** 48 | * @return array 49 | */ 50 | public function getQueries() 51 | { 52 | return $this->data['queries']; 53 | } 54 | 55 | /** 56 | * @return int 57 | */ 58 | public function getTime() 59 | { 60 | $time = 0; 61 | foreach ($this->data['queries'] as $query) { 62 | $time += $query['executionMS']; 63 | } 64 | 65 | return $time; 66 | } 67 | 68 | /** 69 | * @param array $request 70 | * 71 | * @return array 72 | */ 73 | public function parseQuery($request) 74 | { 75 | list($endpoint, $params) = explode('?', $request['request']['uri']); 76 | 77 | $request['endpoint'] = $endpoint; 78 | $request['params'] = $params; 79 | $request['method'] = $request['request']['method']; 80 | $request['raw_data'] = $request['request']['raw_data']; 81 | 82 | if (class_exists(VarCloner::class)) { 83 | $varCloner = new VarCloner(); 84 | 85 | parse_str($params, $stub); 86 | $request['stub'] = Kernel::VERSION_ID >= 30200 ? $varCloner->cloneVar($stub) : $stub; 87 | } 88 | 89 | return $request; 90 | } 91 | 92 | /** 93 | * @param DebugLogger $logger 94 | */ 95 | public function setLogger(DebugLogger $logger) 96 | { 97 | $this->logger = $logger; 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function getName() 104 | { 105 | return 'solr'; 106 | } 107 | 108 | /** 109 | * {@inheritdoc} 110 | */ 111 | public function reset() 112 | { 113 | $this->data = [ 114 | 'queries' => [], 115 | ]; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Query/AbstractQuery.php: -------------------------------------------------------------------------------- 1 | metaInformation; 46 | } 47 | 48 | /** 49 | * @param MetaInformationInterface $metaInformation 50 | */ 51 | public function setMetaInformation($metaInformation) 52 | { 53 | $this->metaInformation = $metaInformation; 54 | 55 | $this->entity = $metaInformation->getClassName(); 56 | $this->index = $metaInformation->getIndex(); 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function getEntity() 63 | { 64 | return $this->entity; 65 | } 66 | 67 | /** 68 | * @param string $entity 69 | */ 70 | public function setEntity($entity) 71 | { 72 | $this->entity = $entity; 73 | } 74 | 75 | /** 76 | * @param Document $document 77 | */ 78 | public function setDocument($document) 79 | { 80 | $this->document = $document; 81 | } 82 | 83 | /** 84 | * @return Document 85 | */ 86 | public function getDocument() 87 | { 88 | return $this->document; 89 | } 90 | 91 | /** 92 | * @param SolrInterface $solr 93 | */ 94 | public function setSolr(SolrInterface $solr) 95 | { 96 | $this->solr = $solr; 97 | } 98 | 99 | /** 100 | * @return SolrInterface 101 | */ 102 | public function getSolr() 103 | { 104 | return $this->solr; 105 | } 106 | 107 | /** 108 | * modes defined in FS\SolrBundle\Doctrine\Hydration\HydrationModes 109 | * 110 | * @param string $mode 111 | */ 112 | public function setHydrationMode($mode) 113 | { 114 | $this->getSolr()->getMapper()->setHydrationMode($mode); 115 | } 116 | 117 | /** 118 | * @return string 119 | */ 120 | public function getIndex() 121 | { 122 | return $this->index; 123 | } 124 | 125 | /** 126 | * @param string $index 127 | */ 128 | public function setIndex($index) 129 | { 130 | $this->index = $index; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Tests/SolrClientFake.php: -------------------------------------------------------------------------------- 1 | mapper; 28 | } 29 | 30 | public function getCommandFactory() 31 | { 32 | return $this->commandFactory; 33 | } 34 | 35 | public function getMetaFactory() 36 | { 37 | return $this->metaFactory; 38 | } 39 | 40 | public function addDocument($doc): bool 41 | { 42 | return true; 43 | } 44 | 45 | public function deleteByQuery($query) 46 | { 47 | } 48 | 49 | public function commit() 50 | { 51 | $this->commit = true; 52 | } 53 | 54 | public function isCommited() 55 | { 56 | return $this->commit; 57 | } 58 | 59 | public function query(AbstractQuery $query): array 60 | { 61 | $this->query = $query; 62 | 63 | return $this->response; 64 | } 65 | 66 | public function setResponse(SolrResponseFake $response) 67 | { 68 | $this->response = $response; 69 | } 70 | 71 | public function getOptions() 72 | { 73 | return array(); 74 | } 75 | 76 | public function createQuery($entity) 77 | { 78 | $metaInformation = $this->metaFactory->loadInformation($entity); 79 | $class = $metaInformation->getClassName(); 80 | $entity = new $class; 81 | 82 | $query = new SolrQuery(); 83 | $query->setSolr($this); 84 | $query->setEntity($entity); 85 | $query->setIndex($metaInformation->getIndex()); 86 | $query->setMetaInformation($metaInformation); 87 | $query->setMappedFields($metaInformation->getFieldMapping()); 88 | 89 | return $query; 90 | } 91 | 92 | public function removeDocument($entity) 93 | { 94 | // TODO: Implement removeDocument() method. 95 | } 96 | 97 | public function updateDocument($entity): bool 98 | { 99 | // TODO: Implement updateDocument() method. 100 | } 101 | 102 | public function getRepository($entity): RepositoryInterface 103 | { 104 | // TODO: Implement getRepository() method. 105 | } 106 | 107 | public function computeChangeSet(array $doctrineChangeSet, $entity) 108 | { 109 | // TODO: Implement computeChangeSet() method. 110 | } 111 | 112 | public function createQueryBuilder($entity): QueryBuilderInterface 113 | { 114 | // TODO: Implement createQueryBuilder() method. 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Doctrine/Mapper/EntityMapper.php: -------------------------------------------------------------------------------- 1 | doctrineHydrator = $doctrineHydrator; 46 | $this->indexHydrator = $indexHydrator; 47 | $this->metaInformationFactory = $metaInformationFactory; 48 | $this->documentFactory = new DocumentFactory($metaInformationFactory); 49 | 50 | $this->hydrationMode = HydrationModes::HYDRATE_DOCTRINE; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function toDocument(MetaInformationInterface $metaInformation) 57 | { 58 | return $this->documentFactory->createDocument($metaInformation); 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function toEntity(\ArrayAccess $document, $sourceTargetEntity) 65 | { 66 | if (null === $sourceTargetEntity) { 67 | throw new SolrMappingException('$sourceTargetEntity should not be null'); 68 | } 69 | 70 | $metaInformation = $this->metaInformationFactory->loadInformation($sourceTargetEntity); 71 | 72 | if ($metaInformation->isDoctrineEntity() === false && $this->hydrationMode == HydrationModes::HYDRATE_DOCTRINE) { 73 | throw new SolrMappingException(sprintf('Please check your config. Given entity is not a Doctrine entity, but Doctrine hydration is enabled. Use setHydrationMode(HydrationModes::HYDRATE_DOCTRINE) to fix this.')); 74 | } 75 | 76 | if ($this->hydrationMode == HydrationModes::HYDRATE_INDEX) { 77 | return $this->indexHydrator->hydrate($document, $metaInformation); 78 | } 79 | 80 | if ($this->hydrationMode == HydrationModes::HYDRATE_DOCTRINE) { 81 | return $this->doctrineHydrator->hydrate($document, $metaInformation); 82 | } 83 | } 84 | 85 | /** 86 | * @param string $mode 87 | */ 88 | public function setHydrationMode($mode) 89 | { 90 | $this->hydrationMode = $mode; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/Doctrine/ClassnameResolver/KnownNamespaceAliasesTest.php: -------------------------------------------------------------------------------- 1 | createMock(OrmConfiguration::class); 19 | $config1->expects($this->once()) 20 | ->method('getEntityNamespaces') 21 | ->will($this->returnValue(array('AcmeDemoBundle'))); 22 | 23 | $config2 = $this->createMock(OrmConfiguration::class); 24 | $config2->expects($this->once()) 25 | ->method('getEntityNamespaces') 26 | ->will($this->returnValue(array('AcmeBlogBundle'))); 27 | 28 | $knownAliases = new KnownNamespaceAliases(); 29 | $knownAliases->addEntityNamespaces($config1); 30 | $knownAliases->addEntityNamespaces($config2); 31 | 32 | $this->assertTrue(in_array('AcmeDemoBundle', $knownAliases->getAllNamespaceAliases())); 33 | $this->assertTrue(in_array('AcmeBlogBundle', $knownAliases->getAllNamespaceAliases())); 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function addAliasFromMultipleOdmConfigurations() 40 | { 41 | $config1 = $this->createMock(OdmConfiguration::class); 42 | $config1->expects($this->once()) 43 | ->method('getDocumentNamespaces') 44 | ->will($this->returnValue(array('AcmeDemoBundle'))); 45 | 46 | $config2 = $this->createMock(OdmConfiguration::class); 47 | $config2->expects($this->once()) 48 | ->method('getDocumentNamespaces') 49 | ->will($this->returnValue(array('AcmeBlogBundle'))); 50 | 51 | $knownAliases = new KnownNamespaceAliases(); 52 | $knownAliases->addDocumentNamespaces($config1); 53 | $knownAliases->addDocumentNamespaces($config2); 54 | 55 | $this->assertTrue(in_array('AcmeDemoBundle', $knownAliases->getAllNamespaceAliases())); 56 | $this->assertTrue(in_array('AcmeBlogBundle', $knownAliases->getAllNamespaceAliases())); 57 | } 58 | 59 | /** 60 | * @test 61 | */ 62 | public function knowAliasHasAValidNamespace() 63 | { 64 | $config1 = $this->createMock(OdmConfiguration::class); 65 | $config1->expects($this->once()) 66 | ->method('getDocumentNamespaces') 67 | ->will($this->returnValue(array('AcmeDemoBundle' => 'Acme\DemoBundle\Document'))); 68 | 69 | $config2 = $this->createMock(OdmConfiguration::class); 70 | $config2->expects($this->once()) 71 | ->method('getDocumentNamespaces') 72 | ->will($this->returnValue(array('AcmeBlogBundle' => 'Acme\BlogBundle\Document'))); 73 | 74 | $knownAliases = new KnownNamespaceAliases(); 75 | $knownAliases->addDocumentNamespaces($config1); 76 | $knownAliases->addDocumentNamespaces($config2); 77 | 78 | $this->assertEquals('Acme\DemoBundle\Document', $knownAliases->getFullyQualifiedNamespace('AcmeDemoBundle')); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/Fixtures/ValidTestEntityWithRelation.php: -------------------------------------------------------------------------------- 1 | id; 63 | } 64 | 65 | public function setId($id) 66 | { 67 | $this->id = $id; 68 | } 69 | 70 | /** 71 | * @param string $costomField 72 | */ 73 | public function setCostomField($costomField) 74 | { 75 | $this->costomField = $costomField; 76 | } 77 | 78 | /** 79 | * @return string 80 | */ 81 | public function getCostomField() 82 | { 83 | return $this->costomField; 84 | } 85 | 86 | /** 87 | * @return object 88 | */ 89 | public function getRelation() 90 | { 91 | return $this->relation; 92 | } 93 | 94 | /** 95 | * @param object $relation 96 | */ 97 | public function setRelation($relation) 98 | { 99 | $this->relation = $relation; 100 | } 101 | 102 | /** 103 | * @return object 104 | */ 105 | public function getPosts() 106 | { 107 | return $this->posts; 108 | } 109 | 110 | /** 111 | * @param object $posts 112 | */ 113 | public function setPosts($posts) 114 | { 115 | $this->posts = $posts; 116 | } 117 | 118 | /** 119 | * @return string 120 | */ 121 | public function getText() 122 | { 123 | return $this->text; 124 | } 125 | 126 | /** 127 | * @param string $text 128 | */ 129 | public function setText($text) 130 | { 131 | $this->text = $text; 132 | } 133 | 134 | /** 135 | * @return string 136 | */ 137 | public function getTitle() 138 | { 139 | return $this->title; 140 | } 141 | 142 | /** 143 | * @param string $title 144 | */ 145 | public function setTitle($title) 146 | { 147 | $this->title = $title; 148 | } 149 | 150 | /** 151 | * @return \DateTime 152 | */ 153 | public function getCreatedAt() 154 | { 155 | return $this->created_at; 156 | } 157 | 158 | /** 159 | * @param \DateTime $created_at 160 | */ 161 | public function setCreatedAt($created_at) 162 | { 163 | $this->created_at = $created_at; 164 | } 165 | } 166 | 167 | -------------------------------------------------------------------------------- /Tests/Fixtures/EntityNestedProperty.php: -------------------------------------------------------------------------------- 1 | id; 70 | } 71 | 72 | /** 73 | * @param mixed $id 74 | */ 75 | public function setId($id) 76 | { 77 | $this->id = $id; 78 | } 79 | 80 | public function sliceCollection() 81 | { 82 | return [$this->collectionValidGetter[0]]; 83 | } 84 | 85 | /** 86 | * @param string $name 87 | */ 88 | public function setName($name) 89 | { 90 | $this->name = $name; 91 | } 92 | 93 | /** 94 | 95 | /** 96 | * @param array $collection 97 | */ 98 | public function setCollection($collection) 99 | { 100 | $this->collection = $collection; 101 | } 102 | 103 | /** 104 | * @param object $nestedProperty 105 | */ 106 | public function setNestedProperty($nestedProperty) 107 | { 108 | $this->nestedProperty = $nestedProperty; 109 | } 110 | 111 | /** 112 | * @param array $collectionValidGetter 113 | */ 114 | public function setCollectionValidGetter($collectionValidGetter) 115 | { 116 | $this->collectionValidGetter = $collectionValidGetter; 117 | } 118 | 119 | /** 120 | * @param array $collectionInvalidGetter 121 | */ 122 | public function setCollectionInvalidGetter($collectionInvalidGetter) 123 | { 124 | $this->collectionInvalidGetter = $collectionInvalidGetter; 125 | } 126 | 127 | /** 128 | * @param mixed $objectToSimpleFormat 129 | */ 130 | public function setGetterWithParameters($getterWithParameters) 131 | { 132 | $this->getterWithParameters = $getterWithParameters; 133 | } 134 | 135 | /** 136 | * @param mixed $simpleGetter 137 | */ 138 | public function setSimpleGetter($simpleGetter) 139 | { 140 | $this->simpleGetter = $simpleGetter; 141 | } 142 | } -------------------------------------------------------------------------------- /Tests/Doctrine/ClassnameResolver/ClassnameResolverTest.php: -------------------------------------------------------------------------------- 1 | knownAliases = $this->createMock(KnownNamespaceAliases::class); 22 | } 23 | 24 | /** 25 | * @test 26 | */ 27 | public function resolveClassnameOfCommonEntity() 28 | { 29 | $resolver = $this->getResolverWithKnowNamespace(self::ENTITY_NAMESPACE); 30 | 31 | $this->assertEquals(ValidTestEntity::class, $resolver->resolveFullQualifiedClassname('FSTest:ValidTestEntity')); 32 | } 33 | 34 | /** 35 | * @test 36 | * @expectedException \FS\SolrBundle\Doctrine\ClassnameResolver\ClassnameResolverException 37 | */ 38 | public function cantResolveClassnameFromUnknowClassWithValidNamespace() 39 | { 40 | $resolver = $this->getResolverWithOrmAndOdmConfigBothHasEntity(self::ENTITY_NAMESPACE); 41 | 42 | $resolver->resolveFullQualifiedClassname('FSTest:UnknownEntity'); 43 | } 44 | 45 | /** 46 | * @test 47 | * @expectedException \FS\SolrBundle\Doctrine\ClassnameResolver\ClassnameResolverException 48 | */ 49 | public function cantResolveClassnameIfEntityNamespaceIsUnknown() 50 | { 51 | $resolver = $this->getResolverWithOrmConfigPassedInvalidNamespace(self::UNKNOW_ENTITY_NAMESPACE); 52 | 53 | $resolver->resolveFullQualifiedClassname('FStest:entity'); 54 | } 55 | 56 | /** 57 | * both has a namespace 58 | * 59 | * @param string $knownNamespace 60 | * @return ClassnameResolver 61 | */ 62 | private function getResolverWithOrmAndOdmConfigBothHasEntity($knownNamespace) 63 | { 64 | $this->knownAliases->expects($this->once()) 65 | ->method('isKnownNamespaceAlias') 66 | ->will($this->returnValue(true)); 67 | 68 | $this->knownAliases->expects($this->once()) 69 | ->method('getFullyQualifiedNamespace') 70 | ->will($this->returnValue($knownNamespace)); 71 | 72 | $resolver = new ClassnameResolver($this->knownAliases); 73 | 74 | return $resolver; 75 | } 76 | 77 | private function getResolverWithOrmConfigPassedInvalidNamespace($knownNamespace) 78 | { 79 | $this->knownAliases->expects($this->once()) 80 | ->method('isKnownNamespaceAlias') 81 | ->will($this->returnValue(false)); 82 | 83 | $this->knownAliases->expects($this->once()) 84 | ->method('getAllNamespaceAliases') 85 | ->will($this->returnValue(array('FSTest'))); 86 | 87 | $resolver = new ClassnameResolver($this->knownAliases); 88 | 89 | return $resolver; 90 | } 91 | 92 | private function getResolverWithKnowNamespace($knownNamespace) 93 | { 94 | $this->knownAliases->expects($this->once()) 95 | ->method('isKnownNamespaceAlias') 96 | ->will($this->returnValue(true)); 97 | 98 | $this->knownAliases->expects($this->once()) 99 | ->method('getFullyQualifiedNamespace') 100 | ->will($this->returnValue($knownNamespace)); 101 | 102 | $resolver = new ClassnameResolver($this->knownAliases); 103 | 104 | return $resolver; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Tests/Fixtures/ValidTestEntityWithCollection.php: -------------------------------------------------------------------------------- 1 | id; 67 | } 68 | 69 | public function setId($id) 70 | { 71 | $this->id = $id; 72 | } 73 | 74 | /** 75 | * @param string $costomField 76 | */ 77 | public function setCostomField($costomField) 78 | { 79 | $this->costomField = $costomField; 80 | } 81 | 82 | /** 83 | * @return string 84 | */ 85 | public function getCostomField() 86 | { 87 | return $this->costomField; 88 | } 89 | 90 | /** 91 | * @return ArrayCollection 92 | */ 93 | public function getCollection() 94 | { 95 | return $this->collection; 96 | } 97 | 98 | /** 99 | * @param ArrayCollection $collection 100 | */ 101 | public function setCollection($collection) 102 | { 103 | $this->collection = $collection; 104 | } 105 | 106 | /** 107 | * @return string 108 | */ 109 | public function getText() 110 | { 111 | return $this->text; 112 | } 113 | 114 | /** 115 | * @param string $text 116 | */ 117 | public function setText($text) 118 | { 119 | $this->text = $text; 120 | } 121 | 122 | /** 123 | * @return string 124 | */ 125 | public function getTitle() 126 | { 127 | return $this->title; 128 | } 129 | 130 | /** 131 | * @param string $title 132 | */ 133 | public function setTitle($title) 134 | { 135 | $this->title = $title; 136 | } 137 | 138 | /** 139 | * @return \DateTime 140 | */ 141 | public function getCreatedAt() 142 | { 143 | return $this->created_at; 144 | } 145 | 146 | /** 147 | * @param \DateTime $created_at 148 | */ 149 | public function setCreatedAt($created_at) 150 | { 151 | $this->created_at = $created_at; 152 | } 153 | 154 | /** 155 | * @return ArrayCollection 156 | */ 157 | public function getCollectionNoGetter() 158 | { 159 | return $this->collectionNoGetter; 160 | } 161 | 162 | /** 163 | * @param ArrayCollection $collectionNoGetter 164 | */ 165 | public function setCollectionNoGetter(ArrayCollection $collectionNoGetter) 166 | { 167 | $this->collectionNoGetter = $collectionNoGetter; 168 | } 169 | } 170 | 171 | -------------------------------------------------------------------------------- /Doctrine/Mapper/MetaInformationInterface.php: -------------------------------------------------------------------------------- 1 | exampleentity 57 | * 58 | * @return string 59 | */ 60 | public function getDocumentName(); 61 | 62 | /** 63 | * @return Field[] 64 | */ 65 | public function getFields(); 66 | 67 | /** 68 | * Returns full qualified classname of repository-class 69 | * 70 | * @return string 71 | */ 72 | public function getRepository(); 73 | 74 | /** 75 | * Source/target entity instance 76 | * 77 | * @return object 78 | */ 79 | public function getEntity(); 80 | 81 | /** 82 | * @param string $fieldName 83 | * 84 | * @return Field|null 85 | * 86 | * @throws \InvalidArgumentException if given $fieldName is unknown 87 | */ 88 | public function getField($fieldName); 89 | 90 | /** 91 | * @return array 92 | */ 93 | public function getFieldMapping(); 94 | 95 | /** 96 | * The document boost value 97 | * 98 | * @return number 99 | */ 100 | public function getBoost(); 101 | 102 | /** 103 | * @return string 104 | */ 105 | public function getSynchronizationCallback(); 106 | 107 | /** 108 | * @return boolean 109 | */ 110 | public function hasSynchronizationFilter(); 111 | 112 | /** 113 | * Returns the configured index argument in FS\SolrBundle\Doctrine\Annotation\Document or the returns value of the index-handler callback 114 | * 115 | * @return string 116 | */ 117 | public function getIndex(); 118 | 119 | /** 120 | * Returns combination of DOCUMENT_KEY_FIELD_NAME and entity-id 121 | * 122 | * @return string 123 | */ 124 | public function getDocumentKey(); 125 | 126 | /** 127 | * The property which has the FS\SolrBundle\Doctrine\Annotation\Id annotation 128 | * 129 | * @return string 130 | */ 131 | public function getIdentifierFieldName(); 132 | 133 | /** 134 | * Returns MetaInformationInterface::DOCTRINE_MAPPER_TYPE_DOCUMENT if target is an doctrine-odm object or 135 | * MetaInformationInterface::DOCTRINE_MAPPER_TYPE_RELATIONAL if it is an doctrine-orm object, otherwise an empty string 136 | * 137 | * @return string 138 | */ 139 | public function getDoctrineMapperType(); 140 | 141 | /** 142 | * @return bool 143 | */ 144 | public function isNested(); 145 | } -------------------------------------------------------------------------------- /Doctrine/ORM/Listener/EntityIndexerSubscriber.php: -------------------------------------------------------------------------------- 1 | getEntity(); 44 | 45 | if ($this->isAbleToIndex($entity) === false) { 46 | return; 47 | } 48 | 49 | $doctrineChangeSet = $args->getEntityManager()->getUnitOfWork()->getEntityChangeSet($entity); 50 | try { 51 | if ($this->hasChanged($doctrineChangeSet, $entity) === false) { 52 | return; 53 | } 54 | 55 | $this->solr->updateDocument($entity); 56 | } catch (\Exception $e) { 57 | $this->logger->debug($e->getMessage()); 58 | } 59 | } 60 | 61 | /** 62 | * @param LifecycleEventArgs $args 63 | */ 64 | public function postPersist(LifecycleEventArgs $args) 65 | { 66 | $entity = $args->getEntity(); 67 | 68 | if ($this->isAbleToIndex($entity) === false) { 69 | return; 70 | } 71 | 72 | $this->persistedEntities[] = $entity; 73 | } 74 | 75 | /** 76 | * @param LifecycleEventArgs $args 77 | */ 78 | public function preRemove(LifecycleEventArgs $args) 79 | { 80 | $entity = $args->getEntity(); 81 | 82 | if ($this->isAbleToIndex($entity) === false) { 83 | return; 84 | } 85 | 86 | if ($this->isNested($entity)) { 87 | $this->deletedNestedEntities[] = $this->emptyCollections($entity); 88 | } else { 89 | $this->deletedRootEntities[] = $this->emptyCollections($entity); 90 | } 91 | } 92 | 93 | /** 94 | * @param object $object 95 | * 96 | * @return object 97 | */ 98 | private function emptyCollections($object) 99 | { 100 | $deepcopy = new DeepCopy(); 101 | $deepcopy->addFilter(new DoctrineEmptyCollectionFilter(), new PropertyTypeMatcher('Doctrine\Common\Collections\Collection')); 102 | 103 | return $deepcopy->copy($object); 104 | } 105 | 106 | /** 107 | * @param PostFlushEventArgs $eventArgs 108 | */ 109 | public function postFlush(PostFlushEventArgs $eventArgs) 110 | { 111 | foreach ($this->persistedEntities as $entity) { 112 | $this->solr->addDocument($entity); 113 | } 114 | $this->persistedEntities = []; 115 | 116 | foreach ($this->deletedRootEntities as $entity) { 117 | $this->solr->removeDocument($entity); 118 | } 119 | $this->deletedRootEntities = []; 120 | 121 | foreach ($this->deletedNestedEntities as $entity) { 122 | $this->solr->removeDocument($entity); 123 | } 124 | $this->deletedNestedEntities = []; 125 | } 126 | } -------------------------------------------------------------------------------- /Tests/Fixtures/ValidTestEntityAllCores.php: -------------------------------------------------------------------------------- 1 | id; 70 | } 71 | 72 | /** 73 | * @param int $id 74 | */ 75 | public function setId($id) 76 | { 77 | $this->id = $id; 78 | } 79 | 80 | /** 81 | * @return string $text 82 | */ 83 | public function getText() 84 | { 85 | return $this->text; 86 | } 87 | 88 | /** 89 | * @return string $title 90 | */ 91 | public function getTitle() 92 | { 93 | return $this->title; 94 | } 95 | 96 | /** 97 | * @param string $text 98 | */ 99 | public function setText($text) 100 | { 101 | $this->text = $text; 102 | } 103 | 104 | /** 105 | * @param string $title 106 | */ 107 | public function setTitle($title) 108 | { 109 | $this->title = $title; 110 | } 111 | 112 | /** 113 | * @param string $costomField 114 | */ 115 | public function setCostomField($costomField) 116 | { 117 | $this->costomField = $costomField; 118 | } 119 | 120 | /** 121 | * @return string 122 | */ 123 | public function getCostomField() 124 | { 125 | return $this->costomField; 126 | } 127 | 128 | /** 129 | * @return \DateTime 130 | */ 131 | public function getCreatedAt() 132 | { 133 | return $this->created_at; 134 | } 135 | 136 | /** 137 | * @param \DateTime $created_at 138 | */ 139 | public function setCreatedAt($created_at) 140 | { 141 | $this->created_at = $created_at; 142 | } 143 | 144 | /** 145 | * @return ValidTestEntity[] 146 | */ 147 | public function getPosts() 148 | { 149 | return $this->posts; 150 | } 151 | 152 | /** 153 | * @param ValidTestEntity[] $posts 154 | */ 155 | public function setPosts($posts) 156 | { 157 | $this->posts = $posts; 158 | } 159 | 160 | /** 161 | * @param string $field 162 | */ 163 | public function setField($field) 164 | { 165 | $this->privateField = $field; 166 | } 167 | 168 | /** 169 | * @return string 170 | */ 171 | public function getField() 172 | { 173 | return $this->privateField; 174 | } 175 | 176 | /** 177 | * @return string 178 | */ 179 | public function getPublishDate() 180 | { 181 | return $this->publishDate; 182 | } 183 | 184 | /** 185 | * @param string $publishDate 186 | */ 187 | public function setPublishDate($publishDate) 188 | { 189 | $this->publishDate = $publishDate; 190 | } 191 | } 192 | 193 | -------------------------------------------------------------------------------- /Tests/Fixtures/ValidOdmTestDocument.php: -------------------------------------------------------------------------------- 1 | id; 71 | } 72 | 73 | /** 74 | * @param int $id 75 | */ 76 | public function setId($id) 77 | { 78 | $this->id = $id; 79 | } 80 | 81 | /** 82 | * @return string $text 83 | */ 84 | public function getText() 85 | { 86 | return $this->text; 87 | } 88 | 89 | /** 90 | * @return string $title 91 | */ 92 | public function getTitle() 93 | { 94 | return $this->title; 95 | } 96 | 97 | /** 98 | * @param string $text 99 | */ 100 | public function setText($text) 101 | { 102 | $this->text = $text; 103 | } 104 | 105 | /** 106 | * @param string $title 107 | */ 108 | public function setTitle($title) 109 | { 110 | $this->title = $title; 111 | } 112 | 113 | /** 114 | * @param string $costomField 115 | */ 116 | public function setCostomField($costomField) 117 | { 118 | $this->costomField = $costomField; 119 | } 120 | 121 | /** 122 | * @return string 123 | */ 124 | public function getCostomField() 125 | { 126 | return $this->costomField; 127 | } 128 | 129 | /** 130 | * @return \DateTime 131 | */ 132 | public function getCreatedAt() 133 | { 134 | return $this->created_at; 135 | } 136 | 137 | /** 138 | * @param \DateTime $created_at 139 | */ 140 | public function setCreatedAt($created_at) 141 | { 142 | $this->created_at = $created_at; 143 | } 144 | 145 | /** 146 | * @return ValidTestEntity[] 147 | */ 148 | public function getPosts() 149 | { 150 | return $this->posts; 151 | } 152 | 153 | /** 154 | * @param ValidTestEntity[] $posts 155 | */ 156 | public function setPosts($posts) 157 | { 158 | $this->posts = $posts; 159 | } 160 | 161 | /** 162 | * @param string $field 163 | */ 164 | public function setField($field) 165 | { 166 | $this->privateField = $field; 167 | } 168 | 169 | /** 170 | * @return string 171 | */ 172 | public function getField() 173 | { 174 | return $this->privateField; 175 | } 176 | 177 | /** 178 | * @return string 179 | */ 180 | public function getPublishDate() 181 | { 182 | return $this->publishDate; 183 | } 184 | 185 | /** 186 | * @param string $publishDate 187 | */ 188 | public function setPublishDate($publishDate) 189 | { 190 | $this->publishDate = $publishDate; 191 | } 192 | } 193 | 194 | -------------------------------------------------------------------------------- /Repository/Repository.php: -------------------------------------------------------------------------------- 1 | solr = $solr; 40 | $this->metaInformation = $metaInformation; 41 | 42 | $this->hydrationMode = HydrationModes::HYDRATE_DOCTRINE; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function find($id) 49 | { 50 | $documentKey = $this->metaInformation->getDocumentName() . '_' . $id; 51 | 52 | $query = new FindByIdentifierQuery(); 53 | $query->setIndex($this->metaInformation->getIndex()); 54 | $query->setDocumentKey($documentKey); 55 | $query->setEntity($this->metaInformation->getEntity()); 56 | $query->setSolr($this->solr); 57 | $query->setHydrationMode($this->hydrationMode); 58 | $found = $this->solr->query($query); 59 | 60 | if (count($found) == 0) { 61 | return null; 62 | } 63 | 64 | return array_pop($found); 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function findAll() 71 | { 72 | $query = new FindByDocumentNameQuery(); 73 | $query->setRows(1000000); 74 | $query->setDocumentName($this->metaInformation->getDocumentName()); 75 | $query->setIndex($this->metaInformation->getIndex()); 76 | $query->setEntity($this->metaInformation->getEntity()); 77 | $query->setSolr($this->solr); 78 | $query->setHydrationMode($this->hydrationMode); 79 | 80 | return $this->solr->query($query); 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | public function findBy(array $args) 87 | { 88 | $query = $this->solr->createQuery($this->metaInformation->getEntity()); 89 | $query->setHydrationMode($this->hydrationMode); 90 | $query->setRows(100000); 91 | $query->setUseAndOperator(true); 92 | $query->addSearchTerm('id', $this->metaInformation->getDocumentName() . '_*'); 93 | $query->setQueryDefaultField('id'); 94 | 95 | $helper = $query->getHelper(); 96 | foreach ($args as $fieldName => $fieldValue) { 97 | $fieldValue = $helper->escapeTerm($fieldValue); 98 | 99 | $query->addSearchTerm($fieldName, $fieldValue); 100 | } 101 | 102 | return $this->solr->query($query); 103 | } 104 | 105 | /** 106 | * {@inheritdoc} 107 | */ 108 | public function findOneBy(array $args) 109 | { 110 | $query = $this->solr->createQuery($this->metaInformation->getEntity()); 111 | $query->setHydrationMode($this->hydrationMode); 112 | $query->setRows(1); 113 | $query->setUseAndOperator(true); 114 | $query->addSearchTerm('id', $this->metaInformation->getDocumentName() . '_*'); 115 | $query->setQueryDefaultField('id'); 116 | 117 | $helper = $query->getHelper(); 118 | foreach ($args as $fieldName => $fieldValue) { 119 | $fieldValue = $helper->escapeTerm($fieldValue); 120 | 121 | $query->addSearchTerm($fieldName, $fieldValue); 122 | } 123 | 124 | $found = $this->solr->query($query); 125 | 126 | return array_pop($found); 127 | } 128 | 129 | /** 130 | * @return QueryBuilderInterface 131 | */ 132 | public function getQueryBuilder() 133 | { 134 | return $this->solr->getQueryBuilder($this->metaInformation->getEntity()); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Tests/Client/Solarium/SolariumClientBuilderTest.php: -------------------------------------------------------------------------------- 1 | defaultEndpoints = [ 21 | 'unittest' => [ 22 | 'schema' => 'http', 23 | 'host' => '127.0.0.1', 24 | 'port' => 8983, 25 | 'path' => '/solr', 26 | 'timeout' => 5, 27 | 'core' => null 28 | ] 29 | ]; 30 | } 31 | 32 | public function testCreateClientWithoutDsn() 33 | { 34 | $actual = $this->createClientWithSettings($this->defaultEndpoints); 35 | 36 | $endpoint = $actual->getEndpoint('unittest'); 37 | $this->assertEquals('http://127.0.0.1:8983/solr/', $endpoint->getBaseUri()); 38 | } 39 | 40 | public function testCreateClientWithoutDsnWithCore() 41 | { 42 | $this->defaultEndpoints['unittest']['core'] = 'core0'; 43 | 44 | $actual = $this->createClientWithSettings($this->defaultEndpoints); 45 | 46 | $endpoint = $actual->getEndpoint('unittest'); 47 | $this->assertEquals('http://127.0.0.1:8983/solr/core0/', $endpoint->getBaseUri()); 48 | } 49 | 50 | /** 51 | * @param string $dsn 52 | * @param string $expectedBaseUri 53 | * @param string $message 54 | * 55 | * @dataProvider dsnProvider 56 | */ 57 | public function testCreateClientWithDsn($dsn, $expectedBaseUri, $message) 58 | { 59 | $settings = $this->defaultEndpoints; 60 | $settings['unittest'] = [ 61 | 'dsn' => $dsn 62 | ]; 63 | 64 | $actual = $this->createClientWithSettings($settings); 65 | 66 | $endpoint = $actual->getEndpoint('unittest'); 67 | $this->assertEquals($expectedBaseUri, $endpoint->getBaseUri(), $message); 68 | } 69 | 70 | /** 71 | * @param string $dsn 72 | * @param string $expectedBaseUri 73 | * @param string $message 74 | * 75 | * @dataProvider dsnProvider 76 | */ 77 | public function testCreateClientWithDsnAndCore($dsn, $expectedBaseUri, $message) 78 | { 79 | $settings = $this->defaultEndpoints; 80 | $settings['unittest'] = [ 81 | 'dsn' => $dsn, 82 | 'core' => 'core0' 83 | ]; 84 | 85 | $actual = $this->createClientWithSettings($settings); 86 | 87 | $endpoint = $actual->getEndpoint('unittest'); 88 | $this->assertEquals($expectedBaseUri . 'core0/', $endpoint->getBaseUri(), $message . ' with core'); 89 | } 90 | 91 | /** 92 | * @return array 93 | */ 94 | public function dsnProvider() 95 | { 96 | return [ 97 | [ 98 | 'http://example.com:1234', 99 | 'http://example.com:1234/', 100 | 'Test DSN without path and any authentication' 101 | ], 102 | [ 103 | 'http://example.com:1234/solr', 104 | 'http://example.com:1234/solr/', 105 | 'Test DSN without any authentication' 106 | ], 107 | [ 108 | 'http://user@example.com:1234/solr', 109 | 'http://user@example.com:1234/solr/', 110 | 'Test DSN with user-only authentication' 111 | ], 112 | [ 113 | 'http://user:secret@example.com:1234/solr', 114 | 'http://user:secret@example.com:1234/solr/', 115 | 'Test DSN with authentication' 116 | ], 117 | [ 118 | 'https://example.com:1234/solr', 119 | 'https://example.com:1234/solr/', 120 | 'Test DSN with HTTPS' 121 | ] 122 | ]; 123 | } 124 | 125 | /** 126 | * @param array $settings 127 | * 128 | * @return \Solarium\Client 129 | */ 130 | private function createClientWithSettings(array $settings) 131 | { 132 | /** @var EventDispatcherInterface $eventDispatcherMock */ 133 | $eventDispatcherMock = $this->createMock(EventDispatcherInterface::class); 134 | 135 | return (new SolariumClientBuilder($settings, $eventDispatcherMock))->build(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Tests/Fixtures/ValidTestEntity.php: -------------------------------------------------------------------------------- 1 | id; 76 | } 77 | 78 | /** 79 | * @param int $id 80 | */ 81 | public function setId($id) 82 | { 83 | $this->id = $id; 84 | } 85 | 86 | /** 87 | * @return string $text 88 | */ 89 | public function getText() 90 | { 91 | return $this->text; 92 | } 93 | 94 | /** 95 | * @return string $title 96 | */ 97 | public function getTitle() 98 | { 99 | return $this->title; 100 | } 101 | 102 | /** 103 | * @param string $text 104 | */ 105 | public function setText($text) 106 | { 107 | $this->text = $text; 108 | } 109 | 110 | /** 111 | * @param string $title 112 | */ 113 | public function setTitle($title) 114 | { 115 | $this->title = $title; 116 | } 117 | 118 | /** 119 | * @param string $costomField 120 | */ 121 | public function setCostomField($costomField) 122 | { 123 | $this->costomField = $costomField; 124 | } 125 | 126 | /** 127 | * @return string 128 | */ 129 | public function getCostomField() 130 | { 131 | return $this->costomField; 132 | } 133 | 134 | /** 135 | * @return \DateTime 136 | */ 137 | public function getCreatedAt() 138 | { 139 | return $this->created_at; 140 | } 141 | 142 | /** 143 | * @param \DateTime $created_at 144 | */ 145 | public function setCreatedAt($created_at) 146 | { 147 | $this->created_at = $created_at; 148 | } 149 | 150 | /** 151 | * @return ValidTestEntity[] 152 | */ 153 | public function getPosts() 154 | { 155 | return $this->posts; 156 | } 157 | 158 | /** 159 | * @param ValidTestEntity[] $posts 160 | */ 161 | public function setPosts($posts) 162 | { 163 | $this->posts = $posts; 164 | } 165 | 166 | /** 167 | * @param string $field 168 | */ 169 | public function setField($field) 170 | { 171 | $this->privateField = $field; 172 | } 173 | 174 | /** 175 | * @return string 176 | */ 177 | public function getField() 178 | { 179 | return $this->privateField; 180 | } 181 | 182 | /** 183 | * @return string 184 | */ 185 | public function getPublishDate() 186 | { 187 | return $this->publishDate; 188 | } 189 | 190 | /** 191 | * @param string $publishDate 192 | */ 193 | public function setPublishDate($publishDate) 194 | { 195 | $this->publishDate = $publishDate; 196 | } 197 | 198 | /** 199 | * @return array 200 | */ 201 | public function getComplexDataType() 202 | { 203 | return $this->complexDataType; 204 | } 205 | 206 | /** 207 | * @param string $complexDataType 208 | */ 209 | public function setComplexDataType($complexDataType) 210 | { 211 | $this->complexDataType = $complexDataType; 212 | } 213 | 214 | public function getComplexData() 215 | { 216 | return json_decode($this->complexDataType, true); 217 | } 218 | } 219 | 220 | -------------------------------------------------------------------------------- /Doctrine/Annotation/Field.php: -------------------------------------------------------------------------------- 1 | '_s', 55 | 'text' => '_t', 56 | 'date' => '_dt', 57 | 'boolean' => '_b', 58 | 'integer' => '_i', 59 | 'long' => '_l', 60 | 'float' => '_f', 61 | 'double' => '_d', 62 | 'datetime' => '_dt', 63 | 'point' => '_p' 64 | ); 65 | 66 | /** 67 | * @var array 68 | */ 69 | private static $TYP_COMPLEX_MAPPING = array( 70 | 'doubles' => '_ds', 71 | 'floats' => '_fs', 72 | 'longs' => '_ls', 73 | 'integers' => '_is', 74 | 'booleans' => '_bs', 75 | 'dates' => '_dts', 76 | 'texts' => '_txt', 77 | 'strings' => '_ss', 78 | ); 79 | 80 | /** 81 | * returns field name with type-suffix: 82 | * 83 | * eg: title_s 84 | * 85 | * @throws \RuntimeException 86 | * 87 | * @return string 88 | */ 89 | public function getNameWithAlias() 90 | { 91 | return $this->normalizeName($this->name) . $this->getTypeSuffix($this->type); 92 | } 93 | 94 | /** 95 | * @param string $type 96 | * 97 | * @return string 98 | */ 99 | private function getTypeSuffix($type) 100 | { 101 | self::$TYP_MAPPING = array_merge(self::$TYP_COMPLEX_MAPPING, self::$TYP_SIMPLE_MAPPING); 102 | 103 | if ($type == '') { 104 | return ''; 105 | } 106 | 107 | if (!isset(self::$TYP_MAPPING[$this->type])) { 108 | return ''; 109 | } 110 | 111 | return self::$TYP_MAPPING[$this->type]; 112 | } 113 | 114 | /** 115 | * Related object getter name 116 | * 117 | * @return string 118 | */ 119 | public function getGetterName() 120 | { 121 | return $this->getter; 122 | } 123 | 124 | /** 125 | * @return string 126 | */ 127 | public function getFieldModifier() 128 | { 129 | return $this->fieldModifier; 130 | } 131 | 132 | /** 133 | * @return string 134 | */ 135 | public function getValue() 136 | { 137 | return $this->value; 138 | } 139 | 140 | /** 141 | * @return string 142 | */ 143 | public function __toString() 144 | { 145 | return $this->name; 146 | } 147 | 148 | /** 149 | * @throws \InvalidArgumentException if boost is not a number 150 | * 151 | * @return number 152 | */ 153 | public function getBoost() 154 | { 155 | if (!is_numeric($this->boost)) { 156 | throw new \InvalidArgumentException(sprintf('Invalid boost value %s', $this->boost)); 157 | } 158 | 159 | if (($boost = floatval($this->boost)) > 0) { 160 | return $boost; 161 | } 162 | 163 | return null; 164 | } 165 | 166 | /** 167 | * normalize class attributes camelcased names to underscores 168 | * (according to solr specification, document field names should 169 | * contain only lowercase characters and underscores to maintain 170 | * retro compatibility with old components). 171 | * 172 | * @param $name The field name 173 | * 174 | * @return string normalized field name 175 | */ 176 | private function normalizeName($name) 177 | { 178 | $words = preg_split('/(?=[A-Z])/', $name); 179 | $words = array_map( 180 | function ($value) { 181 | return strtolower($value); 182 | }, 183 | $words 184 | ); 185 | 186 | return implode('_', $words); 187 | } 188 | 189 | /** 190 | * @return array 191 | */ 192 | public static function getComplexFieldMapping() 193 | { 194 | return self::$TYP_COMPLEX_MAPPING; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /Tests/DependencyInjection/FSSolrExtensionTest.php: -------------------------------------------------------------------------------- 1 | container = new ContainerBuilder(); 25 | } 26 | 27 | private function enableOdmConfig() 28 | { 29 | $this->container->setParameter('doctrine_mongodb.odm.document_managers', array('default' => 'odm.default.mananger')); 30 | } 31 | 32 | private function enableOrmConfig() 33 | { 34 | $this->container->setParameter('doctrine.entity_managers', array('default' => 'orm.default.mananger')); 35 | } 36 | 37 | private function commonConfig() 38 | { 39 | return array(array( 40 | 41 | 'endpoints' => array( 42 | 'default' => array( 43 | 'host' => '192.168.178.24', 44 | 'port' => 8983, 45 | 'path' => '/solr/', 46 | ) 47 | ) 48 | )); 49 | } 50 | 51 | public function testDoctrineORMSetup() 52 | { 53 | $this->enableOrmConfig(); 54 | $config = $this->commonConfig(); 55 | 56 | $extension = new FSSolrExtension(); 57 | $extension->load($config, $this->container); 58 | 59 | $this->assertTrue($this->container->has('solr.document.orm.subscriber'), 'orm subscriber'); 60 | 61 | $this->assertDefinitionHasTag('solr.document.orm.subscriber', 'doctrine.event_subscriber'); 62 | 63 | $this->assertClassnameResolverHasOrmDefaultConfiguration(); 64 | } 65 | 66 | public function testDoctrineODMSetup() 67 | { 68 | $config = $this->commonConfig(); 69 | $this->enableOdmConfig(); 70 | 71 | $extension = new FSSolrExtension(); 72 | $extension->load($config, $this->container); 73 | 74 | $this->assertTrue($this->container->has('solr.document.odm.subscriber'), 'odm subscriber'); 75 | 76 | $this->assertDefinitionHasTag('solr.document.odm.subscriber', 'doctrine_mongodb.odm.event_subscriber'); 77 | 78 | $this->assertClassnameResolverHasOdmDefaultConfiguration(); 79 | } 80 | 81 | /** 82 | * @test 83 | */ 84 | public function solrListensToOdmAndOrmEvents() 85 | { 86 | $config = $this->commonConfig(); 87 | $this->enableOdmConfig(); 88 | $this->enableOrmConfig(); 89 | 90 | $extension = new FSSolrExtension(); 91 | $extension->load($config, $this->container); 92 | 93 | $this->assertTrue($this->container->has('solr.document.odm.subscriber'), 'odm subscriber'); 94 | $this->assertDefinitionHasTag('solr.document.odm.subscriber', 'doctrine_mongodb.odm.event_subscriber'); 95 | 96 | $this->assertTrue($this->container->has('solr.document.orm.subscriber'), 'orm subscriber'); 97 | $this->assertDefinitionHasTag('solr.document.orm.subscriber', 'doctrine.event_subscriber'); 98 | } 99 | 100 | private function assertClassnameResolverHasOrmDefaultConfiguration() 101 | { 102 | $doctrineConfiguration = $this->getReferenzIdOfCalledMethod(); 103 | 104 | $this->assertEquals('doctrine.orm.default_configuration', $doctrineConfiguration); 105 | } 106 | 107 | private function assertClassnameResolverHasOdmDefaultConfiguration() 108 | { 109 | $doctrineConfiguration = $this->getReferenzIdOfCalledMethod(); 110 | 111 | $this->assertEquals('doctrine_mongodb.odm.default_configuration', $doctrineConfiguration); 112 | } 113 | 114 | /** 115 | * @return Reference 116 | */ 117 | private function getReferenzIdOfCalledMethod() 118 | { 119 | $methodCalls = $this->container->getDefinition('solr.doctrine.classnameresolver.known_entity_namespaces')->getMethodCalls(); 120 | 121 | $firstMethodCall = $methodCalls[0]; 122 | $references = $firstMethodCall[1]; 123 | $reference = $references[0]; 124 | 125 | return $reference; 126 | } 127 | 128 | private function assertDefinitionHasTag($definition, $tag) 129 | { 130 | $tags = $this->container->getDefinition($definition)->getTags(); 131 | 132 | $this->assertTrue( 133 | $this->container->getDefinition($definition)->hasTag($tag), 134 | sprintf('%s with %s tag, has %s', $definition, $tag, print_r($tags, true)) 135 | ); 136 | } 137 | } 138 | 139 | -------------------------------------------------------------------------------- /Query/QueryBuilderInterface.php: -------------------------------------------------------------------------------- 1 | reader = new AnnotationReader(new \Doctrine\Common\Annotations\AnnotationReader()); 29 | } 30 | 31 | /** 32 | * @test 33 | */ 34 | public function documentShouldMapToEntity() 35 | { 36 | $obj = new SolrDocumentStub(array( 37 | 'id' => 'document_1', 38 | 'title_t' => 'foo', 39 | 'publish_date_s' => '10.10.2016', 40 | 'field_s' => 'value 1234', 41 | 'unknown_field_s' => 'value' 42 | )); 43 | 44 | $entity = new ValidTestEntity(); 45 | 46 | $metainformations = new MetaInformationFactory($this->reader); 47 | $metainformations = $metainformations->loadInformation($entity); 48 | 49 | $hydrator = new ValueHydrator(); 50 | $hydratedDocument = $hydrator->hydrate($obj, $metainformations); 51 | 52 | $this->assertTrue($hydratedDocument instanceof $entity); 53 | $this->assertEquals(1, $entity->getId()); 54 | $this->assertEquals('foo', $entity->getTitle()); 55 | $this->assertEquals('10.10.2016', $entity->getPublishDate()); 56 | $this->assertEquals('value 1234', $entity->getField()); 57 | } 58 | 59 | /** 60 | * @test 61 | */ 62 | public function underscoreFieldBecomeCamelCase() 63 | { 64 | $obj = new SolrDocumentStub(array( 65 | 'id' => 'document_1', 66 | 'created_at_d' => 12345 67 | )); 68 | 69 | $entity = new ValidTestEntity(); 70 | 71 | $metainformations = new MetaInformationFactory($this->reader); 72 | $metainformations = $metainformations->loadInformation($entity); 73 | 74 | $hydrator = new ValueHydrator(); 75 | $hydratedDocument = $hydrator->hydrate($obj, $metainformations); 76 | 77 | $this->assertTrue($hydratedDocument instanceof $entity); 78 | $this->assertEquals(1, $entity->getId()); 79 | $this->assertEquals(12345, $entity->getCreatedAt()); 80 | } 81 | 82 | /** 83 | * @test 84 | */ 85 | public function doNotOverwriteComplexTypes_Collection() 86 | { 87 | $obj = new SolrDocumentStub(array( 88 | 'id' => 'document_1', 89 | 'title_t' => 'foo', 90 | 'collection_ss' => array('title 1', 'title 2') 91 | )); 92 | 93 | $entity = new ValidTestEntityWithCollection(); 94 | 95 | $metainformations = new MetaInformationFactory($this->reader); 96 | $metainformations = $metainformations->loadInformation($entity); 97 | 98 | $hydrator = new ValueHydrator(); 99 | $hydratedDocument = $hydrator->hydrate($obj, $metainformations); 100 | 101 | $this->assertTrue($hydratedDocument instanceof $entity); 102 | $this->assertEquals(1, $entity->getId()); 103 | $this->assertEquals('foo', $entity->getTitle()); 104 | $this->assertEquals(array('title 1', 'title 2'), $entity->getCollection()); 105 | } 106 | 107 | /** 108 | * @test 109 | */ 110 | public function doNotOverwriteComplexTypes_Relation() 111 | { 112 | $obj = new SolrDocumentStub(array( 113 | 'id' => 'document_1', 114 | 'title_t' => 'foo', 115 | 'posts_ss' => array('title 1', 'title2') 116 | )); 117 | 118 | $entity1 = new ValidTestEntity(); 119 | $entity1->setTitle('title 1'); 120 | 121 | $entity = new ValidTestEntityWithRelation(); 122 | $entity->setRelation($entity1); 123 | 124 | $metainformations = new MetaInformationFactory($this->reader); 125 | $metainformations = $metainformations->loadInformation($entity); 126 | 127 | $hydrator = new ValueHydrator(); 128 | $hydratedDocument = $hydrator->hydrate($obj, $metainformations); 129 | 130 | $this->assertTrue($hydratedDocument instanceof $entity); 131 | $this->assertEquals(1, $entity->getId()); 132 | $this->assertEquals('foo', $entity->getTitle()); 133 | 134 | $this->assertTrue($hydratedDocument->getRelation() === $entity1); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Tests/MulticoreSolrTest.php: -------------------------------------------------------------------------------- 1 | createMock(Query::class); 25 | $updateQuery->expects($this->once()) 26 | ->method('addDocument'); 27 | 28 | $updateQuery->expects($this->once()) 29 | ->method('addCommit'); 30 | 31 | $this->solrClientFake 32 | ->expects($this->once()) 33 | ->method('createUpdate') 34 | ->will($this->returnValue($updateQuery)); 35 | 36 | return $updateQuery; 37 | } 38 | 39 | /** 40 | * @test 41 | */ 42 | public function addDocumentToAllCores() 43 | { 44 | $updateQuery = $this->assertUpdateQueryExecuted(); 45 | 46 | $this->eventDispatcher->expects($this->any()) 47 | ->method('dispatch'); 48 | 49 | $this->solrClientFake->expects($this->once()) 50 | ->method('getEndpoints') 51 | ->will($this->returnValue(array( 52 | 'core0' => array(), 53 | 'core1' => array() 54 | ))); 55 | 56 | $this->solrClientFake->expects($this->at(2)) 57 | ->method('update') 58 | ->with($updateQuery, 'core0'); 59 | 60 | $this->solrClientFake->expects($this->at(3)) 61 | ->method('update') 62 | ->with($updateQuery, 'core1'); 63 | 64 | $this->mapper->expects($this->once()) 65 | ->method('toDocument') 66 | ->will($this->returnValue(new DocumentStub())); 67 | 68 | $solr = new Solr($this->solrClientFake, $this->eventDispatcher, $this->metaFactory, $this->mapper); 69 | $solr->addDocument(new ValidTestEntityAllCores()); 70 | } 71 | 72 | /** 73 | * @test 74 | */ 75 | public function updateDocumentInAllCores() 76 | { 77 | $updateQuery = $this->assertUpdateQueryExecuted(); 78 | 79 | $this->eventDispatcher->expects($this->exactly(2)) 80 | ->method('dispatch'); 81 | 82 | $this->solrClientFake->expects($this->once()) 83 | ->method('getEndpoints') 84 | ->will($this->returnValue(array( 85 | 'core0' => array(), 86 | 'core1' => array() 87 | ))); 88 | 89 | $this->solrClientFake->expects($this->at(2)) 90 | ->method('update') 91 | ->with($updateQuery, 'core0'); 92 | 93 | $this->solrClientFake->expects($this->at(3)) 94 | ->method('update') 95 | ->with($updateQuery, 'core1'); 96 | 97 | $this->mapper->expects($this->once()) 98 | ->method('toDocument') 99 | ->will($this->returnValue(new DocumentStub())); 100 | 101 | $solr = new Solr($this->solrClientFake, $this->eventDispatcher, $this->metaFactory, $this->mapper); 102 | $solr->updateDocument(new ValidTestEntityAllCores()); 103 | } 104 | 105 | /** 106 | * @test 107 | */ 108 | public function removeDocumentFromAllCores() 109 | { 110 | $this->mapper->expects($this->once()) 111 | ->method('toDocument') 112 | ->will($this->returnValue(new DocumentStub())); 113 | 114 | $this->solrClientFake->expects($this->once()) 115 | ->method('getEndpoints') 116 | ->will($this->returnValue(array( 117 | 'core0' => array(), 118 | 'core1' => array() 119 | ))); 120 | 121 | $deleteQuery = $this->createMock(Query::class); 122 | $deleteQuery->expects($this->once()) 123 | ->method('addDeleteQuery') 124 | ->with($this->isType('string')); 125 | 126 | $deleteQuery->expects($this->once()) 127 | ->method('addCommit'); 128 | 129 | $this->solrClientFake 130 | ->expects($this->once()) 131 | ->method('createUpdate') 132 | ->will($this->returnValue($deleteQuery)); 133 | 134 | $this->solrClientFake->expects($this->exactly(2)) 135 | ->method('update'); 136 | 137 | $solr = new Solr($this->solrClientFake, $this->eventDispatcher, $this->metaFactory, $this->mapper); 138 | $solr->removeDocument(new ValidTestEntityAllCores()); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Tests/AbstractSolrTest.php: -------------------------------------------------------------------------------- 1 | metaFactory = new MetaInformationFactory(new AnnotationReader(new \Doctrine\Common\Annotations\AnnotationReader())); 36 | $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); 37 | $this->mapper = $this->getMockBuilder(EntityMapperInterface::class) 38 | ->disableOriginalConstructor() 39 | ->setMethods(array('setMappingCommand', 'toDocument', 'toEntity', 'setHydrationMode')) 40 | ->getMock(); 41 | 42 | $this->solrClientFake = $this->createMock(Client::class); 43 | 44 | $this->solr = new Solr($this->solrClientFake, $this->eventDispatcher, $this->metaFactory, $this->mapper); 45 | } 46 | 47 | protected function assertUpdateQueryExecuted($index = null) 48 | { 49 | $updateQuery = $this->createMock(UpdateQuery::class); 50 | $updateQuery->expects($this->once()) 51 | ->method('addDocument'); 52 | 53 | $updateQuery->expects($this->once()) 54 | ->method('addCommit'); 55 | 56 | $this->solrClientFake 57 | ->expects($this->once()) 58 | ->method('createUpdate') 59 | ->will($this->returnValue($updateQuery)); 60 | 61 | $this->solrClientFake 62 | ->expects($this->once()) 63 | ->method('update') 64 | ->with($updateQuery, $index); 65 | 66 | return $updateQuery; 67 | } 68 | 69 | protected function assertUpdateQueryWasNotExecuted() 70 | { 71 | $updateQuery = $this->createMock(UpdateQuery::class); 72 | $updateQuery->expects($this->never()) 73 | ->method('addDocument'); 74 | 75 | $updateQuery->expects($this->never()) 76 | ->method('addCommit'); 77 | 78 | $this->solrClientFake 79 | ->expects($this->never()) 80 | ->method('createUpdate'); 81 | } 82 | 83 | protected function assertDeleteQueryWasExecuted() 84 | { 85 | $deleteQuery = $this->createMock(UpdateQuery::class); 86 | $deleteQuery->expects($this->once()) 87 | ->method('addDeleteQuery') 88 | ->with($this->isType('string')); 89 | 90 | $deleteQuery->expects($this->once()) 91 | ->method('addCommit'); 92 | 93 | $this->solrClientFake 94 | ->expects($this->once()) 95 | ->method('createUpdate') 96 | ->will($this->returnValue($deleteQuery)); 97 | 98 | $this->solrClientFake 99 | ->expects($this->once()) 100 | ->method('update') 101 | ->with($deleteQuery); 102 | } 103 | 104 | protected function setupMetaFactoryLoadOneCompleteInformation($metaInformation = null) 105 | { 106 | if (null === $metaInformation) { 107 | $metaInformation = MetaTestInformationFactory::getMetaInformation(); 108 | } 109 | 110 | $this->metaFactory->expects($this->once()) 111 | ->method('loadInformation') 112 | ->will($this->returnValue($metaInformation)); 113 | } 114 | 115 | protected function assertQueryWasExecuted($data = array(), $index) 116 | { 117 | $selectQuery = $this->createMock(SelectQuery::class); 118 | $selectQuery->expects($this->once()) 119 | ->method('setQuery'); 120 | 121 | $queryResult = new ResultFake($data); 122 | 123 | $this->solrClientFake 124 | ->expects($this->once()) 125 | ->method('createSelect') 126 | ->will($this->returnValue($selectQuery)); 127 | 128 | $this->solrClientFake 129 | ->expects($this->once()) 130 | ->method('select') 131 | ->will($this->returnValue($queryResult)); 132 | } 133 | 134 | protected function mapOneDocument() 135 | { 136 | $this->mapper->expects($this->once()) 137 | ->method('toDocument') 138 | ->will($this->returnValue($this->createMock(DocumentInterface::class))); 139 | } 140 | } -------------------------------------------------------------------------------- /DependencyInjection/FSSolrExtension.php: -------------------------------------------------------------------------------- 1 | load('services.xml'); 21 | $loader->load('event_listener.xml'); 22 | $loader->load('log_listener.xml'); 23 | 24 | $configuration = new Configuration(); 25 | $config = $this->processConfiguration($configuration, $configs); 26 | 27 | $this->setupClients($config, $container); 28 | 29 | if (!$container->hasParameter('solr.auto_index')) { 30 | $container->setParameter('solr.auto_index', $config['auto_index']); 31 | } 32 | 33 | $this->setupDoctrineListener($config, $container); 34 | $this->setupDoctrineConfiguration($config, $container); 35 | 36 | } 37 | 38 | /** 39 | * @param array $config 40 | * @param ContainerBuilder $container 41 | */ 42 | private function setupClients(array $config, ContainerBuilder $container) 43 | { 44 | $endpoints = $config['endpoints']; 45 | 46 | $builderDefinition = $container->getDefinition('solr.client.adapter.builder'); 47 | $builderDefinition->replaceArgument(0, $endpoints); 48 | $builderDefinition->addMethodCall('addPlugin', array('request_debugger', new Reference('solr.debug.client_debugger'))); 49 | } 50 | 51 | /** 52 | * 53 | * @param array $config 54 | * @param ContainerBuilder $container 55 | */ 56 | private function setupDoctrineConfiguration(array $config, ContainerBuilder $container) 57 | { 58 | if ($this->isOrmConfigured($container)) { 59 | $entityManagers = $container->getParameter('doctrine.entity_managers'); 60 | 61 | $entityManagersNames = array_keys($entityManagers); 62 | foreach ($entityManagersNames as $entityManager) { 63 | $container->getDefinition('solr.doctrine.classnameresolver.known_entity_namespaces')->addMethodCall( 64 | 'addEntityNamespaces', 65 | array(new Reference(sprintf('doctrine.orm.%s_configuration', $entityManager))) 66 | ); 67 | } 68 | } 69 | 70 | if ($this->isODMConfigured($container)) { 71 | $documentManagers = $container->getParameter('doctrine_mongodb.odm.document_managers'); 72 | 73 | $documentManagersNames = array_keys($documentManagers); 74 | foreach ($documentManagersNames as $documentManager) { 75 | $container->getDefinition('solr.doctrine.classnameresolver.known_entity_namespaces')->addMethodCall( 76 | 'addDocumentNamespaces', 77 | array(new Reference(sprintf('doctrine_mongodb.odm.%s_configuration', $documentManager))) 78 | ); 79 | } 80 | } 81 | 82 | $container->getDefinition('solr.meta.information.factory')->addMethodCall( 83 | 'setClassnameResolver', 84 | array(new Reference('solr.doctrine.classnameresolver')) 85 | ); 86 | } 87 | 88 | /** 89 | * doctrine_orm and doctrine_mongoDB can't be used together. mongo_db wins when it is configured. 90 | * 91 | * listener-methods expecting different types of events 92 | * 93 | * @param array $config 94 | * @param ContainerBuilder $container 95 | */ 96 | private function setupDoctrineListener(array $config, ContainerBuilder $container) 97 | { 98 | $autoIndexing = $container->getParameter('solr.auto_index'); 99 | 100 | if ($autoIndexing == false) { 101 | return; 102 | } 103 | 104 | if ($this->isODMConfigured($container)) { 105 | $container->getDefinition('solr.document.odm.subscriber')->addTag('doctrine_mongodb.odm.event_subscriber'); 106 | } 107 | 108 | if ($this->isOrmConfigured($container)) { 109 | $container->getDefinition('solr.document.orm.subscriber')->addTag('doctrine.event_subscriber'); 110 | } 111 | } 112 | 113 | /** 114 | * @param ContainerBuilder $container 115 | * 116 | * @return boolean 117 | */ 118 | private function isODMConfigured(ContainerBuilder $container) 119 | { 120 | return $container->hasParameter('doctrine_mongodb.odm.document_managers'); 121 | } 122 | 123 | /** 124 | * @param ContainerBuilder $container 125 | * 126 | * @return boolean 127 | */ 128 | private function isOrmConfigured(ContainerBuilder $container) 129 | { 130 | return $container->hasParameter('doctrine.entity_managers'); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Command/ShowSchemaCommand.php: -------------------------------------------------------------------------------- 1 | setName('solr:schema:show') 20 | ->setDescription('Show configured entities and their fields'); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | protected function execute(InputInterface $input, OutputInterface $output) 27 | { 28 | $namespaces = $this->getContainer()->get('solr.doctrine.classnameresolver.known_entity_namespaces'); 29 | $metaInformationFactory = $this->getContainer()->get('solr.meta.information.factory'); 30 | 31 | foreach ($namespaces->getEntityClassnames() as $classname) { 32 | try { 33 | $metaInformation = $metaInformationFactory->loadInformation($classname); 34 | 35 | if ($metaInformation->isNested()) { 36 | continue; 37 | } 38 | } catch (SolrMappingException $e) { 39 | continue; 40 | } 41 | 42 | $nested = ''; 43 | if ($metaInformation->isNested()) { 44 | $nested = '(nested)'; 45 | } 46 | $output->writeln(sprintf('%s %s', $classname, $nested)); 47 | $output->writeln(sprintf('Documentname: %s', $metaInformation->getDocumentName())); 48 | $output->writeln(sprintf('Document Boost: %s', $metaInformation->getBoost()?$metaInformation->getBoost(): '-')); 49 | 50 | $simpleFields = $this->getSimpleFields($metaInformation); 51 | 52 | $rows = []; 53 | foreach ($simpleFields as $documentField => $property) { 54 | if ($field = $metaInformation->getField($documentField)) { 55 | $rows[] = [$property, $documentField, $field->boost]; 56 | } 57 | } 58 | $this->renderTable($output, $rows); 59 | 60 | $nestedFields = $this->getNestedFields($metaInformation); 61 | if (count($nestedFields) == 0) { 62 | return; 63 | } 64 | 65 | $output->writeln(sprintf('Fields (%s) with nested documents', count($nestedFields))); 66 | 67 | foreach ($nestedFields as $idField) { 68 | $propertyName = substr($idField, 0, strpos($idField, '.')); 69 | 70 | if ($nestedField = $metaInformation->getField($propertyName)) { 71 | $output->writeln(sprintf('Field %s contains nested class %s', $propertyName, $nestedField->nestedClass)); 72 | 73 | $nestedDocument = $metaInformationFactory->loadInformation($nestedField->nestedClass); 74 | $rows = []; 75 | foreach ($nestedDocument->getFieldMapping() as $documentField => $property) { 76 | $field = $nestedDocument->getField($documentField); 77 | 78 | if ($field === null) { 79 | continue; 80 | } 81 | 82 | $rows[] = [$property, $documentField, $field->boost]; 83 | } 84 | 85 | $this->renderTable($output, $rows); 86 | } 87 | } 88 | 89 | } 90 | 91 | } 92 | 93 | /** 94 | * @param OutputInterface $output 95 | * @param array $rows 96 | */ 97 | private function renderTable(OutputInterface $output, array $rows) 98 | { 99 | $table = new Table($output); 100 | $table->setHeaders(array('Property', 'Document Fieldname', 'Boost')); 101 | $table->setRows($rows); 102 | 103 | $table->render(); 104 | } 105 | 106 | /** 107 | * @param MetaInformationInterface $metaInformation 108 | * 109 | * @return array 110 | */ 111 | private function getSimpleFields(MetaInformationInterface $metaInformation) 112 | { 113 | $simpleFields = array_filter($metaInformation->getFieldMapping(), function ($field) { 114 | if (strpos($field, '.') === false) { 115 | return true; 116 | } 117 | 118 | return false; 119 | }); 120 | 121 | return $simpleFields; 122 | } 123 | 124 | /** 125 | * @param MetaInformationInterface $metaInformation 126 | * 127 | * @return array 128 | */ 129 | protected function getNestedFields(MetaInformationInterface $metaInformation) 130 | { 131 | $complexFields = array_filter($metaInformation->getFieldMapping(), function ($field) { 132 | if (strpos($field, '.id') !== false) { 133 | return true; 134 | } 135 | 136 | return false; 137 | }); 138 | 139 | return $complexFields; 140 | } 141 | } -------------------------------------------------------------------------------- /Resources/doc/index_relations.md: -------------------------------------------------------------------------------- 1 | # Index OneToOne/ManyToOne relation 2 | 3 | Given you have the following entity with a ManyToOne relation to `Category`. 4 | 5 | ```php 6 | setTitle('post category #1'); 65 | 66 | $post = new Post(); 67 | $post->setTitle('a post title'); 68 | $post->setCategory($category); 69 | 70 | $em = $this->getDoctrine()->getManager(); 71 | $em->persist($post); 72 | $em->flush(); 73 | ``` 74 | 75 | ### Quering the relation 76 | 77 | ```php 78 | $posts = $this->get('solr.client')->getRepository('AcmeDemoBundle:Post')->findOneBy(array( 79 | 'category' => 'post category #1' 80 | )); 81 | ``` 82 | 83 | # Index OneToMany relation 84 | 85 | Given you have the following `Post` entity with a OneToMany relation to `Tag`. 86 | 87 | Again you can index the collection in two ways: 88 | 89 | - flat strings representation 90 | - full objects 91 | 92 | ## flat strings representation 93 | 94 | ```php 95 | setTitle($postTitle); 141 | $post->setText('relation'); 142 | $post->setTags(array( 143 | new Tag('tag #1'), 144 | new Tag('tag #2'), 145 | new Tag('tag #3') 146 | )); 147 | 148 | $em = $this->getDoctrine()->getManager(); 149 | $em->persist($post); 150 | $em->flush(); 151 | ``` 152 | 153 | Which will result in a document like this: 154 | 155 | ```json 156 | "docs": [ 157 | { 158 | "id": "post_391", 159 | "title_s": "post 25.03.2016", 160 | "text_t": "relation", 161 | "tags_ss": [ 162 | "tag #1", 163 | "tag #2", 164 | "tag #3" 165 | ], 166 | "_version_": 1529771282767282200 167 | } 168 | ] 169 | ``` 170 | 171 | ### Quering the strings collection 172 | 173 | Now `Post` can be searched like this 174 | 175 | ```php 176 | $posts = $this->get('solr.client')->getRepository('AcmeDemoBundle:Post')->findOneBy(array( 177 | 'tags' => 'tag #1' 178 | )); 179 | ``` 180 | 181 | ## Index full objects 182 | 183 | Post entity: 184 | 185 | ```php 186 | /** 187 | * @Solr\Field(type="strings", nestedClass="Acme\DemoBundle\Entity\Tag") 188 | * 189 | * @ORM\OneToMany(targetEntity="Acme\DemoBundle\Entity\Tag", mappedBy="post", cascade={"persist"}) 190 | */ 191 | private $tags; 192 | ``` 193 | 194 | Mark the `Tag` entity as Nested 195 | 196 | ```php 197 | /** 198 | * Tag 199 | * 200 | * @Solr\Nested() 201 | * 202 | * @ORM\Table() 203 | * @ORM\Entity 204 | */ 205 | class Tag 206 | { 207 | /** 208 | * @var integer 209 | * 210 | * @Solr\Id 211 | * 212 | * orm stuff 213 | */ 214 | private $id; 215 | 216 | /** 217 | * @var string 218 | * 219 | * @Solr\Field(type="string") 220 | * 221 | * @ORM\Column(name="name", type="string", length=255) 222 | */ 223 | private $name; 224 | 225 | // getter and setter 226 | } 227 | ``` 228 | 229 | ## Querying the collection 230 | 231 | Now `Post` can be searched like this 232 | 233 | ```php 234 | $posts = $this->get('solr.client')->getRepository('AcmeDemoBundle:Post')->findOneBy(array( 235 | 'tags.name' => 'tag #1' 236 | )); 237 | ``` 238 | 239 | -------------------------------------------------------------------------------- /Doctrine/Hydration/ValueHydrator.php: -------------------------------------------------------------------------------- 1 | cache[$metaInformation->getDocumentName()])) { 28 | $this->cache[$metaInformation->getDocumentName()] = array(); 29 | } 30 | 31 | $targetEntity = $metaInformation->getEntity(); 32 | 33 | $reflectionClass = new \ReflectionClass($targetEntity); 34 | foreach ($document as $property => $value) { 35 | if ($property === MetaInformationInterface::DOCUMENT_KEY_FIELD_NAME) { 36 | $value = $this->removePrefixedKeyValues($value); 37 | } 38 | 39 | // skip field if value is array or "flat" object 40 | // hydrated object should contain a list of real entities / entity 41 | if ($this->mapValue($property, $value, $metaInformation) == false) { 42 | continue; 43 | } 44 | 45 | if (isset($this->cache[$metaInformation->getDocumentName()][$property])) { 46 | $this->cache[$metaInformation->getDocumentName()][$property]->setValue($targetEntity, $value); 47 | 48 | continue; 49 | } 50 | 51 | // find setter method 52 | $camelCasePropertyName = $this->toCamelCase($this->removeFieldSuffix($property)); 53 | $setterMethodName = 'set'.ucfirst($camelCasePropertyName); 54 | if (method_exists($targetEntity, $setterMethodName)) { 55 | $accessor = new MethodCallPropertyAccessor($setterMethodName); 56 | $accessor->setValue($targetEntity, $value); 57 | 58 | $this->cache[$metaInformation->getDocumentName()][$property] = $accessor; 59 | 60 | continue; 61 | } 62 | 63 | 64 | if ($reflectionClass->hasProperty($this->removeFieldSuffix($property))) { 65 | $classProperty = $reflectionClass->getProperty($this->removeFieldSuffix($property)); 66 | } else { 67 | // could no found document-field in underscore notation, transform them to camel-case notation 68 | $camelCasePropertyName = $this->toCamelCase($this->removeFieldSuffix($property)); 69 | if ($reflectionClass->hasProperty($camelCasePropertyName) == false) { 70 | continue; 71 | } 72 | 73 | $classProperty = $reflectionClass->getProperty($camelCasePropertyName); 74 | } 75 | 76 | $accessor = new PrivatePropertyAccessor($classProperty); 77 | $accessor->setValue($targetEntity, $value); 78 | 79 | $this->cache[$metaInformation->getDocumentName()][$property] = $accessor; 80 | } 81 | 82 | return $targetEntity; 83 | } 84 | 85 | /** 86 | * returns the clean fieldname without type-suffix 87 | * 88 | * eg: title_s => title 89 | * 90 | * @param string $property 91 | * 92 | * @return string 93 | */ 94 | protected function removeFieldSuffix($property) 95 | { 96 | if (($pos = strrpos($property, '_')) !== false) { 97 | return substr($property, 0, $pos); 98 | } 99 | 100 | return $property; 101 | } 102 | 103 | /** 104 | * keyfield product_1 becomes 1 105 | * 106 | * @param string $value 107 | * 108 | * @return string 109 | */ 110 | public function removePrefixedKeyValues($value) 111 | { 112 | if (($pos = strrpos($value, '_')) !== false) { 113 | return substr($value, ($pos + 1)); 114 | } 115 | 116 | return $value; 117 | } 118 | 119 | /** 120 | * returns field name camelcased if it has underlines 121 | * 122 | * eg: user_id => userId 123 | * 124 | * @param string $fieldname 125 | * 126 | * @return string 127 | */ 128 | private function toCamelCase($fieldname) 129 | { 130 | $words = str_replace('_', ' ', $fieldname); 131 | $words = ucwords($words); 132 | $pascalCased = str_replace(' ', '', $words); 133 | 134 | return lcfirst($pascalCased); 135 | } 136 | 137 | /** 138 | * Check if given field and value can be mapped 139 | * 140 | * @param string $fieldName 141 | * @param string $value 142 | * @param MetaInformationInterface $metaInformation 143 | * 144 | * @return bool 145 | */ 146 | public function mapValue($fieldName, $value, MetaInformationInterface $metaInformation) 147 | { 148 | return true; 149 | } 150 | } -------------------------------------------------------------------------------- /Tests/Doctrine/ORM/Listener/EntityIndexerSubscriberTest.php: -------------------------------------------------------------------------------- 1 | logger = $this->createMock(LoggerInterface::class); 37 | $this->solr = $this->createMock(SolrInterface::class); 38 | $this->metaInformationFactory = new MetaInformationFactory(new AnnotationReader(new \Doctrine\Common\Annotations\AnnotationReader())); 39 | 40 | $this->subscriber = new EntityIndexerSubscriber($this->solr, $this->metaInformationFactory, $this->logger); 41 | } 42 | 43 | /** 44 | * @test 45 | */ 46 | public function separteDeletedRootEntitiesFromNested() 47 | { 48 | $nested = new NestedEntity(); 49 | $nested->setId(uniqid()); 50 | 51 | $entity = new ValidTestEntityWithCollection(); 52 | $entity->setId(uniqid()); 53 | $entity->setCollection(new ArrayCollection([$nested])); 54 | 55 | $objectManager = $this->createMock(ObjectManager::class); 56 | 57 | $this->solr->expects($this->at(0)) 58 | ->method('removeDocument') 59 | ->with($this->callback(function(ValidTestEntityWithCollection $entity) { 60 | if (count($entity->getCollection())) { 61 | return false; 62 | } 63 | 64 | return true; 65 | })); 66 | 67 | $this->solr->expects($this->at(1)) 68 | ->method('removeDocument') 69 | ->with($this->callback(function($entity) { 70 | if (!$entity instanceof NestedEntity) { 71 | return false; 72 | } 73 | 74 | return true; 75 | })); 76 | 77 | $deleteRootEntityEvent = new LifecycleEventArgs($entity, $objectManager); 78 | $this->subscriber->preRemove($deleteRootEntityEvent); 79 | 80 | $deleteNestedEntityEvent = new LifecycleEventArgs($nested, $objectManager); 81 | $this->subscriber->preRemove($deleteNestedEntityEvent); 82 | 83 | $entityManager = $this->createMock(EntityManagerInterface::class); 84 | 85 | $this->subscriber->postFlush(new PostFlushEventArgs($entityManager)); 86 | } 87 | 88 | /** 89 | * @test 90 | */ 91 | public function indexOnlyModifiedEntites() 92 | { 93 | $changedEntity = new ValidTestEntityWithCollection(); 94 | $this->solr->expects($this->once()) 95 | ->method('updateDocument') 96 | ->with($changedEntity); 97 | 98 | $unitOfWork = $this->createMock(UnitOfWork::class); 99 | $unitOfWork->expects($this->at(0)) 100 | ->method('getEntityChangeSet') 101 | ->willReturn(['title' => 'value']); 102 | 103 | $unitOfWork->expects($this->at(1)) 104 | ->method('getEntityChangeSet') 105 | ->willReturn([]); 106 | 107 | $objectManager = $this->createMock(EntityManagerInterface::class); 108 | $objectManager->expects($this->any()) 109 | ->method('getUnitOfWork') 110 | ->willReturn($unitOfWork); 111 | 112 | $updateEntityEvent1 = new LifecycleEventArgs($changedEntity, $objectManager); 113 | 114 | $unmodifiedEntity = new ValidTestEntityWithCollection(); 115 | $updateEntityEvent2 = new LifecycleEventArgs($unmodifiedEntity, $objectManager); 116 | 117 | $this->subscriber->postUpdate($updateEntityEvent1); 118 | $this->subscriber->postUpdate($updateEntityEvent2); 119 | } 120 | 121 | /** 122 | * @test 123 | */ 124 | public function doNotFailHardIfNormalEntityIsPersisted() 125 | { 126 | $this->solr->expects($this->never()) 127 | ->method('addDocument'); 128 | 129 | $this->solr->expects($this->never()) 130 | ->method('removeDocument'); 131 | 132 | $entity = new NotIndexedEntity(); 133 | 134 | $objectManager = $this->createMock(EntityManagerInterface::class); 135 | 136 | $lifecycleEventArgs = new LifecycleEventArgs($entity, $objectManager); 137 | 138 | $this->subscriber->postPersist($lifecycleEventArgs); 139 | $this->subscriber->preRemove($lifecycleEventArgs); 140 | 141 | 142 | 143 | $this->subscriber->postFlush(new PostFlushEventArgs($objectManager)); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Resources/doc/indexing.md: -------------------------------------------------------------------------------- 1 | # How to index more than 500k entities 2 | 3 | If you want to index a lot of entities then it is not a good idea to use `solr:index:populate`. 4 | The command works well with 100k-200k entites everything larger than that makes the command incredible slow and needs a lot of memory. This is because doctrine is not designed to handle hundred-thousands of entities. 5 | 6 | In my following example I have a `person` table with 5000000 rows and three columns: `id`, `name` and `email`. The resulting documents are schemaless, so all fields have a suffix e.g. `name_s`. 7 | 8 | Here are some possibilities which works well for me: 9 | 10 | ## CSV export with MySQL Prepared Statement + Solr PostTool 11 | 12 | This solution does not use PHP. 13 | 14 | 1. export your data to person.csv 15 | ```sql 16 | SET @TS = DATE_FORMAT(NOW(),'_%Y_%m_%d_%H_%i_%s'); 17 | 18 | SET @FOLDER = '/tmp/'; -- target dir 19 | SET @PREFIX = 'person'; 20 | SET @EXT = '.csv'; 21 | 22 | -- first select defines the header of the csv-file 23 | SET @CMD = CONCAT("SELECT 'id', 'name_s', 'email_s' UNION ALL SELECT * FROM person INTO OUTFILE '",@FOLDER,@PREFIX,@TS,@EXT, 24 | "' FIELDS ENCLOSED BY '\"' TERMINATED BY ',' ESCAPED BY '\"'", 25 | " LINES TERMINATED BY '\r\n';"); 26 | 27 | PREPARE statement FROM @CMD; 28 | 29 | EXECUTE statement; 30 | ``` 31 | 32 | Then run this SQL-script: 33 | 34 | ```bash 35 | mysql -udbuser -p123 dbname < dump_person_table.sql 36 | ``` 37 | 38 | The resulting file looks like this: `/tmp/person_2017_03_01_11_21_41.csv` 39 | 40 | 2. index the csv with [post-tool](https://lucidworks.com/2015/08/04/solr-5-new-binpost-utility/) 41 | 42 | ```bash 43 | /opt/solr/solr-5.5.2/bin/post -c core0 /tmp/person_2017_03_01_11_21_41.csv 44 | ``` 45 | 46 | ## PDO Select + [Solarium BufferedAdd](http://solarium.readthedocs.io/en/stable/plugins/#example-usage) 47 | 48 | The script has two parts: 49 | 50 | 1. select a chunk of rows from the DB 51 | 2. add the rows to the index with Solarium 52 | 53 | ```php 54 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 59 | 60 | $statement = $connection->prepare('SELECT COUNT(*) as total_items FROM person'); 61 | $statement->execute(); 62 | $countResult = $statement->fetch(PDO::FETCH_ASSOC); 63 | 64 | $totalItems = $countResult['total_items']; 65 | $batchSize = 5000; 66 | 67 | $pages = ceil($totalItems / $batchSize); 68 | 69 | $client = new Solarium\Client([ 70 | 'endpoint' => [ 71 | 'localhost' => [ 72 | 'host' => 'localhost', 73 | 'port' => 8983, 74 | 'path' => '/solr/core0', 75 | ] 76 | ] 77 | ]); 78 | 79 | /** @var \Solarium\Plugin\BufferedAdd\BufferedAdd $buffer */ 80 | $buffer = $client->getPlugin('bufferedadd'); 81 | $buffer->setBufferSize($batchSize); 82 | 83 | for ($i = 0; $i <= $pages; $i++) { 84 | $limitStart = ($i - 1) * $batchSize; 85 | $limitEnd = $batchSize * $i; 86 | if ($i == 0) { 87 | $limitStart = 1; 88 | $limitEnd = $batchSize; 89 | } 90 | 91 | $statement = $connection->prepare(sprintf('SELECT id, name, email FROM person WHERE id >= %s AND id <= %s ', $limitStart, $limitEnd)); 92 | $statement->execute(); 93 | 94 | foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $item) { 95 | $buffer->createDocument([ 96 | 'id' => $item['id'], 97 | 'name_s' => $item['name'], 98 | 'email_s' => $item['email'] 99 | ]); 100 | } 101 | 102 | $statement->closeCursor(); 103 | 104 | $buffer->commit(); 105 | 106 | echo sprintf('Indexing page %s / %s', $i, $pages) . PHP_EOL; 107 | } 108 | 109 | $buffer->flush(); 110 | ``` 111 | 112 | # PDO Select + CSV Export + Solr Post-Tool 113 | 114 | This solution exports the database to csv by using PDO. The exported files are located under `/tmp/export`. 115 | 116 | ```php 117 | $connection = new PDO('mysql:host=localhost;dbname=dbname;charset=utf8mb4', 'dbuser', '123'); 118 | $connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 119 | 120 | $statement = $connection->prepare('SELECT COUNT(*) as total_items FROM person'); 121 | $statement->execute(); 122 | $countResult = $statement->fetch(PDO::FETCH_ASSOC); 123 | $statement->closeCursor(); 124 | 125 | $totalItems = $countResult['total_items']; 126 | $batchSize = 10000; 127 | 128 | $pages = ceil($totalItems / $batchSize); 129 | 130 | @mkdir('/tmp/export'); 131 | 132 | for ($i = 0; $i <= $pages; $i++) { 133 | $data = []; 134 | $limitStart = ($i - 1) * $batchSize; 135 | $limitEnd = $batchSize * $i; 136 | if ($i == 0) { 137 | $limitStart = 1; 138 | $limitEnd = $batchSize; 139 | } 140 | 141 | $statement = $connection->prepare(sprintf('SELECT id, name, email FROM person WHERE id >= %s AND id <= %s ', $limitStart, $limitEnd)); 142 | $statement->execute(); 143 | 144 | $data[] = "id, name_s, email_s\n"; 145 | 146 | foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $item) { 147 | $data[] = sprintf("\"%s\", \"%s\", \"%s\"", $item['id'], $item['name'], $item['email']); 148 | } 149 | 150 | $statement->closeCursor(); 151 | 152 | file_put_contents(sprintf('/tmp/export/person_%s.csv', $i), join("\n", $data)); 153 | 154 | echo sprintf('Indexing page %s / %s', $i, $pages) . PHP_EOL; 155 | } 156 | ``` 157 | 158 | To import the data we are using Solr Post-Tool: 159 | 160 | `/opt/solr/solr-5.5.2/bin/post -c core0 /tmp/export` 161 | --------------------------------------------------------------------------------