diff --git a/Client/Solarium/Plugin/LoggerPlugin.php b/Client/Solarium/Plugin/LoggerPlugin.php index ddf02074..27c9ff0d 100644 --- a/Client/Solarium/Plugin/LoggerPlugin.php +++ b/Client/Solarium/Plugin/LoggerPlugin.php @@ -42,11 +42,18 @@ protected function initPluginType() public function preExecuteRequest(PreExecuteRequest $event) { $endpoint = $event->getEndpoint(); - $uri = $event->getRequest()->getUri(); + $request = $event->getRequest(); + $uri = $request->getUri(); $path = sprintf('%s://%s:%s%s/%s', $endpoint->getScheme(), $endpoint->getHost(), $endpoint->getPort(), $endpoint->getPath(), urldecode($uri)); - $this->logger->startRequest($path); + $requestInformation = [ + 'uri' => $path, + 'method' => $request->getMethod(), + 'raw_data' => $request->getRawData() + ]; + + $this->logger->startRequest($requestInformation); } /** diff --git a/Client/Solarium/SolariumClientBuilder.php b/Client/Solarium/SolariumClientBuilder.php index 8ac00568..8c2d8ee8 100644 --- a/Client/Solarium/SolariumClientBuilder.php +++ b/Client/Solarium/SolariumClientBuilder.php @@ -53,11 +53,40 @@ public function addPlugin($pluginName, AbstractPlugin $plugin) */ public function build() { - $solariumClient = new Client(array('endpoint' => $this->settings), $this->eventDispatcher); + $settings = []; + foreach ($this->settings as $name => $options) { + if (isset($options['dsn'])) { + unset( + $options['scheme'], + $options['host'], + $options['port'], + $options['path'] + ); + + $parsedDsn = parse_url($options['dsn']); + unset($options['dsn']); + if ($parsedDsn) { + $options['scheme'] = isset($parsedDsn['scheme']) ? $parsedDsn['scheme'] : 'http'; + if (isset($parsedDsn['host'])) { + $options['host'] = $parsedDsn['host']; + } + if (isset($parsedDsn['user'])) { + $auth = $parsedDsn['user'] . (isset($parsedDsn['pass']) ? ':' . $parsedDsn['pass'] : ''); + $options['host'] = $auth . '@' . $options['host']; + } + $options['port'] = isset($parsedDsn['port']) ? $parsedDsn['port'] : 80; + $options['path'] = isset($parsedDsn['path']) ? $parsedDsn['path'] : ''; + } + } + + $settings[$name] = $options; + } + + $solariumClient = new Client(array('endpoint' => $settings), $this->eventDispatcher); foreach ($this->plugins as $pluginName => $plugin) { $solariumClient->registerPlugin($pluginName, $plugin); } return $solariumClient; } -} \ No newline at end of file +} diff --git a/Command/ShowSchemaCommand.php b/Command/ShowSchemaCommand.php index a9c01642..2516c28a 100644 --- a/Command/ShowSchemaCommand.php +++ b/Command/ShowSchemaCommand.php @@ -2,6 +2,7 @@ namespace FS\SolrBundle\Command; +use FS\SolrBundle\Doctrine\Mapper\MetaInformationInterface; use FS\SolrBundle\Doctrine\Mapper\SolrMappingException; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Helper\Table; @@ -30,31 +31,111 @@ protected function execute(InputInterface $input, OutputInterface $output) foreach ($namespaces->getEntityClassnames() as $classname) { try { $metaInformation = $metaInformationFactory->loadInformation($classname); + + if ($metaInformation->isNested()) { + continue; + } } catch (SolrMappingException $e) { - $output->writeln(sprintf('%s', $e->getMessage())); continue; } - $output->writeln(sprintf('%s', $classname)); + $nested = ''; + if ($metaInformation->isNested()) { + $nested = '(nested)'; + } + $output->writeln(sprintf('%s %s', $classname, $nested)); $output->writeln(sprintf('Documentname: %s', $metaInformation->getDocumentName())); $output->writeln(sprintf('Document Boost: %s', $metaInformation->getBoost()?$metaInformation->getBoost(): '-')); - $table = new Table($output); - $table->setHeaders(array('Property', 'Document Fieldname', 'Boost')); - - foreach ($metaInformation->getFieldMapping() as $documentField => $property) { - $field = $metaInformation->getField($documentField); + $simpleFields = $this->getSimpleFields($metaInformation); - if ($field === null) { - continue; + $rows = []; + foreach ($simpleFields as $documentField => $property) { + if ($field = $metaInformation->getField($documentField)) { + $rows[] = [$property, $documentField, $field->boost]; } + } + $this->renderTable($output, $rows); - $table->addRow(array($property, $documentField, $field->boost)); + $nestedFields = $this->getNestedFields($metaInformation); + if (count($nestedFields) == 0) { + return; } - $table->render(); + + $output->writeln(sprintf('Fields (%s) with nested documents', count($nestedFields))); + + foreach ($nestedFields as $idField) { + $propertyName = substr($idField, 0, strpos($idField, '.')); + + if ($nestedField = $metaInformation->getField($propertyName)) { + $output->writeln(sprintf('Field %s contains nested class %s', $propertyName, $nestedField->nestedClass)); + + $nestedDocument = $metaInformationFactory->loadInformation($nestedField->nestedClass); + $rows = []; + foreach ($nestedDocument->getFieldMapping() as $documentField => $property) { + $field = $nestedDocument->getField($documentField); + + if ($field === null) { + continue; + } + + $rows[] = [$property, $documentField, $field->boost]; + } + + $this->renderTable($output, $rows); + } + } + } } + /** + * @param OutputInterface $output + * @param array $rows + */ + private function renderTable(OutputInterface $output, array $rows) + { + $table = new Table($output); + $table->setHeaders(array('Property', 'Document Fieldname', 'Boost')); + $table->setRows($rows); + + $table->render(); + } + + /** + * @param MetaInformationInterface $metaInformation + * + * @return array + */ + private function getSimpleFields(MetaInformationInterface $metaInformation) + { + $simpleFields = array_filter($metaInformation->getFieldMapping(), function ($field) { + if (strpos($field, '.') === false) { + return true; + } + + return false; + }); + return $simpleFields; + } + + /** + * @param MetaInformationInterface $metaInformation + * + * @return array + */ + protected function getNestedFields(MetaInformationInterface $metaInformation) + { + $complexFields = array_filter($metaInformation->getFieldMapping(), function ($field) { + if (strpos($field, '.id') !== false) { + return true; + } + + return false; + }); + + return $complexFields; + } } \ No newline at end of file diff --git a/Command/SynchronizeIndexCommand.php b/Command/SynchronizeIndexCommand.php index 28b63bb2..0dfdbec4 100644 --- a/Command/SynchronizeIndexCommand.php +++ b/Command/SynchronizeIndexCommand.php @@ -95,7 +95,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $i++; } - $entities = $repository->findBy(array(), null, $batchSize, $offset); + $entities = $repository->findBy([], null, $batchSize, $offset); try { $solr->synchronizeIndex($entities); @@ -141,16 +141,20 @@ private function getObjectManager($entityClassname) private function getIndexableEntities($entity = null) { if ($entity) { - return array($entity); + return [$entity]; } - $entities = array(); + $entities = []; $namespaces = $this->getContainer()->get('solr.doctrine.classnameresolver.known_entity_namespaces'); $metaInformationFactory = $this->getContainer()->get('solr.meta.information.factory'); foreach ($namespaces->getEntityClassnames() as $classname) { try { $metaInformation = $metaInformationFactory->loadInformation($classname); + if ($metaInformation->isNested()) { + continue; + } + array_push($entities, $metaInformation->getClassName()); } catch (SolrMappingException $e) { continue; diff --git a/DataCollector/RequestCollector.php b/DataCollector/RequestCollector.php index 5e6216e5..b5649ae9 100644 --- a/DataCollector/RequestCollector.php +++ b/DataCollector/RequestCollector.php @@ -3,11 +3,11 @@ namespace FS\SolrBundle\DataCollector; use FS\SolrBundle\Logging\DebugLogger; -use FS\SolrBundle\Logging\SolrLoggerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; -use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\VarDumper\Cloner\VarCloner; class RequestCollector extends DataCollector { @@ -25,9 +25,7 @@ public function __construct(DebugLogger $logger) } /** - * @param Request $request - * @param Response $response - * @param \Exception|null $exception + * {@inheritdoc} */ public function collect(Request $request, Response $response, \Exception $exception = null) { @@ -74,12 +72,21 @@ public function getTime() */ public function parseQuery($request) { - list($endpoint, $params) = explode('?', $request['request']); + list($endpoint, $params) = explode('?', $request['request']['uri']); + + $request['endpoint'] = $endpoint; + $request['params'] = $params; + $request['method'] = $request['request']['method']; + $request['raw_data'] = $request['request']['raw_data']; + + if (class_exists(VarCloner::class)) { + $varCloner = new VarCloner(); + + parse_str($params, $stub); + $request['stub'] = Kernel::VERSION_ID >= 30200 ? $varCloner->cloneVar($stub) : $stub; + } - return array_merge($request, [ - 'endpoint' => $endpoint, - 'params' => $params - ]); + return $request; } /** diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 706cb1e0..955b051a 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -9,7 +9,7 @@ class Configuration implements ConfigurationInterface { /** - * @return TreeBuilder + * {@inheritdoc} */ public function getConfigTreeBuilder() { @@ -17,30 +17,22 @@ public function getConfigTreeBuilder() $rootNode = $treeBuilder->root('fs_solr'); $rootNode->children() ->arrayNode('endpoints') - ->useAttributeAsKey('name') - ->prototype('array') - ->children() - ->scalarNode('host')->end() - ->scalarNode('port')->end() - ->scalarNode('path')->end() - ->scalarNode('core')->end() - ->scalarNode('timeout')->end() - ->booleanNode('active')->defaultValue(true)->end() + ->useAttributeAsKey('name') + ->prototype('array') + ->children() + ->scalarNode('dsn')->end() + ->scalarNode('scheme')->end() + ->scalarNode('host')->end() + ->scalarNode('port')->end() + ->scalarNode('path')->end() + ->scalarNode('core')->end() + ->scalarNode('timeout')->end() + ->booleanNode('active')->defaultValue(true)->end() + ->end() + ->end() ->end() - ->end() - ->end() -// ->arrayNode('clients') -// ->useAttributeAsKey('name') -// ->prototype('array') -// ->children() -// ->arrayNode('endpoints') -// ->prototype('scalar')->end() -// ->end() -// ->end() -// ->end() -// ->end() ->booleanNode('auto_index')->defaultValue(true)->end() - ->end(); + ->end(); return $treeBuilder; } diff --git a/DependencyInjection/FSSolrExtension.php b/DependencyInjection/FSSolrExtension.php index b38e476e..1a3f2745 100644 --- a/DependencyInjection/FSSolrExtension.php +++ b/DependencyInjection/FSSolrExtension.php @@ -2,8 +2,6 @@ namespace FS\SolrBundle\DependencyInjection; -use Symfony\Component\DependencyInjection\DefinitionDecorator; -use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\FileLocator; @@ -14,8 +12,7 @@ class FSSolrExtension extends Extension { /** - * @param array $configs - * @param ContainerBuilder $container + * {@inheritdoc} */ public function load(array $configs, ContainerBuilder $container) { @@ -29,7 +26,9 @@ public function load(array $configs, ContainerBuilder $container) $this->setupClients($config, $container); - $container->setParameter('solr.auto_index', $config['auto_index']); + if (!$container->hasParameter('solr.auto_index')) { + $container->setParameter('solr.auto_index', $config['auto_index']); + } $this->setupDoctrineListener($config, $container); $this->setupDoctrineConfiguration($config, $container); diff --git a/Doctrine/AbstractIndexingListener.php b/Doctrine/AbstractIndexingListener.php index 29b1268d..50dff63b 100644 --- a/Doctrine/AbstractIndexingListener.php +++ b/Doctrine/AbstractIndexingListener.php @@ -61,4 +61,16 @@ protected function hasChanged($doctrineChangeSet, $entity) return count($documentChangeSet) > 0; } + + /** + * @param object $entity + * + * @return bool + */ + protected function isNested($entity) + { + $metaInformation = $this->metaInformationFactory->loadInformation($entity); + + return $metaInformation->isNested(); + } } \ No newline at end of file diff --git a/Doctrine/Annotation/AnnotationReader.php b/Doctrine/Annotation/AnnotationReader.php index 7ad16c66..d8c8ed64 100644 --- a/Doctrine/Annotation/AnnotationReader.php +++ b/Doctrine/Annotation/AnnotationReader.php @@ -4,6 +4,7 @@ use Doctrine\Common\Annotations\Annotation; use Doctrine\Common\Annotations\Reader; +use FS\SolrBundle\Doctrine\Mapper\SolrMappingException; class AnnotationReader { @@ -18,9 +19,9 @@ class AnnotationReader private $entityProperties; const DOCUMENT_CLASS = 'FS\SolrBundle\Doctrine\Annotation\Document'; + const DOCUMENT_NESTED_CLASS = 'FS\SolrBundle\Doctrine\Annotation\Nested'; const FIELD_CLASS = 'FS\SolrBundle\Doctrine\Annotation\Field'; const FIELD_IDENTIFIER_CLASS = 'FS\SolrBundle\Doctrine\Annotation\Id'; - const DOCUMENT_INDEX_CLASS = 'FS\SolrBundle\Doctrine\Annotation\Document'; const SYNCHRONIZATION_FILTER_CLASS = 'FS\SolrBundle\Doctrine\Annotation\SynchronizationFilter'; /** @@ -43,7 +44,7 @@ private function getPropertiesByType($entity, $type) { $properties = $this->readClassProperties($entity); - $fields = array(); + $fields = []; foreach ($properties as $property) { $annotation = $this->reader->getPropertyAnnotation($property, $type); @@ -86,6 +87,38 @@ public function getFields($entity) return $this->getPropertiesByType($entity, self::FIELD_CLASS); } + /** + * @param object $entity + * + * @return array + * + * @throws \ReflectionException + */ + public function getMethods($entity) + { + $reflectionClass = new \ReflectionClass($entity); + + $methods = []; + foreach ($reflectionClass->getMethods() as $method) { + /** @var Field $annotation */ + $annotation = $this->reader->getMethodAnnotation($method, self::FIELD_CLASS); + + if ($annotation === null) { + continue; + } + + $annotation->value = $method->invoke($entity); + + if ($annotation->name == '') { + throw new SolrMappingException(sprintf('Please configure a field-name for method "%s" with field-annotation in class "%s"', $method->getName(), get_class($entity))); + } + + $methods[] = $annotation; + } + + return $methods; + } + /** * @param object $entity * @@ -95,7 +128,7 @@ public function getFields($entity) */ public function getEntityBoost($entity) { - $annotation = $this->getClassAnnotation($entity, self::DOCUMENT_INDEX_CLASS); + $annotation = $this->getClassAnnotation($entity, self::DOCUMENT_CLASS); if (!$annotation instanceof Document) { return 0; @@ -120,9 +153,9 @@ public function getEntityBoost($entity) */ public function getDocumentIndex($entity) { - $annotation = $this->getClassAnnotation($entity, self::DOCUMENT_INDEX_CLASS); + $annotation = $this->getClassAnnotation($entity, self::DOCUMENT_CLASS); if (!$annotation instanceof Document) { - return ''; + return null; } $indexHandler = $annotation->indexHandler; @@ -178,11 +211,9 @@ public function getFieldMapping($entity) { $fields = $this->getPropertiesByType($entity, self::FIELD_CLASS); - $mapping = array(); + $mapping = []; foreach ($fields as $field) { - if ($field instanceof Field) { - $mapping[$field->getNameWithAlias()] = $field->name; - } + $mapping[$field->getNameWithAlias()] = $field->name; } $id = $this->getIdentifier($entity); @@ -198,9 +229,15 @@ public function getFieldMapping($entity) */ public function hasDocumentDeclaration($entity) { - $annotation = $this->getClassAnnotation($entity, self::DOCUMENT_INDEX_CLASS); + if ($rootDocument = $this->getClassAnnotation($entity, self::DOCUMENT_CLASS)) { + return true; + } + + if ($this->isNested($entity)) { + return true; + } - return $annotation !== null; + return false; } /** @@ -251,6 +288,20 @@ public function isOdm($entity) return true; } + /** + * @param object $entity + * + * @return bool + */ + public function isNested($entity) + { + if ($nestedDocument = $this->getClassAnnotation($entity, self::DOCUMENT_NESTED_CLASS)) { + return true; + } + + return false; + } + /** * @param string $entity * @param string $annotationName @@ -285,7 +336,7 @@ private function readClassProperties($entity) $reflectionClass = new \ReflectionClass($entity); $inheritedProperties = array_merge($this->getParentProperties($reflectionClass), $reflectionClass->getProperties()); - $properties = array(); + $properties = []; foreach ($inheritedProperties as $property) { $properties[$property->getName()] = $property; } diff --git a/Doctrine/Annotation/Field.php b/Doctrine/Annotation/Field.php index e7b7734b..3f87f08b 100644 --- a/Doctrine/Annotation/Field.php +++ b/Doctrine/Annotation/Field.php @@ -37,6 +37,11 @@ class Field extends Annotation */ public $fieldModifier; + /** + * @var string + */ + public $nestedClass; + /** * @var array */ @@ -54,7 +59,8 @@ class Field extends Annotation 'long' => '_l', 'float' => '_f', 'double' => '_d', - 'datetime' => '_dt' + 'datetime' => '_dt', + 'point' => '_p' ); /** diff --git a/Doctrine/Annotation/Nested.php b/Doctrine/Annotation/Nested.php new file mode 100644 index 00000000..d6d2fd53 --- /dev/null +++ b/Doctrine/Annotation/Nested.php @@ -0,0 +1,13 @@ +getValue(); - if ($fieldValue instanceof Collection) { - $document->addField($field->getNameWithAlias(), $this->mapCollection($field, $metaInformation->getClassName()), $field->getBoost()); - } elseif (is_object($fieldValue)) { - $document->addField($field->getNameWithAlias(), $this->mapObject($field), $field->getBoost()); - } else if ($field->getter && $fieldValue) { + if ($fieldValue instanceof Collection && $field->nestedClass) { + $this->mapCollectionField($document, $field, $metaInformation->getEntity()); + } else if (is_object($fieldValue) && $field->nestedClass) { // index sinsgle object as nested child-document + $document->addField('_childDocuments_', [$this->objectToDocument($fieldValue)], $field->getBoost()); + } else if (is_object($fieldValue) && !$field->nestedClass) { // index object as "flat" string, call getter + $document->addField($field->getNameWithAlias(), $this->mapObjectField($field), $field->getBoost()); + } else if ($field->getter && $fieldValue) { // call getter to transform data (json to array, etc.) $getterValue = $this->callGetterMethod($metaInformation->getEntity(), $field->getGetterName()); $document->addField($field->getNameWithAlias(), $getterValue, $field->getBoost()); - } else { + } else { // field contains simple data-type $document->addField($field->getNameWithAlias(), $fieldValue, $field->getBoost()); } @@ -85,29 +87,21 @@ public function createDocument(MetaInformationInterface $metaInformation) * * @throws SolrMappingException if getter return value is object */ - private function mapObject(Field $field) + private function mapObjectField(Field $field) { $value = $field->getValue(); $getter = $field->getGetterName(); - if (!empty($getter)) { - $getterReturnValue = $this->callGetterMethod($value, $getter); - - if (is_object($getterReturnValue)) { - throw new SolrMappingException(sprintf('The configured getter "%s" in "%s" must return a string or array, got object', $getter, get_class($value))); - } - - return $getterReturnValue; + if (empty($getter)) { + throw new SolrMappingException(sprintf('Please configure a getter for property "%s" in class "%s"', $field->name, get_class($value))); } + + $getterReturnValue = $this->callGetterMethod($value, $getter); - $metaInformation = $this->metaInformationFactory->loadInformation($value); - - $field = array(); - $document = $this->createDocument($metaInformation); - foreach ($document as $fieldName => $value) { - $field[$fieldName] = $value; + if (is_object($getterReturnValue)) { + throw new SolrMappingException(sprintf('The configured getter "%s" in "%s" must return a string or array, got object', $getter, get_class($value))); } - return $field; + return $getterReturnValue; } /** @@ -151,20 +145,47 @@ private function callGetterMethod($object, $getter) * * @throws SolrMappingException if no getter method was found */ - private function mapCollection(Field $field, $sourceTargetClass) + private function mapCollectionField($document, Field $field, $sourceTargetObject) { /** @var Collection $value */ $value = $field->getValue(); $getter = $field->getGetterName(); - if ($getter == '') { - throw new SolrMappingException(sprintf('No getter method for property "%s" configured in class "%s"', $field->name, $sourceTargetClass)); + + if ($getter != '') { + $value = $this->callGetterMethod($sourceTargetObject, $getter); } - $values = array(); + $values = []; foreach ($value as $relatedObj) { - $values[] = $this->callGetterMethod($relatedObj, $getter); + if (is_object($relatedObj)) { + $values[] = $this->objectToDocument($relatedObj); + } else { + $values[] = $relatedObj; + } } + $document->addField('_childDocuments_', $values, $field->getBoost()); + return $values; } + + /** + * @param mixed $value + * + * @return array + * + * @throws SolrMappingException + */ + private function objectToDocument($value) + { + $metaInformation = $this->metaInformationFactory->loadInformation($value); + + $field = []; + $document = $this->createDocument($metaInformation); + foreach ($document as $fieldName => $value) { + $field[$fieldName] = $value; + } + + return $field; + } } \ No newline at end of file diff --git a/Doctrine/Mapper/MetaInformation.php b/Doctrine/Mapper/MetaInformation.php index 0353fbf1..220b97db 100644 --- a/Doctrine/Mapper/MetaInformation.php +++ b/Doctrine/Mapper/MetaInformation.php @@ -75,6 +75,11 @@ class MetaInformation implements MetaInformationInterface */ private $doctrineMapperType; + /** + * @var bool + */ + private $nested; + /** * {@inheritdoc} */ @@ -380,4 +385,20 @@ public function generateDocumentId() return $this->identifier->generateId; } + + /** + * {@inheritdoc} + */ + public function isNested() + { + return $this->nested; + } + + /** + * @param bool $nested + */ + public function setNested(bool $nested) + { + $this->nested = $nested; + } } diff --git a/Doctrine/Mapper/MetaInformationFactory.php b/Doctrine/Mapper/MetaInformationFactory.php index 167b95c0..071b219f 100644 --- a/Doctrine/Mapper/MetaInformationFactory.php +++ b/Doctrine/Mapper/MetaInformationFactory.php @@ -58,12 +58,14 @@ public function loadInformation($entity) throw new SolrMappingException(sprintf('no declaration for document found in entity %s', $className)); } + $fields = array_merge($this->annotationReader->getFields($entity), $this->annotationReader->getMethods($entity)); + $metaInformation = new MetaInformation(); $metaInformation->setEntity($entity); $metaInformation->setClassName($className); $metaInformation->setDocumentName($this->getDocumentName($className)); $metaInformation->setFieldMapping($this->annotationReader->getFieldMapping($entity)); - $metaInformation->setFields($this->annotationReader->getFields($entity)); + $metaInformation->setFields($fields); $metaInformation->setRepository($this->annotationReader->getRepository($entity)); $metaInformation->setIdentifier($this->annotationReader->getIdentifier($entity)); $metaInformation->setBoost($this->annotationReader->getEntityBoost($entity)); @@ -71,6 +73,27 @@ public function loadInformation($entity) $metaInformation->setIndex($this->annotationReader->getDocumentIndex($entity)); $metaInformation->setIsDoctrineEntity($this->isDoctrineEntity($entity)); $metaInformation->setDoctrineMapperType($this->getDoctrineMapperType($entity)); + $metaInformation->setNested($this->annotationReader->isNested($entity)); + + $fields = $this->annotationReader->getFields($entity); + foreach ($fields as $field) { + if (!$field->nestedClass) { + continue; + } + + $nestedObjectMetainformation = $this->loadInformation($field->nestedClass); + + $subentityMapping = []; + $nestedFieldName = $field->name; + foreach ($nestedObjectMetainformation->getFieldMapping() as $documentName => $fieldName) { + $subentityMapping[$nestedFieldName . '.' . $documentName] = $nestedFieldName . '.' . $fieldName; + } + + $rootEntityMapping = $metaInformation->getFieldMapping(); + $subentityMapping = array_merge($subentityMapping, $rootEntityMapping); + unset($subentityMapping[$field->name]); + $metaInformation->setFieldMapping($subentityMapping); + } return $metaInformation; } diff --git a/Doctrine/Mapper/MetaInformationInterface.php b/Doctrine/Mapper/MetaInformationInterface.php index 4215c489..01f425d7 100644 --- a/Doctrine/Mapper/MetaInformationInterface.php +++ b/Doctrine/Mapper/MetaInformationInterface.php @@ -72,6 +72,8 @@ public function getFields(); public function getRepository(); /** + * Source/target entity instance + * * @return object */ public function getEntity(); @@ -80,6 +82,8 @@ public function getEntity(); * @param string $fieldName * * @return Field|null + * + * @throws \InvalidArgumentException if given $fieldName is unknown */ public function getField($fieldName); @@ -89,6 +93,8 @@ public function getField($fieldName); public function getFieldMapping(); /** + * The document boost value + * * @return number */ public function getBoost(); @@ -104,6 +110,8 @@ public function getSynchronizationCallback(); public function hasSynchronizationFilter(); /** + * Returns the configured index argument in FS\SolrBundle\Doctrine\Annotation\Document or the returns value of the index-handler callback + * * @return string */ public function getIndex(); @@ -116,12 +124,22 @@ public function getIndex(); public function getDocumentKey(); /** + * The property which has the FS\SolrBundle\Doctrine\Annotation\Id annotation + * * @return string */ public function getIdentifierFieldName(); /** + * Returns MetaInformationInterface::DOCTRINE_MAPPER_TYPE_DOCUMENT if target is an doctrine-odm object or + * MetaInformationInterface::DOCTRINE_MAPPER_TYPE_RELATIONAL if it is an doctrine-orm object, otherwise an empty string + * * @return string */ public function getDoctrineMapperType(); + + /** + * @return bool + */ + public function isNested(); } \ No newline at end of file diff --git a/Doctrine/ORM/Listener/EntityIndexerSubscriber.php b/Doctrine/ORM/Listener/EntityIndexerSubscriber.php index 29135335..660e7ed4 100644 --- a/Doctrine/ORM/Listener/EntityIndexerSubscriber.php +++ b/Doctrine/ORM/Listener/EntityIndexerSubscriber.php @@ -2,22 +2,37 @@ namespace FS\SolrBundle\Doctrine\ORM\Listener; +use DeepCopy\DeepCopy; +use DeepCopy\Filter\Doctrine\DoctrineEmptyCollectionFilter; +use DeepCopy\Matcher\PropertyTypeMatcher; use Doctrine\Common\EventSubscriber; use Doctrine\ORM\Event\LifecycleEventArgs; +use Doctrine\ORM\Event\PostFlushEventArgs; use FS\SolrBundle\Doctrine\AbstractIndexingListener; -use FS\SolrBundle\Doctrine\Mapper\MetaInformationFactory; -use FS\SolrBundle\SolrInterface; -use Psr\Log\LoggerInterface; class EntityIndexerSubscriber extends AbstractIndexingListener implements EventSubscriber { + /** + * @var array + */ + private $persistedEntities = []; + + /** + * @var array + */ + private $deletedRootEntities = []; + + /** + * @var array + */ + private $deletedNestedEntities = []; /** * {@inheritdoc} */ public function getSubscribedEvents() { - return array('postUpdate', 'postPersist', 'preRemove'); + return ['postUpdate', 'postPersist', 'preRemove', 'postFlush']; } /** @@ -46,11 +61,7 @@ public function postPersist(LifecycleEventArgs $args) { $entity = $args->getEntity(); - try { - $this->solr->addDocument($entity); - } catch (\Exception $e) { - $this->logger->debug($e->getMessage()); - } + $this->persistedEntities[] = $entity; } /** @@ -60,10 +71,44 @@ public function preRemove(LifecycleEventArgs $args) { $entity = $args->getEntity(); - try { + if ($this->isNested($entity)) { + $this->deletedNestedEntities[] = $this->emptyCollections($entity); + } else { + $this->deletedRootEntities[] = $this->emptyCollections($entity); + } + } + + /** + * @param object $object + * + * @return object + */ + private function emptyCollections($object) + { + $deepcopy = new DeepCopy(); + $deepcopy->addFilter(new DoctrineEmptyCollectionFilter(), new PropertyTypeMatcher('Doctrine\Common\Collections\Collection')); + + return $deepcopy->copy($object); + } + + /** + * @param PostFlushEventArgs $eventArgs + */ + public function postFlush(PostFlushEventArgs $eventArgs) + { + foreach ($this->persistedEntities as $entity) { + $this->solr->addDocument($entity); + } + $this->persistedEntities = []; + + foreach ($this->deletedRootEntities as $entity) { + $this->solr->removeDocument($entity); + } + $this->deletedRootEntities = []; + + foreach ($this->deletedNestedEntities as $entity) { $this->solr->removeDocument($entity); - } catch (\Exception $e) { - $this->logger->debug($e->getMessage()); } + $this->deletedNestedEntities = []; } } \ No newline at end of file diff --git a/Helper/DocumentHelper.php b/Helper/DocumentHelper.php new file mode 100644 index 00000000..4d776686 --- /dev/null +++ b/Helper/DocumentHelper.php @@ -0,0 +1,73 @@ +solariumClient = $solr->getClient(); + $this->metaInformationFactory = $solr->getMetaFactory(); + } + + /** + * @param mixed $entity + * + * @return int + */ + public function getLastInsertDocumentId($entity) + { + $metaInformation = $this->metaInformationFactory->loadInformation($entity); + + /** @var Query $select */ + $select = $this->solariumClient->createQuery(SolariumClient::QUERY_SELECT); + $select->setQuery(sprintf('id:%s*', $metaInformation->getDocumentKey())); + $select->setRows($this->getNumberOfDocuments($metaInformation->getDocumentName())); + $select->addFields(array('id')); + + $result = $this->solariumClient->select($select); + + if ($result->count() == 0) { + return 0; + } + + $ids = array_map(function ($document) { + return substr($document->id, stripos($document->id, '_') + 1); + }, $result->getIterator()->getArrayCopy()); + + return intval(max($ids)); + } + + /** + * @param string $documentKey + * + * @return int + */ + private function getNumberOfDocuments($documentKey) + { + $select = $this->solariumClient->createQuery(SolariumClient::QUERY_SELECT); + $select->setQuery(sprintf('id:%s_*', $documentKey)); + + $result = $this->solariumClient->select($select); + + return $result->getNumFound(); + } +} \ No newline at end of file diff --git a/Logging/DebugLogger.php b/Logging/DebugLogger.php index 734827d5..e58c19f0 100644 --- a/Logging/DebugLogger.php +++ b/Logging/DebugLogger.php @@ -33,7 +33,7 @@ public function getQueries() /** * {@inheritdoc} */ - public function startRequest($request) + public function startRequest(array $request) { $this->start = microtime(true); $this->queries[++$this->currentQuery] = [ diff --git a/Logging/LoggerChain.php b/Logging/LoggerChain.php deleted file mode 100644 index dc110bfb..00000000 --- a/Logging/LoggerChain.php +++ /dev/null @@ -1,39 +0,0 @@ -loggers, $logger); - } - - /** - * {@inheritdoc} - */ - public function startRequest($request) - { - foreach ($this->loggers as $logger) { - $logger->startRequest($request); - } - } - - /** - * {@inheritdoc} - */ - public function stopRequest() - { - foreach ($this->loggers as $logger) { - $logger->stopRequest(); - } - } -} \ No newline at end of file diff --git a/Logging/SolrLoggerInterface.php b/Logging/SolrLoggerInterface.php index 72bfd8f0..324d1347 100644 --- a/Logging/SolrLoggerInterface.php +++ b/Logging/SolrLoggerInterface.php @@ -7,16 +7,12 @@ interface SolrLoggerInterface /** * Called when the request is started * - * @param string $request - * - * @return mixed + * @param array $request */ - public function startRequest($request); + public function startRequest(array $request); /** * Called when the request has ended - * - * @return mixed */ public function stopRequest(); } \ No newline at end of file diff --git a/Query/SolrQuery.php b/Query/SolrQuery.php index c4dfb6cf..d0b3c0b6 100644 --- a/Query/SolrQuery.php +++ b/Query/SolrQuery.php @@ -10,12 +10,17 @@ class SolrQuery extends AbstractQuery /** * @var array */ - private $mappedFields = array(); + private $mappedFields = []; /** * @var array */ - private $searchTerms = array(); + private $searchTerms = []; + + /** + * @var array + */ + private $childQueries = []; /** * @var bool @@ -132,7 +137,20 @@ public function addSearchTerm($field, $value) } $documentFieldName = $documentFieldsAsValues[$field]; - $this->searchTerms[$documentFieldName] = $value; + if ($position = strpos($field, '.')) { + $nestedFieldMapping = $documentFieldsAsValues[$field]; + + $nestedField = substr($nestedFieldMapping, $position + 1); + + $documentName = $this->getMetaInformation()->getDocumentName(); + $documentFieldName = sprintf('{!parent which="id:%s_*"}%s', $documentName, $nestedField); + $childFilterPhrase = str_replace('"', '*', $value); + $childFilterPhrase = str_replace(' ', '*', $value); + $childFilterPhrase = str_replace('\*', '*', $value); + $this->childQueries[$documentFieldName] = $childFilterPhrase; + } else { + $this->searchTerms[$documentFieldName] = $value; + } return $this; } @@ -169,6 +187,8 @@ public function addFilterQuery($filterQuery) */ public function getQuery() { + $searchTerms = array_merge($this->searchTerms, $this->childQueries); + $keyField = $this->getMetaInformation()->getDocumentKey(); $documentLimitation = $this->createFilterQuery('id')->setQuery('id:'.$keyField.'*'); @@ -182,7 +202,7 @@ public function getQuery() $term = ''; // query all documents if no terms exists - if (count($this->searchTerms) == 0) { + if (count($searchTerms) == 0) { $query = '*:*'; parent::setQuery($query); @@ -195,7 +215,7 @@ public function getQuery() } $termCount = 1; - foreach ($this->searchTerms as $fieldName => $fieldValue) { + foreach ($searchTerms as $fieldName => $fieldValue) { if ($fieldName == 'id') { $this->getFilterQuery('id')->setQuery('id:' . $fieldValue); @@ -209,7 +229,7 @@ public function getQuery() $term .= $fieldName . ':' . $fieldValue; - if ($termCount < count($this->searchTerms)) { + if ($termCount < count($searchTerms)) { $term .= ' ' . $logicOperator . ' '; } diff --git a/README.md b/README.md index b06410b5..7abfcc60 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Finally, configure the bundle: fs_solr: endpoints: core0: + schema: http host: host port: 8983 path: /solr/core0 @@ -59,10 +60,29 @@ fs_solr: timeout: 5 ``` +Default values will be used for any option left out. + +#### With DSN + +``` yaml +# app/config/config.yml +fs_solr: + endpoints: + core0: + dsn: http://host:8983/solr + core: core0 + timeout: 5 +``` + +Any values in `schema`, `host`, `port` and `path` option, will be ignored if you use the `dsn` option. + ### Step 4: Configure your entities To make an entity indexed, you must add some annotations to your entity. Basic configuration requires two annotations: -`@Solr\Document()`, `@Solr\Id()`. To index data add `@Solr\Field()` to your properties. +`@Solr\Document()`, `@Solr\Id()`. To index data add `@Solr\Field()` to your properties. + +If you want to index documents without any database, then you have to use the same annotations. Make sure you have set a Id or +set `@Solr\Id(generateId=true)`. ```php // .... @@ -222,70 +242,13 @@ Solr supports partial updates of fields in an existing document. Supported value - remove (multivalue field only, removes a value(s) from existing list) - inc (integer field only) +### `nestedClass` property -### Object relations - -Indexing relations works in simplified way. Related entities will not be indexed as a new document, but only as a searchable value. -Related entities do not need a `@Solr\Document` annotation. - -#### ManyToOne relation - -```php -/** - * @var Category - * - * @Solr\Field(type="string", getter="getTitle") - * - * @ORM\ManyToOne(targetEntity="Acme\DemoBundle\Entity\Category", inversedBy="posts", cascade={"persist"}) - * @ORM\JoinColumn(name="category_id", referencedColumnName="id") - */ -private $category; -``` - -Related entity: - -```php -class Category -{ - /** - * @return string - */ - public function getTitle() - { - return $this->title; - } -} -``` - -#### OneToMany relation - -To index a set of objects it is important to use the fieldtype `strings`. +Set this property if you want to index collections with nested Objects. -```php -/** - * @var Tag[] - * - * @Solr\Field(type="strings", getter="getName") - * - * @ORM\OneToMany(targetEntity="Acme\DemoBundle\Entity\Tag", mappedBy="post", cascade={"persist"}) - */ -private $tags; -``` -Related entity: -```php -class Tag -{ - /** - * @return string - */ - public function getName() - { - return $this->name; - } -} -``` +### Object relations [For more information read the more detailed "How to index relation" guide](Resources/doc/index_relations.md) @@ -394,29 +357,6 @@ class YourRepository extends Repository ``` - -### Define Result-Mapping - -To narrow the mapping, you can use the `addField()` method. - -```php -$query = $this->get('solr.client')->createQuery('AcmeDemoBundle:Post'); -$query->addSearchTerm('title', 'my title'); -$query->addField('id'); -$query->addField('text'); - -$result = $query->getResult(); -``` - -In this case, only the `id` and `text` fields will be mapped (addField()), `title` and created_at` fields will be -empty. If nothing was found $result is empty. - -By default, the result set contains 10 rows. You can increase this value: - -```php -$query->setRows(1000000); -``` - ### Configure HydrationModes HydrationMode tells the bundle how to create an entity from a document. @@ -490,3 +430,12 @@ To hook into the [Solarium events](http://solarium.readthedocs.io/en/stable/cust ```xml ``` + +## Document helper + +### Retrieve the last insert entity-id + +```php +$helper = $this->get('solr.client')->getDocumentHelper(); +$id = $helper->getLastInsertDocumentId(); +``` \ No newline at end of file diff --git a/Repository/Repository.php b/Repository/Repository.php index 1b056a73..35d810ce 100644 --- a/Repository/Repository.php +++ b/Repository/Repository.php @@ -47,9 +47,11 @@ public function __construct(SolrInterface $solr, MetaInformationInterface $metaI */ public function find($id) { + $documentKey = $this->metaInformation->getDocumentName() . '_' . $id; + $query = new FindByIdentifierQuery(); $query->setIndex($this->metaInformation->getIndex()); - $query->setDocumentKey($this->metaInformation->getDocumentKey()); + $query->setDocumentKey($documentKey); $query->setEntity($this->metaInformation->getEntity()); $query->setSolr($this->solr); $query->setHydrationMode($this->hydrationMode); diff --git a/Resources/doc/index_relations.md b/Resources/doc/index_relations.md index 8d3e7de5..8c432088 100644 --- a/Resources/doc/index_relations.md +++ b/Resources/doc/index_relations.md @@ -10,8 +10,6 @@ Given you have the following entity with a ManyToOne relation to `Category`. use FS\SolrBundle\Doctrine\Annotation as Solr; /** - * Post - * * @Solr\Document() * * @ORM\Table() @@ -22,9 +20,7 @@ class Post /** * @var integer * - * @ORM\Column(name="id", type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") + * orm stuff * * @Solr\Id */ @@ -53,6 +49,13 @@ class Post } ``` +You have now different ways to index the `category` relation: + +- flat string representation +- full object + +## Flat string representation + The important configuration is `@Solr\Field(type="string", getter="getTitle")`. This tells Solr to call `Category::getTitle()` when the `Post` is indexed. ```php @@ -69,20 +72,7 @@ $em->persist($post); $em->flush(); ``` -The index data would look something like this: - -```json -"docs": [ - { - "id": "post_1", - "title_s": "a post title", - "category_s": "post category #1", - "_version_": 1529771282767282200 - } -] -``` - -The result of search-queries like this +### Quering the relation ```php $posts = $this->get('solr.client')->getRepository('AcmeDemoBundle:Post')->findOneBy(array( @@ -90,12 +80,17 @@ $posts = $this->get('solr.client')->getRepository('AcmeDemoBundle:Post')->findOn )); ``` -contain a `Post` entity with a `Category` entity. The indexed data `post category #1` was replaced by DB reference. - # Index OneToMany relation Given you have the following `Post` entity with a OneToMany relation to `Tag`. +Again you can index the collection in two ways: + +- flat strings representation +- full objects + +## flat strings representation + ```php get('solr.client')->getRepository('AcmeDemoBundle:Post')->findOn 'tags' => 'tag #1' )); ``` - \ No newline at end of file + +## Index full objects + +Post entity: + +```php + /** + * @Solr\Field(type="strings", nestedClass="Acme\DemoBundle\Entity\Tag") + * + * @ORM\OneToMany(targetEntity="Acme\DemoBundle\Entity\Tag", mappedBy="post", cascade={"persist"}) + */ + private $tags; +``` + +Mark the `Tag` entity as Nested + +```php +/** + * Tag + * + * @Solr\Nested() + * + * @ORM\Table() + * @ORM\Entity + */ +class Tag +{ + /** + * @var integer + * + * @Solr\Id + * + * orm stuff + */ + private $id; + + /** + * @var string + * + * @Solr\Field(type="string") + * + * @ORM\Column(name="name", type="string", length=255) + */ + private $name; + + // getter and setter +} +``` + +## Querying the collection + +Now `Post` can be searched like this + +```php +$posts = $this->get('solr.client')->getRepository('AcmeDemoBundle:Post')->findOneBy(array( + 'tags.name' => 'tag #1' +)); +``` + diff --git a/Resources/views/Profiler/icon.svg b/Resources/views/Profiler/icon.svg index 3567873c..8b9214fa 100644 --- a/Resources/views/Profiler/icon.svg +++ b/Resources/views/Profiler/icon.svg @@ -1,7 +1,3 @@ - - - - - - + + \ No newline at end of file diff --git a/Resources/views/Profiler/solr.html.twig b/Resources/views/Profiler/solr.html.twig index c670c083..7c2036b8 100644 --- a/Resources/views/Profiler/solr.html.twig +++ b/Resources/views/Profiler/solr.html.twig @@ -3,44 +3,53 @@ {% block toolbar %} {% set profiler_markup_version = profiler_markup_version|default(1) %} - {% set icon %} + {% if collector.querycount > 0 %} {% if profiler_markup_version == 1 %} - - - {{ collector.querycount }} - {% if collector.querycount > 0 %} - in {{ '%0.2f'|format(collector.time * 1000) }} ms - {% endif %} - + {% set icon %} + + {{ collector.querycount }} + {% if collector.querycount > 0 %} + in {{ '%0.2f'|format(collector.time * 1000) }} ms + {% endif %} + {% endset %} + + {% set text %} + + Solr queries + {{ collector.querycount }} + + + Query time + {{ '%0.2f'|format(collector.time * 1000) }} ms + + {% endset %} {% else %} - {{ include('@FSSolr/Profiler/icon.svg') }} - - {% if collector.querycount > 0 %} - {% set status = collector.querycount > 50 ? 'yellow' %} + {% set status = collector.querycount > 50 ? 'yellow' %} + {% set icon %} + {{ include('@FSSolr/Profiler/icon.svg') }} {{ collector.querycount }} in {{ '%0.2f'|format(collector.time * 1000) }} ms - {% endif %} - + {% endset %} + + {% set text %} + + Solr queries + {{ collector.querycount }} + + + Query time + {{ '%0.2f'|format(collector.time * 1000) }} ms + + {% endset %} {% endif %} - {% endset %} - - {% set text %} - - Solr Queries - {{ collector.querycount }} - - - Query time - {{ '%0.2f'|format(collector.time * 1000) }} ms - - {% endset %} - {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status|default('') }) }} + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status|default('') }) }} + {% endif %} {% endblock %} {% block menu %} @@ -49,7 +58,7 @@ {% if profiler_markup_version == 1 %} - + Solr {{ collector.querycount }} @@ -75,6 +84,7 @@ {% block queries %} {% if profiler_markup_version == 1 %} + - {% endif %} - Queries + Solr Queries + + {% else %} + + Query Metrics + + + {{ collector.querycount }} + Solr queries + + + + {{ '%0.2f'|format(collector.time * 1000) }} ms + Query time + + + + Queries + + {% endif %} {% if collector.queries is empty %} @@ -105,7 +133,9 @@ #▲ Time End Point + Method Parameters + Raw Data @@ -117,7 +147,17 @@ {{ query.endpoint }} - {{ query.params | replace({'&': ""}) | raw }} + {{ query.method }} + + + {% if query.stub is not defined %} + {{ query.params | replace({'&': ""}) | raw }} + {% else %} + {{ profiler_dump(query.stub, 1) }} + {% endif %} + + + {{ query.raw_data }} {% endfor %} diff --git a/Solr.php b/Solr.php index f7f82aad..9c303a93 100644 --- a/Solr.php +++ b/Solr.php @@ -1,18 +1,20 @@ solrClientCore; } @@ -84,7 +87,7 @@ public function getClient() /** * @return EntityMapper */ - public function getMapper() + public function getMapper(): EntityMapper { return $this->entityMapper; } @@ -92,17 +95,25 @@ public function getMapper() /** * @return MetaInformationFactory */ - public function getMetaFactory() + public function getMetaFactory(): MetaInformationFactory { return $this->metaInformationFactory; } + /** + * @return DocumentHelper + */ + public function getDocumentHelper() + { + return new DocumentHelper($this); + } + /** * @param object|string $entity entity, entity-alias or classname * * @return SolrQuery */ - public function createQuery($entity) + public function createQuery($entity): SolrQuery { $metaInformation = $this->metaInformationFactory->loadInformation($entity); @@ -121,7 +132,7 @@ public function createQuery($entity) * * @return QueryBuilderInterface */ - public function getQueryBuilder($entity) + public function getQueryBuilder($entity): QueryBuilderInterface { $metaInformation = $this->metaInformationFactory->loadInformation($entity); @@ -131,7 +142,7 @@ public function getQueryBuilder($entity) /** * {@inheritdoc} */ - public function getRepository($entity) + public function getRepository($entity): RepositoryInterface { $metaInformation = $this->metaInformationFactory->loadInformation($entity); @@ -152,7 +163,7 @@ public function getRepository($entity) /** * {@inheritdoc} */ - public function createQueryBuilder($entity) + public function createQueryBuilder($entity): QueryBuilderInterface { $metaInformation = $this->metaInformationFactory->loadInformation($entity); @@ -164,21 +175,21 @@ public function createQueryBuilder($entity) */ public function removeDocument($entity) { - $metaInformations = $this->metaInformationFactory->loadInformation($entity); + $metaInformation = $this->metaInformationFactory->loadInformation($entity); - $event = new Event($this->solrClientCore, $metaInformations); + $event = new Event($this->solrClientCore, $metaInformation); $this->eventManager->dispatch(Events::PRE_DELETE, $event); - if ($document = $this->entityMapper->toDocument($metaInformations)) { + if ($document = $this->entityMapper->toDocument($metaInformation)) { try { - $indexName = $metaInformations->getIndex(); + $indexName = $metaInformation->getIndex(); $client = new SolariumMulticoreClient($this->solrClientCore); $client->delete($document, $indexName); } catch (\Exception $e) { - $errorEvent = new ErrorEvent(null, $metaInformations, 'delete-document', $event); + $errorEvent = new ErrorEvent(null, $metaInformation, 'delete-document', $event); $errorEvent->setException($e); $this->eventManager->dispatch(Events::ERROR, $errorEvent); @@ -193,7 +204,7 @@ public function removeDocument($entity) /** * {@inheritdoc} */ - public function addDocument($entity) + public function addDocument($entity): bool { $metaInformation = $this->metaInformationFactory->loadInformation($entity); @@ -201,6 +212,10 @@ public function addDocument($entity) return false; } + if ($metaInformation->isNested()) { + return false; + } + $event = new Event($this->solrClientCore, $metaInformation); $this->eventManager->dispatch(Events::PRE_INSERT, $event); @@ -209,6 +224,8 @@ public function addDocument($entity) $this->addDocumentToIndex($doc, $metaInformation, $event); $this->eventManager->dispatch(Events::POST_INSERT, $event); + + return true; } /** @@ -219,7 +236,7 @@ public function addDocument($entity) * * @throws SolrException if callback method not exists */ - private function addToIndex(MetaInformationInterface $metaInformation, $entity) + private function addToIndex(MetaInformationInterface $metaInformation, $entity): bool { if (!$metaInformation->hasSynchronizationFilter()) { return true; @@ -238,9 +255,9 @@ private function addToIndex(MetaInformationInterface $metaInformation, $entity) * * @param AbstractQuery $query * - * @return \Solarium\QueryType\Select\Query\Query + * @return SolariumQuery */ - public function getSelectQuery(AbstractQuery $query) + public function getSelectQuery(AbstractQuery $query): SolariumQuery { $selectQuery = $this->solrClientCore->createSelect($query->getOptions()); @@ -255,7 +272,7 @@ public function getSelectQuery(AbstractQuery $query) /** * {@inheritdoc} */ - public function query(AbstractQuery $query) + public function query(AbstractQuery $query): array { $entity = $query->getEntity(); $runQueryInIndex = $query->getIndex(); @@ -287,13 +304,15 @@ public function query(AbstractQuery $query) * * @return integer */ - public function getNumFound() + public function getNumFound(): int { return $this->numberOfFoundDocuments; } /** * clears the whole index by using the query *:* + * + * @throws SolrException if an error occurs */ public function clearIndex() { @@ -351,7 +370,7 @@ public function synchronizeIndex($entities) /** * {@inheritdoc} */ - public function updateDocument($entity) + public function updateDocument($entity): bool { $metaInformations = $this->metaInformationFactory->loadInformation($entity); @@ -374,9 +393,9 @@ public function updateDocument($entity) /** * @param MetaInformationInterface $metaInformation * - * @return Document + * @return DocumentInterface */ - private function toDocument(MetaInformationInterface $metaInformation) + private function toDocument(MetaInformationInterface $metaInformation): DocumentInterface { $doc = $this->entityMapper->toDocument($metaInformation); @@ -388,7 +407,7 @@ private function toDocument(MetaInformationInterface $metaInformation) * @param MetaInformationInterface $metaInformation * @param Event $event * - * @throws SolrException + * @throws SolrException if an error occurs */ private function addDocumentToIndex($doc, MetaInformationInterface $metaInformation, Event $event) { diff --git a/SolrInterface.php b/SolrInterface.php index adf12feb..8ac1b3c4 100644 --- a/SolrInterface.php +++ b/SolrInterface.php @@ -5,6 +5,7 @@ use FS\SolrBundle\Query\AbstractQuery; use FS\SolrBundle\Query\QueryBuilderInterface; use FS\SolrBundle\Repository\Repository; +use FS\SolrBundle\Repository\RepositoryInterface; interface SolrInterface { @@ -19,7 +20,7 @@ public function removeDocument($entity); * * @return bool */ - public function addDocument($entity); + public function addDocument($entity): bool; /** * @param AbstractQuery $query @@ -28,28 +29,28 @@ public function addDocument($entity); * * @throws SolrException */ - public function query(AbstractQuery $query); + public function query(AbstractQuery $query): array; /** * @param object|string $entity entity, entity-alias or classname * * @return bool */ - public function updateDocument($entity); + public function updateDocument($entity): bool; /** * @param object|string $entity entity, entity-alias or classname * - * @return Repository + * @return RepositoryInterface * - * @throws \RuntimeException if repository of the given $entityAlias does not extend FS\SolrBundle\Repository\Repository + * @throws SolrException if repository of the given $entityAlias does not extend FS\SolrBundle\Repository\Repository */ - public function getRepository($entity); + public function getRepository($entity): RepositoryInterface; /** * @param object|string $entity entity, entity-alias or classname * * @return QueryBuilderInterface */ - public function createQueryBuilder($entity); + public function createQueryBuilder($entity): QueryBuilderInterface; } \ No newline at end of file diff --git a/Tests/Client/Solarium/SolariumClientBuilderTest.php b/Tests/Client/Solarium/SolariumClientBuilderTest.php new file mode 100644 index 00000000..8a9723c0 --- /dev/null +++ b/Tests/Client/Solarium/SolariumClientBuilderTest.php @@ -0,0 +1,137 @@ +defaultEndpoints = [ + 'unittest' => [ + 'schema' => 'http', + 'host' => '127.0.0.1', + 'port' => 8983, + 'path' => '/solr', + 'timeout' => 5, + 'core' => null + ] + ]; + } + + public function testCreateClientWithoutDsn() + { + $actual = $this->createClientWithSettings($this->defaultEndpoints); + + $endpoint = $actual->getEndpoint('unittest'); + $this->assertEquals('http://127.0.0.1:8983/solr/', $endpoint->getBaseUri()); + } + + public function testCreateClientWithoutDsnWithCore() + { + $this->defaultEndpoints['unittest']['core'] = 'core0'; + + $actual = $this->createClientWithSettings($this->defaultEndpoints); + + $endpoint = $actual->getEndpoint('unittest'); + $this->assertEquals('http://127.0.0.1:8983/solr/core0/', $endpoint->getBaseUri()); + } + + /** + * @param string $dsn + * @param string $expectedBaseUri + * @param string $message + * + * @dataProvider dsnProvider + */ + public function testCreateClientWithDsn($dsn, $expectedBaseUri, $message) + { + $settings = $this->defaultEndpoints; + $settings['unittest'] = [ + 'dsn' => $dsn + ]; + + $actual = $this->createClientWithSettings($settings); + + $endpoint = $actual->getEndpoint('unittest'); + $this->assertEquals($expectedBaseUri, $endpoint->getBaseUri(), $message); + } + + /** + * @param string $dsn + * @param string $expectedBaseUri + * @param string $message + * + * @dataProvider dsnProvider + */ + public function testCreateClientWithDsnAndCore($dsn, $expectedBaseUri, $message) + { + $settings = $this->defaultEndpoints; + $settings['unittest'] = [ + 'dsn' => $dsn, + 'core' => 'core0' + ]; + + $actual = $this->createClientWithSettings($settings); + + $endpoint = $actual->getEndpoint('unittest'); + $this->assertEquals($expectedBaseUri . 'core0/', $endpoint->getBaseUri(), $message . ' with core'); + } + + /** + * @return array + */ + public function dsnProvider() + { + return [ + [ + 'http://example.com:1234', + 'http://example.com:1234/', + 'Test DSN without path and any authentication' + ], + [ + 'http://example.com:1234/solr', + 'http://example.com:1234/solr/', + 'Test DSN without any authentication' + ], + [ + 'http://user@example.com:1234/solr', + 'http://user@example.com:1234/solr/', + 'Test DSN with user-only authentication' + ], + [ + 'http://user:secret@example.com:1234/solr', + 'http://user:secret@example.com:1234/solr/', + 'Test DSN with authentication' + ], + [ + 'https://example.com:1234/solr', + 'https://example.com:1234/solr/', + 'Test DSN with HTTPS' + ] + ]; + } + + /** + * @param array $settings + * + * @return \Solarium\Client + */ + private function createClientWithSettings(array $settings) + { + /** @var EventDispatcherInterface $eventDispatcherMock */ + $eventDispatcherMock = $this->createMock(EventDispatcherInterface::class); + + return (new SolariumClientBuilder($settings, $eventDispatcherMock))->build(); + } +} diff --git a/Tests/Doctrine/Annotation/AnnotationReaderTest.php b/Tests/Doctrine/Annotation/AnnotationReaderTest.php index 0bd7b20b..508a4677 100644 --- a/Tests/Doctrine/Annotation/AnnotationReaderTest.php +++ b/Tests/Doctrine/Annotation/AnnotationReaderTest.php @@ -195,6 +195,14 @@ public function readAnnotationsFromBaseClass() $this->assertTrue($this->reader->hasDocumentDeclaration(new ChildEntity())); } + /** + * @test + */ + public function readAnnotationsOfNestedObject() + { + $this->assertTrue($this->reader->hasDocumentDeclaration(new NestedObject())); + } + /** * @test */ @@ -251,6 +259,15 @@ public function checkIfValidDocumentIsDoctrineDocument() { $this->assertTrue($this->reader->isOdm(new ValidOdmTestDocument()), 'is a doctrine document'); } + + /** + * @test + * @expectedException \FS\SolrBundle\Doctrine\Mapper\SolrMappingException + */ + public function methodWithAnnotationMustHaveAField() + { + $this->reader->getMethods(new EntityMissingNameProperty()); + } } use FS\SolrBundle\Doctrine\Annotation as Solr; @@ -300,4 +317,19 @@ class EntityWithObject * @Solr\Field(type="datetime", getter="format('d.m.Y')") */ private $object; +} + +/** + * @Solr\Nested() + */ +class NestedObject {} + +/** @Solr\Document() */ +class EntityMissingNameProperty { + + /** @Solr\Field(type="string") */ + public function getPropertyValue2() + { + return 1234; + } } \ No newline at end of file diff --git a/Tests/Doctrine/Mapper/EntityMapperObjectRelationTest.php b/Tests/Doctrine/Mapper/EntityMapperObjectRelationTest.php new file mode 100644 index 00000000..83148f5b --- /dev/null +++ b/Tests/Doctrine/Mapper/EntityMapperObjectRelationTest.php @@ -0,0 +1,358 @@ +doctrineHydrator = $this->createMock(HydratorInterface::class); + $this->indexHydrator = $this->createMock(HydratorInterface::class); + $this->metaInformationFactory = new MetaInformationFactory(new AnnotationReader(new \Doctrine\Common\Annotations\AnnotationReader())); + + $this->mapper = new EntityMapper($this->doctrineHydrator, $this->indexHydrator, $this->metaInformationFactory); + } + + /** + * @test + */ + public function mapRelationFieldByGetter() + { + $collectionItem1 = new NestedEntity(); + $collectionItem1->setId(uniqid()); + $collectionItem1->setName('title 1'); + + $collectionItem2 = new NestedEntity(); + $collectionItem2->setId(uniqid()); + $collectionItem2->setName('title 2'); + + $collection = new ArrayCollection([$collectionItem1, $collectionItem2]); + + $entity = new EntityNestedProperty(); + $entity->setId(uniqid()); + $entity->setCollectionValidGetter($collection); + + $metaInformation = $this->metaInformationFactory->loadInformation($entity); + + $document = $this->mapper->toDocument($metaInformation); + + $this->assertArrayHasKey('_childDocuments_', $document->getFields()); + $collectionField = $document->getFields()['_childDocuments_']; + + $this->assertCollectionItemsMappedProperly($collectionField, 1); + } + + /** + * @test + * @expectedException \FS\SolrBundle\Doctrine\Mapper\SolrMappingException + * @expectedExceptionMessage No method "unknown()" found in class "FS\SolrBundle\Tests\Fixtures\EntityNestedProperty" + */ + public function throwExceptionIfConfiguredGetterDoesNotExists() + { + $collection = new ArrayCollection([new \DateTime(), new \DateTime()]); + + $entity = new EntityNestedProperty(); + $entity->setId(uniqid()); + $entity->setCollectionInvalidGetter($collection); + + $metaInformation = $this->metaInformationFactory->loadInformation($entity); + + $this->mapper->toDocument($metaInformation); + } + + /** + * @test + */ + public function mapRelationFieldAllFields() + { + $collectionItem1 = new NestedEntity(); + $collectionItem1->setId(uniqid()); + $collectionItem1->setName('title 1'); + + $collectionItem2 = new NestedEntity(); + $collectionItem2->setId(uniqid()); + $collectionItem2->setName('title 2'); + + $collection = new ArrayCollection([$collectionItem1, $collectionItem2]); + + $entity = new EntityNestedProperty(); + $entity->setId(uniqid()); + $entity->setCollection($collection); + + $metaInformation = $this->metaInformationFactory->loadInformation($entity); + + $document = $this->mapper->toDocument($metaInformation); + + $this->assertArrayHasKey('_childDocuments_', $document->getFields()); + $collectionField = $document->getFields()['_childDocuments_']; + + $this->assertCollectionItemsMappedProperly($collectionField, 2); + } + + /** + * @test + */ + public function mapEntityWithRelation_singleObject() + { + $entity = new EntityNestedProperty(); + $entity->setId(uniqid()); + + $nested1 = new NestedEntity(); + $nested1->setId(uniqid()); + $nested1->setName('nested document'); + + $entity->setNestedProperty($nested1); + + $metaInformation = $this->metaInformationFactory->loadInformation($entity); + + $document = $this->mapper->toDocument($metaInformation); + + $fields = $document->getFields(); + + $this->assertArrayHasKey('_childDocuments_', $fields); + + $subDocument = $fields['_childDocuments_'][0]; + + $this->assertArrayHasKey('id', $subDocument); + $this->assertArrayHasKey('name_t', $subDocument); + } + + /** + * @test + */ + public function indexEntityMultipleRelations() + { + $entity = new EntityNestedProperty(); + $entity->setId(uniqid()); + + $nested1 = new NestedEntity(); + $nested1->setId(uniqid()); + $nested1->setName('nested document'); + + $entity->setNestedProperty($nested1); + + $collectionItem1 = new NestedEntity(); + $collectionItem1->setId(uniqid()); + $collectionItem1->setName('collection item 1'); + + $collectionItem2 = new NestedEntity(); + $collectionItem2->setId(uniqid()); + $collectionItem2->setName('collection item 2'); + + $collection = new ArrayCollection([$collectionItem1, $collectionItem2]); + + $entity->setCollection($collection); + + $metaInformation = $this->metaInformationFactory->loadInformation($entity); + + $document = $this->mapper->toDocument($metaInformation); + + $fields = $document->getFields(); + + $this->assertEquals(3, count($fields['_childDocuments_'])); + } + + /** + * @test + */ + public function mapRelationField_Getter() + { + $entity = new EntityNestedProperty(); + $entity->setId(uniqid()); + + $object = new NestedEntity(); + $object->setId(uniqid()); + $object->setName('nested entity'); + + $entity->setSimpleGetter($object); + + $metaInformation = $this->metaInformationFactory->loadInformation($entity); + + $document = $this->mapper->toDocument($metaInformation); + + $this->assertArrayHasKey('simple_getter_s', $document->getFields()); + + $collectionField = $document->getFields()['simple_getter_s']; + + $this->assertEquals('nested entity', $collectionField); + } + + /** + * @test + */ + public function callGetterWithParameter_ObjectProperty() + { + $date = new \DateTime(); + + $entity = new EntityNestedProperty(); + $entity->setId(uniqid()); + $entity->setGetterWithParameters($date); + + $metaInformation = $this->metaInformationFactory->loadInformation($entity); + + $fields = $metaInformation->getFields(); + $metaInformation->setFields($fields); + + $document = $this->mapper->toDocument($metaInformation); + + $fields = $document->getFields(); + $this->assertArrayHasKey('getter_with_parameters_dt', $fields); + $this->assertEquals($date->format('d.m.Y'), $fields['getter_with_parameters_dt']); + } + + /** + * @test + */ + public function callGetterWithParameters_ObjectProperty() + { + $entity1 = new ValidTestEntity(); + + $metaInformation = MetaTestInformationFactory::getMetaInformation($entity1); + $metaInformation->setFields(array( + new Field(array('name' => 'test_field', 'type' => 'datetime', 'boost' => '1', 'value' => new TestObject(), 'getter' => "testGetter('string3', 'string1', 'string')")) + )); + + $fields = $metaInformation->getFields(); + $metaInformation->setFields($fields); + + $document = $this->mapper->toDocument($metaInformation); + + $fields = $document->getFields(); + + $this->assertArrayHasKey('test_field_dt', $fields); + $this->assertEquals(array('string3', 'string1', 'string'), $fields['test_field_dt']); + } + + /** + * @test + */ + public function callGetterWithParameter_SimpleProperty() + { + $data = ['key' => 'value']; + + $date = new \DateTime(); + $entity1 = new ValidTestEntity(); + $entity1->setId(uniqid()); + $entity1->setComplexDataType(json_encode($data)); + + $metaInformation = $this->metaInformationFactory->loadInformation($entity1); + + $document = $this->mapper->toDocument($metaInformation); + + $fields = $document->getFields(); + + $this->assertArrayHasKey('complex_data_type', $fields); + + $this->assertEquals($data, $fields['complex_data_type']); + } + + /** + * @test + * @expectedException \FS\SolrBundle\Doctrine\Mapper\SolrMappingException + * @expectedExceptionMessage The configured getter "asString" in "FS\SolrBundle\Tests\Doctrine\Mapper\TestObject" must return a string or array, got object + */ + public function callGetterWithObjectAsReturnValue() + { + $entity1 = new ValidTestEntity(); + + $metaInformation = MetaTestInformationFactory::getMetaInformation($entity1); + $metaInformation->setFields(array( + new Field(array('name' => 'test_field', 'type' => 'datetime', 'boost' => '1', 'value' => new TestObject(), 'getter' => "asString")) + )); + + $fields = $metaInformation->getFields(); + $metaInformation->setFields($fields); + + $this->mapper->toDocument($metaInformation); + } + + /** + * @test + */ + public function callGetterToRetrieveFieldValue() + { + $metainformation = $this->metaInformationFactory->loadInformation(new TestObject()); + + $document = $this->mapper->toDocument($metainformation); + + $fields = $document->getFields(); + + $this->assertArrayHasKey('property_s', $fields); + $this->assertEquals(1234, $fields['property_s']); + } + + /** + * @param array $collectionField + * @param int $expectedItems + */ + private function assertCollectionItemsMappedProperly($collectionField, $expectedItems) + { + $this->assertEquals($expectedItems, count($collectionField), 'should be 2 collection items'); + + foreach ($collectionField as $item) { + $this->assertArrayHasKey('id', $item); + $this->assertArrayHasKey('name_t', $item); + $this->assertEquals(2, count($item), 'field has 2 properties'); + } + } +} + +/** @Solr\Document() */ +class TestObject { + + /** @Solr\Id */ + private $id; + + public function __construct() + { + $this->id = uniqid(); + } + + public function getId() + { + return $this->id; + } + + /** @Solr\Field(type="string", name="property") */ + public function getPropertyValue() + { + return 1234; + } + + public function testGetter($para1, $para2, $para3) + { + return array($para1, $para2, $para3); + } + + public function asString() + { + return $this; + } +} \ No newline at end of file diff --git a/Tests/Doctrine/Mapper/EntityMapperTest.php b/Tests/Doctrine/Mapper/EntityMapperTest.php index 07d3aa8f..7cd18916 100644 --- a/Tests/Doctrine/Mapper/EntityMapperTest.php +++ b/Tests/Doctrine/Mapper/EntityMapperTest.php @@ -189,115 +189,6 @@ public function testMapEntity_DocumentShouldContainThreeFields() $this->assertArrayHasKey('created_at_dt', $document); } - /** - * @test - */ - public function mapRelationFieldByGetter() - { - $entity1 = new ValidTestEntity(); - $entity1->setTitle('title 1'); - - $entity2 = new ValidTestEntity(); - $entity2->setTitle('title 2'); - - $collection = new ArrayCollection(); - $collection->add($entity1); - $collection->add($entity2); - - $metaInformation = MetaTestInformationFactory::getMetaInformation(new ValidTestEntityWithCollection()); - $fields = $metaInformation->getFields(); - $fields[] = new Field(array('name' => 'collection', 'type' => 'strings', 'boost' => '1', 'value' => $collection, 'getter'=>'getTitle')); - $metaInformation->setFields($fields); - - $document = $this->mapper->toDocument($metaInformation); - - $this->assertArrayHasKey('collection_ss', $document->getFields()); - $collectionField = $document->getFields()['collection_ss']; - - $this->assertEquals(2, count($collectionField)); - } - - /** - * @test - * @expectedException \FS\SolrBundle\Doctrine\Mapper\SolrMappingException - * @expectedExceptionMessage No method "unknown()" found in class "DateTime" - */ - public function throwExceptionIfConfiguredGetterDoesNotExists() - { - $entity1 = new \DateTime('+2 days'); - - $entity2 = new \DateTime('+1 day'); - - $collection = new ArrayCollection(); - $collection->add($entity1); - $collection->add($entity2); - - $metaInformation = MetaTestInformationFactory::getMetaInformation(new ValidTestEntityWithCollection()); - $fields = $metaInformation->getFields(); - $fields[] = new Field(array('name' => 'collection', 'type' => 'strings', 'boost' => '1', 'value' => $collection, 'getter'=>'unknown(\'d.m.Y\')')); - $metaInformation->setFields($fields); - - $this->mapper->toDocument($metaInformation); - } - - /** - * @test - */ - public function mapRelationFieldAllFields() - { - $this->markTestSkipped('sub-documents not yet supported'); - - $entity1 = new ValidTestEntity(); - $entity1->setId(uniqid()); - $entity1->setTitle('title 1'); - $entity1->setText('text 1'); - - $entity2 = new ValidTestEntity(); - $entity2->setId(uniqid()); - $entity2->setTitle('title 2'); - $entity2->setText('text 2'); - - $collection = new ArrayCollection(); - $collection->add($entity1); - $collection->add($entity2); - - $entity = new ValidTestEntityWithCollection(); - $entity->setId(uniqid()); - $entity->setCollectionNoGetter($collection); - - $metaInformation = $this->metaInformationFactory->loadInformation($entity); - - $document = $this->mapper->toDocument($metaInformation); - - $this->assertArrayHasKey('collection_no_getter_ss', $document->getFields()); - $collectionField = $document->getFields()['collection_no_getter_ss']; - - $this->assertEquals(2, count($collectionField), 'collection contains 2 fields'); - $this->assertEquals(3, count($collectionField[0]), 'field has 2 properties'); - } - - /** - * @test - * @expectedException \FS\SolrBundle\Doctrine\Mapper\SolrMappingException - * @expectedExceptionMessage No getter method for property "collection" configured in class "FS\SolrBundle\Tests\Fixtures\ValidTestEntityWithCollection" - */ - public function throwExceptionIfEmbbededObjectsHasNoGetter() - { - $entity2 = new ValidTestEntity(); - $entity2->setTitle('title 2'); - $entity2->setText('text 2'); - - $collection = new ArrayCollection(); - $collection->add($entity2); - - $metaInformation = MetaTestInformationFactory::getMetaInformation(new ValidTestEntityWithCollection()); - $fields = $metaInformation->getFields(); - $fields[] = new Field(array('name' => 'collection', 'type' => 'strings', 'boost' => '1', 'value' => $collection)); - $metaInformation->setFields($fields); - - $this->mapper->toDocument($metaInformation); - } - /** * @test * @expectedException \FS\SolrBundle\Doctrine\Mapper\SolrMappingException @@ -312,127 +203,6 @@ public function throwExceptionIfEntityHasNoId() $this->mapper->toDocument($metaInformation); } - /** - * @test - */ - public function mapRelationField_AllFields() - { - $entity2 = new ValidTestEntity(); - $entity2->setTitle('embbeded object'); - - $entity1 = new ValidTestEntityWithRelation(); - $entity1->setTitle('title 1'); - $entity1->setText('text 1'); - $entity1->setRelation($entity2); - - $metaInformation = MetaTestInformationFactory::getMetaInformation($entity1); - $fields = $metaInformation->getFields(); - $fields[] = new Field(array('name' => 'relation', 'type' => 'strings', 'boost' => '1', 'value' => $entity1)); - $metaInformation->setFields($fields); - - $document = $this->mapper->toDocument($metaInformation); - - $this->assertArrayHasKey('relation_ss', $document->getFields()); - $collectionField = $document->getFields()['relation_ss']; - - $this->assertEquals(4, count($collectionField), 'collection contains 4 fields'); - } - - /** - * @test - */ - public function mapRelationField_Getter() - { - $entity2 = new ValidTestEntity(); - $entity2->setTitle('embedded object'); - - $entity1 = new ValidTestEntityWithRelation(); - $entity1->setTitle('title 1'); - $entity1->setText('text 1'); - $entity1->setRelation($entity2); - - $metaInformation = MetaTestInformationFactory::getMetaInformation($entity1); - $fields = $metaInformation->getFields(); - $fields[] = new Field(array('name' => 'relation', 'type' => 'strings', 'boost' => '1', 'value' => $entity2, 'getter'=>'getTitle')); - $metaInformation->setFields($fields); - - $document = $this->mapper->toDocument($metaInformation); - - $this->assertArrayHasKey('relation_ss', $document->getFields()); - $collectionField = $document->getFields()['relation_ss']; - - $this->assertEquals('embedded object', $collectionField); - } - - /** - * @test - */ - public function callGetterWithParameter() - { - $data = ['key' => 'value']; - - $date = new \DateTime(); - $entity1 = new ValidTestEntity(); - $entity1->setId(uniqid()); - $entity1->setCreatedAt($date); - $entity1->setComplexDataType(json_encode($data)); - - $metaInformation = $this->metaInformationFactory->loadInformation($entity1); - - $document = $this->mapper->toDocument($metaInformation); - - $fields = $document->getFields(); - - $this->assertArrayHasKey('created_at_dt', $fields); - $this->assertEquals($date->format('d.m.Y'), $fields['created_at_dt']); - $this->assertArrayHasKey('complex_data_type', $fields); - - $this->assertEquals($data, $fields['complex_data_type']); - } - - /** - * @test - */ - public function callGetterWithParameters() - { - $entity1 = new ValidTestEntity(); - - $metaInformation = MetaTestInformationFactory::getMetaInformation($entity1); - $metaInformation->setFields(array( - new Field(array('name' => 'test_field', 'type' => 'datetime', 'boost' => '1', 'value' => new TestObject(), 'getter' => "testGetter('string3', 'string1', 'string')")) - )); - - $fields = $metaInformation->getFields(); - $metaInformation->setFields($fields); - - $document = $this->mapper->toDocument($metaInformation); - - $fields = $document->getFields(); - - $this->assertArrayHasKey('test_field_dt', $fields); - $this->assertEquals(array('string3', 'string1', 'string'), $fields['test_field_dt']); - } - - /** - * @test - * @expectedException \FS\SolrBundle\Doctrine\Mapper\SolrMappingException - * @expectedExceptionMessage The configured getter "asString" in "FS\SolrBundle\Tests\Doctrine\Mapper\TestObject" must return a string or array, got object - */ - public function callGetterWithObjectAsReturnValue() - { - $entity1 = new ValidTestEntity(); - - $metaInformation = MetaTestInformationFactory::getMetaInformation($entity1); - $metaInformation->setFields(array( - new Field(array('name' => 'test_field', 'type' => 'datetime', 'boost' => '1', 'value' => new TestObject(), 'getter' => "asString")) - )); - - $fields = $metaInformation->getFields(); - $metaInformation->setFields($fields); - - $this->mapper->toDocument($metaInformation); - } - private function setupOrmManager($entity, $expectedEntityId) { $repository = $this->createMock(ObjectRepository::class); @@ -489,16 +259,4 @@ class PlainObject * @Solr\Id */ private $id; -} - -class TestObject { - public function testGetter($para1, $para2, $para3) - { - return array($para1, $para2, $para3); - } - - public function asString() - { - return $this; - } } \ No newline at end of file diff --git a/Tests/Doctrine/Mapper/MetaInformationFactoryTest.php b/Tests/Doctrine/Mapper/MetaInformationFactoryTest.php index 3904622f..6fadad25 100644 --- a/Tests/Doctrine/Mapper/MetaInformationFactoryTest.php +++ b/Tests/Doctrine/Mapper/MetaInformationFactoryTest.php @@ -8,6 +8,8 @@ use FS\SolrBundle\Doctrine\Mapper\MetaInformationFactory; use FS\SolrBundle\Doctrine\Mapper\MetaInformation; use FS\SolrBundle\Doctrine\Mapper\MetaInformationInterface; +use FS\SolrBundle\Tests\Fixtures\EntityNestedProperty; +use FS\SolrBundle\Tests\Fixtures\NestedEntity; use FS\SolrBundle\Tests\Fixtures\NotIndexedEntity; use FS\SolrBundle\Tests\Fixtures\ValidOdmTestDocument; use FS\SolrBundle\Tests\Fixtures\ValidTestEntity; @@ -48,8 +50,6 @@ private function getClassnameResolverCouldNotResolveClassname() return $doctrineConfiguration; } - - public function testLoadInformation_ShouldLoadAll() { $testEntity = new ValidTestEntity(); @@ -169,5 +169,38 @@ public function determineDoctrineMapperTypeFromDocument() $this->assertEquals(MetaInformationInterface::DOCTRINE_MAPPER_TYPE_DOCUMENT, $metainformation->getDoctrineMapperType()); } + + /** + * @test + */ + public function useCachedEntityInstanceIfItIsSet() + { + $factory = new MetaInformationFactory($this->reader); + $metainformation1 = $factory->loadInformation(new ValidTestEntity()); + $metainformation2 = $factory->loadInformation(new ValidTestEntity()); + + $this->assertEquals($metainformation1->getEntity(), $metainformation2->getEntity()); + } + + /** + * @test + */ + public function includeNestedFieldsInFieldmapping() + { + $entity = new EntityNestedProperty(); + + $nested1 = new NestedEntity(); + $nested2 = new NestedEntity(); + $entity->setCollection([$nested1, $nested2]); + + $factory = new MetaInformationFactory($this->reader); + $metainformation = $factory->loadInformation($entity); + + $this->assertArrayNotHasKey('collection', $metainformation->getFieldMapping()); + $this->assertArrayHasKey('collection.id', $metainformation->getFieldMapping()); + $this->assertArrayHasKey('collection.name_t', $metainformation->getFieldMapping()); + + + } } diff --git a/Tests/Doctrine/ORM/Listener/EntityIndexerSubscriberTest.php b/Tests/Doctrine/ORM/Listener/EntityIndexerSubscriberTest.php new file mode 100644 index 00000000..289c8152 --- /dev/null +++ b/Tests/Doctrine/ORM/Listener/EntityIndexerSubscriberTest.php @@ -0,0 +1,119 @@ +logger = $this->createMock(LoggerInterface::class); + $this->solr = $this->createMock(SolrInterface::class); + $this->metaInformationFactory = new MetaInformationFactory(new AnnotationReader(new \Doctrine\Common\Annotations\AnnotationReader())); + + $this->subscriber = new EntityIndexerSubscriber($this->solr, $this->metaInformationFactory, $this->logger); + } + + /** + * @test + */ + public function separteDeletedRootEntitiesFromNested() + { + $nested = new NestedEntity(); + $nested->setId(uniqid()); + + $entity = new ValidTestEntityWithCollection(); + $entity->setId(uniqid()); + $entity->setCollection(new ArrayCollection([$nested])); + + $objectManager = $this->createMock(ObjectManager::class); + + $this->solr->expects($this->at(0)) + ->method('removeDocument') + ->with($this->callback(function(ValidTestEntityWithCollection $entity) { + if (count($entity->getCollection())) { + return false; + } + + return true; + })); + + $this->solr->expects($this->at(1)) + ->method('removeDocument') + ->with($this->callback(function($entity) { + if (!$entity instanceof NestedEntity) { + return false; + } + + return true; + })); + + $deleteRootEntityEvent = new LifecycleEventArgs($entity, $objectManager); + $this->subscriber->preRemove($deleteRootEntityEvent); + + $deleteNestedEntityEvent = new LifecycleEventArgs($nested, $objectManager); + $this->subscriber->preRemove($deleteNestedEntityEvent); + + $entityManager = $this->createMock(EntityManagerInterface::class); + + $this->subscriber->postFlush(new PostFlushEventArgs($entityManager)); + } + + /** + * @test + */ + public function indexOnlyModifiedEntites() + { + $changedEntity = new ValidTestEntityWithCollection(); + $this->solr->expects($this->once()) + ->method('updateDocument') + ->with($changedEntity); + + $unitOfWork = $this->createMock(UnitOfWork::class); + $unitOfWork->expects($this->at(0)) + ->method('getEntityChangeSet') + ->willReturn(['title' => 'value']); + + $unitOfWork->expects($this->at(1)) + ->method('getEntityChangeSet') + ->willReturn([]); + + $objectManager = $this->createMock(EntityManagerInterface::class); + $objectManager->expects($this->any()) + ->method('getUnitOfWork') + ->willReturn($unitOfWork); + + $updateEntityEvent1 = new LifecycleEventArgs($changedEntity, $objectManager); + + $unmodifiedEntity = new ValidTestEntityWithCollection(); + $updateEntityEvent2 = new LifecycleEventArgs($unmodifiedEntity, $objectManager); + + $this->subscriber->postUpdate($updateEntityEvent1); + $this->subscriber->postUpdate($updateEntityEvent2); + } +} diff --git a/Tests/Fixtures/EntityNestedProperty.php b/Tests/Fixtures/EntityNestedProperty.php new file mode 100644 index 00000000..f1fe0f02 --- /dev/null +++ b/Tests/Fixtures/EntityNestedProperty.php @@ -0,0 +1,142 @@ +id; + } + + /** + * @param mixed $id + */ + public function setId($id) + { + $this->id = $id; + } + + public function sliceCollection() + { + return [$this->collectionValidGetter[0]]; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + + /** + * @param array $collection + */ + public function setCollection($collection) + { + $this->collection = $collection; + } + + /** + * @param object $nestedProperty + */ + public function setNestedProperty($nestedProperty) + { + $this->nestedProperty = $nestedProperty; + } + + /** + * @param array $collectionValidGetter + */ + public function setCollectionValidGetter($collectionValidGetter) + { + $this->collectionValidGetter = $collectionValidGetter; + } + + /** + * @param array $collectionInvalidGetter + */ + public function setCollectionInvalidGetter($collectionInvalidGetter) + { + $this->collectionInvalidGetter = $collectionInvalidGetter; + } + + /** + * @param mixed $objectToSimpleFormat + */ + public function setGetterWithParameters($getterWithParameters) + { + $this->getterWithParameters = $getterWithParameters; + } + + /** + * @param mixed $simpleGetter + */ + public function setSimpleGetter($simpleGetter) + { + $this->simpleGetter = $simpleGetter; + } +} \ No newline at end of file diff --git a/Tests/Fixtures/NestedEntity.php b/Tests/Fixtures/NestedEntity.php new file mode 100644 index 00000000..e03df8b7 --- /dev/null +++ b/Tests/Fixtures/NestedEntity.php @@ -0,0 +1,59 @@ +id; + } + + /** + * @param mixed $id + */ + public function setId($id) + { + $this->id = $id; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + +} \ No newline at end of file diff --git a/Tests/Query/SolrQueryTest.php b/Tests/Query/SolrQueryTest.php index 2c4bf5b7..e973c589 100644 --- a/Tests/Query/SolrQueryTest.php +++ b/Tests/Query/SolrQueryTest.php @@ -5,6 +5,7 @@ use FS\SolrBundle\Doctrine\Annotation\AnnotationReader; use FS\SolrBundle\Doctrine\Annotation\Id; use FS\SolrBundle\Doctrine\Mapper\MetaInformation; +use FS\SolrBundle\Doctrine\Mapper\MetaInformationFactory; use FS\SolrBundle\Query\Exception\UnknownFieldException; use FS\SolrBundle\Query\SolrQuery; use FS\SolrBundle\SolrInterface; @@ -252,4 +253,24 @@ public function doNotAddIdFieldTwice() $this->assertEquals($expected, $query->getQuery()); $this->assertEquals('id:post_1', $query->getFilterQuery('id')->getQuery()); } + + /** + * @test + */ + public function generateQueryForNestedDocuments() + { + $mapping = [ + 'id' => 'id', + 'title_s' => 'title', + 'collection.id' => 'collection.id', + 'collection.name_s' => 'collection.name' + ]; + + $query = $this->createQueryWithFieldMapping(); + $query->setMappedFields($mapping); + $query->addSearchTerm('collection.name', 'test*bar'); + $query->addSearchTerm('title', 'test post'); + + $this->assertEquals('title_s:"test post" OR {!parent which="id:post_*"}name_s:test*bar', $query->getQuery()); + } } diff --git a/Tests/Repository/RepositoryTest.php b/Tests/Repository/RepositoryTest.php index 3d394478..e58e0145 100644 --- a/Tests/Repository/RepositoryTest.php +++ b/Tests/Repository/RepositoryTest.php @@ -10,6 +10,7 @@ use FS\SolrBundle\Query\AbstractQuery; use FS\SolrBundle\Query\FindByDocumentNameQuery; use FS\SolrBundle\Query\FindByIdentifierQuery; +use FS\SolrBundle\Tests\Fixtures\EntityNestedProperty; use FS\SolrBundle\Tests\SolrClientFake; use FS\SolrBundle\Tests\Util\MetaTestInformationFactory; use FS\SolrBundle\Tests\Util\CommandFactoryStub; @@ -28,9 +29,15 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase */ private $metaInformationFactory; + private $mapper; + protected function setUp() { $this->metaInformationFactory = new MetaInformationFactory($reader = new AnnotationReader(new \Doctrine\Common\Annotations\AnnotationReader())); + $this->mapper = $this->createMock(EntityMapperInterface::class); + $this->mapper->expects($this->once()) + ->method('setHydrationMode') + ->with(HydrationModes::HYDRATE_DOCTRINE); } public function testFind_DocumentIsKnown() @@ -41,15 +48,10 @@ public function testFind_DocumentIsKnown() $metaInformation = MetaTestInformationFactory::getMetaInformation(); - $mapper = $this->createMock(EntityMapperInterface::class); - $mapper->expects($this->once()) - ->method('setHydrationMode') - ->with(HydrationModes::HYDRATE_DOCTRINE); - $entity = new ValidTestEntity(); $solr = new SolrClientFake(); - $solr->mapper = $mapper; + $solr->mapper = $this->mapper; $solr->response = array($entity); $repo = new Repository($solr, $metaInformation); @@ -66,15 +68,10 @@ public function testFindAll() { $metaInformation = MetaTestInformationFactory::getMetaInformation(); - $mapper = $this->createMock(EntityMapperInterface::class); - $mapper->expects($this->once()) - ->method('setHydrationMode') - ->with(HydrationModes::HYDRATE_DOCTRINE); - $entity = new ValidTestEntity(); $solr = new SolrClientFake(); - $solr->mapper = $mapper; + $solr->mapper = $this->mapper; $solr->response = array($entity); $repo = new Repository($solr, $metaInformation); @@ -96,15 +93,10 @@ public function testFindBy() $metaInformation = MetaTestInformationFactory::getMetaInformation(); - $mapper = $this->createMock(EntityMapperInterface::class); - $mapper->expects($this->once()) - ->method('setHydrationMode') - ->with(HydrationModes::HYDRATE_DOCTRINE); - $entity = new ValidTestEntity(); $solr = new SolrClientFake(); - $solr->mapper = $mapper; + $solr->mapper = $this->mapper; $solr->response = array($entity); $solr->metaFactory = $this->metaInformationFactory; @@ -128,15 +120,10 @@ public function testFindOneBy() $metaInformation = MetaTestInformationFactory::getMetaInformation(); - $mapper = $this->createMock(EntityMapperInterface::class); - $mapper->expects($this->once()) - ->method('setHydrationMode') - ->with(HydrationModes::HYDRATE_DOCTRINE); - $entity = new ValidTestEntity(); $solr = new SolrClientFake(); - $solr->mapper = $mapper; + $solr->mapper = $this->mapper; $solr->response = array($entity); $solr->metaFactory = $this->metaInformationFactory; @@ -151,5 +138,27 @@ public function testFindOneBy() $this->assertEquals('id:validtestentity_*', $solr->query->getFilterQuery('id')->getQuery()); } + /** + * @test + */ + public function findOneByNestedField() + { + $metaInformation = $this->metaInformationFactory->loadInformation(EntityNestedProperty::class); + + $entity = new ValidTestEntity(); + + $solr = new SolrClientFake(); + $solr->mapper = $this->mapper; + $solr->response = array($entity); + $solr->metaFactory = $this->metaInformationFactory; + + $repo = new Repository($solr, $metaInformation); + + $found = $repo->findOneBy([ + 'collection.name' => '*test*test*' + ]); + + $this->assertEquals('{!parent which="id:entitynestedproperty_*"}name_t:*test*test*', $solr->query->getQuery()); + } } diff --git a/Tests/SolrClientFake.php b/Tests/SolrClientFake.php index 2a6da86c..ed20801a 100644 --- a/Tests/SolrClientFake.php +++ b/Tests/SolrClientFake.php @@ -6,6 +6,7 @@ use FS\SolrBundle\Query\QueryBuilderInterface; use FS\SolrBundle\Query\SolrQuery; use FS\SolrBundle\Repository\Repository; +use FS\SolrBundle\Repository\RepositoryInterface; use FS\SolrBundle\SolrInterface; class SolrClientFake implements SolrInterface @@ -36,8 +37,9 @@ public function getMetaFactory() return $this->metaFactory; } - public function addDocument($doc) + public function addDocument($doc): bool { + return true; } public function deleteByQuery($query) @@ -54,7 +56,7 @@ public function isCommited() return $this->commit; } - public function query(AbstractQuery $query) + public function query(AbstractQuery $query): array { $this->query = $query; @@ -92,12 +94,12 @@ public function removeDocument($entity) // TODO: Implement removeDocument() method. } - public function updateDocument($entity) + public function updateDocument($entity): bool { // TODO: Implement updateDocument() method. } - public function getRepository($entity) + public function getRepository($entity): RepositoryInterface { // TODO: Implement getRepository() method. } @@ -107,7 +109,7 @@ public function computeChangeSet(array $doctrineChangeSet, $entity) // TODO: Implement computeChangeSet() method. } - public function createQueryBuilder($entity) + public function createQueryBuilder($entity): QueryBuilderInterface { // TODO: Implement createQueryBuilder() method. } diff --git a/Tests/SolrTest.php b/Tests/SolrTest.php index c66752a9..cd489043 100644 --- a/Tests/SolrTest.php +++ b/Tests/SolrTest.php @@ -3,7 +3,6 @@ namespace FS\SolrBundle\Tests; use FS\SolrBundle\Query\QueryBuilderInterface; -use FS\SolrBundle\SolrException; use FS\SolrBundle\Tests\Fixtures\EntityWithInvalidRepository; use FS\SolrBundle\Tests\Fixtures\InvalidTestEntityFiltered; use FS\SolrBundle\Tests\Fixtures\ValidTestEntityFiltered; @@ -11,15 +10,9 @@ use FS\SolrBundle\Tests\Fixtures\EntityCore1; use FS\SolrBundle\Tests\Doctrine\Mapper\SolrDocumentStub; use FS\SolrBundle\Query\FindByDocumentNameQuery; -use FS\SolrBundle\Event\EventManager; -use FS\SolrBundle\Tests\SolrClientFake; use FS\SolrBundle\Tests\Fixtures\ValidTestEntity; use FS\SolrBundle\Tests\Fixtures\EntityWithRepository; -use FS\SolrBundle\Doctrine\Mapper\MetaInformation; -use FS\SolrBundle\Tests\Util\MetaTestInformationFactory; -use FS\SolrBundle\Solr; use FS\SolrBundle\Tests\Fixtures\ValidEntityRepository; -use FS\SolrBundle\Tests\Util\CommandFactoryStub; use FS\SolrBundle\Query\SolrQuery; use Solarium\Plugin\BufferedAdd\BufferedAdd; use Solarium\QueryType\Update\Query\Document\Document; @@ -234,6 +227,11 @@ public function indexDocumentsGroupedByCore() ->with('bufferedadd') ->will($this->returnValue($bufferPlugin)); + + $this->mapper->expects($this->once()) + ->method('toDocument') + ->will($this->returnValue(new DocumentStub())); + $this->solr->synchronizeIndex(array($entity)); } @@ -266,6 +264,11 @@ public function setCoreToNullIfNoIndexExists() ->with('bufferedadd') ->will($this->returnValue($bufferPlugin)); + + $this->mapper->expects($this->exactly(2)) + ->method('toDocument') + ->will($this->returnValue(new DocumentStub())); + $this->solr->synchronizeIndex(array($entity1, $entity2)); } diff --git a/composer.json b/composer.json index 47dec111..fed8435e 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,9 @@ "symfony/config": "^2.3|^3.0|^4.0", "symfony/doctrine-bridge": "^2.3|^3.0|^4.0", "minimalcode/search": "^1.0", - "ramsey/uuid": "^3.5" + "ramsey/uuid": "^3.5", + "myclabs/deep-copy": "^1.6", + "doctrine/annotations": "^1.4" }, "require-dev": { "behat/behat": "^3.1",
{{ query.raw_data }}