diff --git a/Annotation/Document.php b/Annotation/Document.php index 437e9beb..9e2832a0 100644 --- a/Annotation/Document.php +++ b/Annotation/Document.php @@ -19,6 +19,11 @@ */ final class Document { + /** + * @var bool + */ + public $create; + /** * @var string */ @@ -33,4 +38,12 @@ final class Document * @var array */ public $ttl; + + /** + * Constructor. + */ + public function __construct() + { + $this->create = true; + } } diff --git a/Annotation/Inherit.php b/Annotation/Inherit.php new file mode 100644 index 00000000..6932c738 --- /dev/null +++ b/Annotation/Inherit.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ElasticsearchBundle\Annotation; + +/** + * Annotation used inherit properties during the parsing process. + * + * @Annotation + * @Target("CLASS") + */ +final class Inherit +{ + /** + * @var array + */ + public $value; + + /** + * Constructor. + * + * @param array $values + * + * @throws \InvalidArgumentException + */ + public function __constructor(array $values) + { + if (is_string($values['value'])) { + $this->value = [$values['value']]; + } elseif (is_array($values['value'])) { + $this->value = $values['value']; + } else { + throw new \InvalidArgumentException( + 'Annotation Inherit unexpected type given. Expected string or array, given ' . gettype($values['value']) + ); + } + } +} diff --git a/Annotation/Skip.php b/Annotation/Skip.php new file mode 100644 index 00000000..cff0f8ff --- /dev/null +++ b/Annotation/Skip.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ElasticsearchBundle\Annotation; + +/** + * Annotation used to skip properties during the parsing process. + * + * @Annotation + * @Target("CLASS") + */ +final class Skip +{ + /** + * @var array + */ + public $value; + + /** + * Constructor. + * + * @param array $values + * + * @throws \InvalidArgumentException + */ + public function __constructor(array $values) + { + if (is_string($values['value'])) { + $this->value = [$values['value']]; + } elseif (is_array($values['value'])) { + $this->value = $values['value']; + } else { + throw new \InvalidArgumentException( + 'Annotation Inherit unexpected type given. Expected string or array, given ' . gettype($values['value']) + ); + } + } +} diff --git a/Mapping/MetadataCollector.php b/Mapping/MetadataCollector.php index 99560158..ed9c5137 100644 --- a/Mapping/MetadataCollector.php +++ b/Mapping/MetadataCollector.php @@ -215,20 +215,25 @@ private function getDocumentReflectionMapping(\ReflectionClass $reflectionClass) { /** @var Document $class */ $class = $this->reader->getClassAnnotation($reflectionClass, 'ONGR\ElasticsearchBundle\Annotation\Document'); - if ($class !== null) { + if ($class !== null && $class->create) { $type = $this->getDocumentType($reflectionClass, $class); $parent = $class->parent === null ? $class->parent : $this->getDocumentParentType($class->parent); - $properties = $this->getProperties($reflectionClass); + $inherit = $this->getInheritedProperties($reflectionClass); - $setters = []; - $getters = []; + $properties = $this->getProperties( + $reflectionClass, + array_merge($inherit, $this->getSkippedProperties($reflectionClass)) + ); - foreach ($properties as $property => $params) { - $alias = $this->aliases[$reflectionClass->getName()][$property]; - list($setters[$property], $getters[$property]) = $this - ->getInfoAboutProperty($params, $alias, $reflectionClass); + if (!empty($inherit)) { + $properties = array_merge( + $properties, + $this->getProperties($reflectionClass->getParentClass(), $inherit, true) + ); } + list($setters, $getters) = $this->getSettersAndGetters($reflectionClass, $properties); + $class = [ $type => [ 'properties' => $properties, @@ -248,6 +253,70 @@ private function getDocumentReflectionMapping(\ReflectionClass $reflectionClass) return $class; } + /** + * Returns information about accessing properties from document. + * + * @param \ReflectionClass $reflectionClass Document reflection class. + * @param array $properties Document properties. + * + * @return array + */ + private function getSettersAndGetters(\ReflectionClass $reflectionClass, array $properties) + { + $setters = []; + $getters = []; + + foreach ($properties as $property => $params) { + if (array_key_exists($property, $this->aliases[$reflectionClass->getName()])) { + list($setters[$property], $getters[$property]) = $this + ->getInfoAboutProperty( + $params, + $this->aliases[$reflectionClass->getName()][$property], + $reflectionClass + ); + } elseif ($reflectionClass->getParentClass() !== false) { + list($parentSetters, $parentGetters) = $this + ->getSettersAndGetters($reflectionClass->getParentClass(), [$property => $params]); + + if ($parentSetters !== []) { + $setters = array_merge($setters, $parentSetters); + } + + if ($parentGetters !== []) { + $getters = array_merge($getters, $parentGetters); + } + } + } + + return [$setters, $getters]; + } + + /** + * @param \ReflectionClass $reflectionClass + * + * @return array + */ + private function getSkippedProperties(\ReflectionClass $reflectionClass) + { + /** @var Skip $class */ + $class = $this->reader->getClassAnnotation($reflectionClass, 'ONGR\ElasticsearchBundle\Annotation\Skip'); + + return $class === null ? [] : $class->value; + } + + /** + * @param \ReflectionClass $reflectionClass + * + * @return array + */ + private function getInheritedProperties(\ReflectionClass $reflectionClass) + { + /** @var Inherit $class */ + $class = $this->reader->getClassAnnotation($reflectionClass, 'ONGR\ElasticsearchBundle\Annotation\Inherit'); + + return $class === null ? [] : $class->value; + } + /** * Returns document type. * @@ -304,6 +373,7 @@ private function getInfoAboutProperty($params, $alias, $reflectionClass) * @param \ReflectionClass $reflectionClass * * @return array + * * @throws \LogicException */ private function checkPropertyAccess($property, $methodPrefix, $reflectionClass) @@ -405,19 +475,23 @@ private function getDocumentParentType($namespace) /** * Returns properties of reflection class. * - * @param \ReflectionClass $reflectionClass + * @param \ReflectionClass $reflectionClass Class to read properties from. + * @param array $properties Properties to skip. + * @param array $flag If false exludes properties, true only includes properties. * * @return array - * @throws \RuntimeException */ - private function getProperties(\ReflectionClass $reflectionClass) + private function getProperties(\ReflectionClass $reflectionClass, $properties = [], $flag = false) { $mapping = []; /** @var \ReflectionProperty $property */ foreach ($reflectionClass->getProperties() as $property) { $type = $this->getPropertyAnnotationData($property); - if (empty($type)) { + if ((in_array($property->getName(), $properties) && !$flag) + || (!in_array($property->getName(), $properties) && $flag) + || empty($type) + ) { continue; } @@ -483,6 +557,8 @@ private function registerAnnotations() 'Object', 'Nested', 'MultiField', + 'Inherit', + 'Skip', 'Suggester/CompletionSuggesterProperty', 'Suggester/ContextSuggesterProperty', 'Suggester/Context/CategoryContext', @@ -519,6 +595,7 @@ private function getNamespace($namespace) * @param string $name * * @return string + * * @throws \LogicException */ private function getBundle($name) diff --git a/Resources/doc/index.md b/Resources/doc/index.md index c5659c71..404ad3ec 100644 --- a/Resources/doc/index.md +++ b/Resources/doc/index.md @@ -17,4 +17,5 @@ Main things you should know while working with this bundle. More specific features and usages that you might need. -- (Coming soon) \ No newline at end of file +- [Document inheritance](inheritance.md) +- [Autocomplete, suggesters](suggesters/usage.md) \ No newline at end of file diff --git a/Resources/doc/inheritance.md b/Resources/doc/inheritance.md new file mode 100644 index 00000000..de154b3f --- /dev/null +++ b/Resources/doc/inheritance.md @@ -0,0 +1,77 @@ +### Document Inheritance + +By default annotations inherit all properties that have been defined in parent document. We have implemented Skip and Inherit annotations to make inheritance to be less of a headache. F.e. + +```php + +/** + * Document class Item. + * + * @ES\Document(create=false) + */ +class Item implements DocumentInterface +{ + use DocumentTrait; + + /** + * @var string + * + * @ES\Property(name="name", type="string") + */ + public $name; + + /** + * @var float + * + * @ES\Property(type="float", name="price") + */ + public $price; + + /** + * @var \DateTime + * + * @ES\Property(name="created_at", type="date") + */ + public $createdAt; +} + +/** + * Product document for testing. + * + * @ES\Document(type="product") + * @ES\Skip({"name"}) + * @ES\Inherit({"price"}) + */ +class Product extends Item implements DocumentInterface +{ + use DocumentTrait; + + /** + * @var string + * + * @ES\Property(type="string", name="title", fields={@ES\MultiField(name="raw", type="string")}) + */ + public $title; + + /** + * @var string + * + * @ES\Property(type="string", name="description") + */ + public $description; + + /** + * @var int + * + * @ES\Property(type="integer", name="price") + */ + public $price; +} + +``` + +In this example Product document inherits all properties from Item. Let's pretend that we dont want property `$name` in Product document. So we just add `@ES\Skip("name")` or if we want multiple skip's `@ES\Skip({"price", "description"})`. + +> Annotations above class are not inherited. + +Next imagine that we want to extend this product document later but it should have price as integer (not float like Item), but this Type should have it as float and we dont want to rewrite price as integer everywhere. Thats where `@ES\Inherit` kicks in. **It inherits properties from parent documents if it has been defined in current document**. So product document in this example will have price as float. diff --git a/Resources/doc/usage.md b/Resources/doc/usage.md index 01996da1..33010e6c 100644 --- a/Resources/doc/usage.md +++ b/Resources/doc/usage.md @@ -78,6 +78,3 @@ $documents = $repository->findBy(['title' => 'Acme']); ```` To perform a more complex queries there is a Search DSL API. Read more about it in [Search DSL](search.md) chapter. - -### Autocomplete -To use autocomplete follow [suggesters](suggesters/usage.md) section. diff --git a/Tests/Unit/Mapping/MetadataCollectorTest.php b/Tests/Unit/Mapping/MetadataCollectorTest.php index c60bcb45..a9789621 100644 --- a/Tests/Unit/Mapping/MetadataCollectorTest.php +++ b/Tests/Unit/Mapping/MetadataCollectorTest.php @@ -73,6 +73,9 @@ protected function getProductMapping() ], ], ], + 'created_at' => [ + 'type' => 'date', + ] ]; } diff --git a/Tests/app/fixture/Acme/TestBundle/Document/Item.php b/Tests/app/fixture/Acme/TestBundle/Document/Item.php new file mode 100644 index 00000000..8c44a2a5 --- /dev/null +++ b/Tests/app/fixture/Acme/TestBundle/Document/Item.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ElasticsearchBundle\Tests\app\fixture\Acme\TestBundle\Document; + +use ONGR\ElasticsearchBundle\Annotation as ES; +use ONGR\ElasticsearchBundle\Document\DocumentInterface; +use ONGR\ElasticsearchBundle\Document\DocumentTrait; + +/** + * Document class Item. + * + * @ES\Document(create=false) + */ +class Item implements DocumentInterface +{ + use DocumentTrait; + + /** + * @var string + * + * @ES\Property(name="name", type="string") + */ + public $name; + + /** + * @var float + * + * @ES\Property(type="float", name="price") + */ + public $price; + + /** + * @var \DateTime + * + * @ES\Property(name="created_at", type="date") + */ + public $createdAt; +} diff --git a/Tests/app/fixture/Acme/TestBundle/Document/Product.php b/Tests/app/fixture/Acme/TestBundle/Document/Product.php index fc528a3f..e1809c35 100644 --- a/Tests/app/fixture/Acme/TestBundle/Document/Product.php +++ b/Tests/app/fixture/Acme/TestBundle/Document/Product.php @@ -19,8 +19,10 @@ * Product document for testing. * * @ES\Document(type="product") + * @ES\Skip({"name"}) + * @ES\Inherit({"price"}) */ -class Product implements DocumentInterface +class Product extends Item implements DocumentInterface { use DocumentTrait; @@ -67,9 +69,9 @@ class Product implements DocumentInterface public $completionSuggesting; /** - * @var float + * @var int * - * @ES\Property(type="float", name="price") + * @ES\Property(type="integer", name="price") */ public $price;