From c08859d9c409f75e358be19111bb96f88c51526d Mon Sep 17 00:00:00 2001 From: lilHermit Date: Tue, 21 Aug 2018 17:07:00 +0100 Subject: [PATCH 1/5] Allow for assoicated element to be keys when marshalling --- src/Marshaller.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Marshaller.php b/src/Marshaller.php index 39439255..1b614532 100644 --- a/src/Marshaller.php +++ b/src/Marshaller.php @@ -79,7 +79,8 @@ public function one(array $data, array $options = []) foreach ($this->index->embedded() as $embed) { $property = $embed->property(); - if (in_array($embed->getAlias(), $options['associated']) && + $alias = $embed->getAlias(); + if ((in_array($alias, $options['associated']) || isset($options['associated'][$alias])) && isset($data[$property]) ) { $data[$property] = $this->newNested($embed, $data[$property]); From dd11b24069c6d216e4d733b718f40aaae1a874db Mon Sep 17 00:00:00 2001 From: lilHermit Date: Thu, 23 Aug 2018 10:19:12 +0100 Subject: [PATCH 2/5] Added tests --- tests/TestCase/MarshallerTest.php | 113 ++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/tests/TestCase/MarshallerTest.php b/tests/TestCase/MarshallerTest.php index 9015a438..b6b08ec9 100644 --- a/tests/TestCase/MarshallerTest.php +++ b/tests/TestCase/MarshallerTest.php @@ -234,6 +234,58 @@ public function testOneEmbeddedOne() $this->assertSame($data['user']['username'], $result->user['username']); } + /** + * DataProvider for testOneEmbeddedOneWithOption + * + * @return array + */ + public function oneEmbeddedOneWithOptionProvider() + { + return [ + // Test both embeds with options + [['associated' => ['User' => [], 'Comment' => ['guard' => false]]]], + // Test both embeds one with options the other without + [['associated' => ['User' => [], 'Comment']]], + // Test both embeds one without options + [['associated' => ['User', 'Comment']]] + ]; + } + + /** + * test marshalling a simple object with associated options + * + * @dataProvider oneEmbeddedOneWithOptionProvider + * + * @param array $options Options to pass to marshaller->one + * + * @return void + */ + public function testOneEmbeddedOneWithOptions($options) + { + $data = [ + 'title' => 'Testing', + 'body' => 'Elastic text', + 'user' => [ + 'username' => 'mark', + ], + 'comment' => [ + 'text' => 'this is great', + 'id' => 123 + ] + ]; + $this->index->embedOne('User'); + $this->index->embedOne('Comment'); + + $marshaller = new Marshaller($this->index); + $result = $marshaller->one($data, $options); + + $this->assertInstanceOf('Cake\ElasticSearch\Document', $result); + $this->assertInstanceOf('Cake\ElasticSearch\Document', $result->user); + $this->assertInstanceOf('Cake\ElasticSearch\Document', $result->comment); + $this->assertSame($data['user']['username'], $result->user->username); + $this->assertSame($data['comment']['text'], $result->comment->text); + } + /** * test marshalling a simple object. * @@ -264,6 +316,67 @@ public function testOneEmbeddedMany() $this->assertTrue($result->comments[1]->isNew()); } + /** + * DataProvider for testOneEmbeddedManyWithOptions + * + * @return array + */ + public function oneEmbeddedManyWithOptionsProvider() + { + return [ + // Test both embeds with options + [['associated' => ['Comments' => ['guard' => false], 'Authors' => []]]], + // Test both embeds one with options the other without + [['associated' => ['Comments' => ['guard' => false], 'Authors']]], + // Test both embeds one without options + [['associated' => ['Comments', 'Authors']]] + ]; + } + + /** + * test marshalling a simple object. + * + * @dataProvider oneEmbeddedManyWithOptionsProvider + * + * @param array $options Options to pass to marshaller->one + * + * @return void + */ + public function testOneEmbeddedManyWithOptions($options) + { + $data = [ + 'title' => 'Testing', + 'body' => 'Elastic text', + 'comments' => [ + ['comment' => 'First comment'], + ['comment' => 'Second comment'], + 'bad' => 'data' + ], + 'authors' => [ + ['name' => 'Bob Smith'], + ['name' => 'Claire Muller'] + ] + ]; + $this->index->embedMany('Comments'); + $this->index->embedMany('Authors'); + + $marshaller = new Marshaller($this->index); + $result = $marshaller->one($data, $options); + + $this->assertInstanceOf('Cake\ElasticSearch\Document', $result); + $this->assertInternalType('array', $result->comments); + $this->assertInternalType('array', $result->authors); + $this->assertInstanceOf('Cake\ElasticSearch\Document', $result->comments[0]); + $this->assertInstanceOf('Cake\ElasticSearch\Document', $result->comments[1]); + $this->assertInstanceOf('Cake\ElasticSearch\Document', $result->authors[0]); + $this->assertInstanceOf('Cake\ElasticSearch\Document', $result->authors[1]); + $this->assertTrue($result->isNew()); + $this->assertTrue($result->comments[0]->isNew()); + $this->assertTrue($result->comments[1]->isNew()); + $this->assertTrue($result->authors[0]->isNew()); + $this->assertTrue($result->authors[1]->isNew()); + } + /** * Test converting multiple objects at once. * From 4be6840c3e8185139917e39f1a052b86a544b9a6 Mon Sep 17 00:00:00 2001 From: lilHermit Date: Thu, 23 Aug 2018 12:41:43 +0100 Subject: [PATCH 3/5] Moved create and hydration code to separate method so we can reuse it for embed This allows for passing `accessibleFields` to embed and they are honoured --- src/Marshaller.php | 67 ++++++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/src/Marshaller.php b/src/Marshaller.php index 1b614532..021a27dd 100644 --- a/src/Marshaller.php +++ b/src/Marshaller.php @@ -59,9 +59,40 @@ public function __construct(Index $index) */ public function one(array $data, array $options = []) { + $options += ['associated' => []]; + $entityClass = $this->index->entityClass(); - $entity = new $entityClass(); + $entity = $this->createAndHydrate($entityClass, $data, $options); $entity->setSource($this->index->getRegistryAlias()); + + foreach ($this->index->embedded() as $embed) { + $property = $embed->property(); + $alias = $embed->getAlias(); + if (isset($data[$property])) { + if (isset($options['associated'][$alias])) { + $entity->set($property, $this->newNested($embed, $data[$property], $options['associated'][$alias])); + } elseif (in_array($alias, $options['associated'])) { + $entity->set($property, $this->newNested($embed, $data[$property])); + } + } + } + + return $entity; + } + + /** + * Creates and Hydrates Document whilst honouring accessibleFields etc + * + * @param string $class Class name of Document to create + * @param array $data The data to hydrate with + * @param array $options Options to control the hydration + * + * @return Document + */ + protected function createAndHydrate($class, array $data, array $options = []) + { + $entity = new $class(); + $options += ['associated' => []]; list($data, $options) = $this->_prepareDataAndOptions($data, $options); @@ -77,25 +108,13 @@ public function one(array $data, array $options = []) unset($data[$badKey]); } - foreach ($this->index->embedded() as $embed) { - $property = $embed->property(); - $alias = $embed->getAlias(); - if ((in_array($alias, $options['associated']) || isset($options['associated'][$alias])) && - isset($data[$property]) - ) { - $data[$property] = $this->newNested($embed, $data[$property]); - } - } - if (!isset($options['fieldList'])) { $entity->set($data); - - return $entity; - } - - foreach ((array)$options['fieldList'] as $field) { - if (array_key_exists($field, $data)) { - $entity->set($field, $data[$field]); + } else { + foreach ((array)$options['fieldList'] as $field) { + if (array_key_exists($field, $data)) { + $entity->set($field, $data[$field]); + } } } @@ -105,22 +124,24 @@ public function one(array $data, array $options = []) /** * Marshal an embedded document. * - * @param \Cake\ElasticSearch\Association\Embedded $embed The embed definition. - * @param array $data The data to marshal + * @param \Cake\ElasticSearch\Association\Embedded $embed The embed definition. + * @param array $data The data to marshal + * @param array $options The options to pass on + * * @return array|\Cake\ElasticSearch\Document Either a document or an array of documents. */ - protected function newNested(Embedded $embed, array $data) + protected function newNested(Embedded $embed, array $data, array $options = []) { $class = $embed->entityClass(); if ($embed->type() === Embedded::ONE_TO_ONE) { - return new $class($data); + return $this->createAndHydrate($class, $data, $options); } if ($embed->type() === Embedded::ONE_TO_MANY) { $children = []; foreach ($data as $row) { if (is_array($row)) { - $children[] = new $class($row); + $children[] = $this->createAndHydrate($class, $row, $options); } } From 13af84cf33cab2d9b5b5a716d2ed3d9cca6b0600 Mon Sep 17 00:00:00 2001 From: lilHermit Date: Thu, 23 Aug 2018 17:12:47 +0100 Subject: [PATCH 4/5] Added indexClass to Embedded --- src/Association/Embedded.php | 43 ++++++++++++++++++++++++- tests/TestCase/EmbeddedDocumentTest.php | 18 +++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/Association/Embedded.php b/src/Association/Embedded.php index 4d818b45..5e600867 100644 --- a/src/Association/Embedded.php +++ b/src/Association/Embedded.php @@ -2,6 +2,7 @@ namespace Cake\ElasticSearch\Association; use Cake\Core\App; +use Cake\ElasticSearch\Index; use Cake\Utility\Inflector; /** @@ -46,6 +47,13 @@ abstract class Embedded */ protected $property; + /** + * The index class this embed is linked to + * + * @var string + */ + protected $indexClass; + /** * Constructor * @@ -57,7 +65,8 @@ public function __construct($alias, $options = []) $this->alias = $alias; $properties = [ 'entityClass', - 'property' + 'property', + 'indexClass' ]; $options += [ 'entityClass' => $alias @@ -119,6 +128,38 @@ public function entityClass($name = null) return $this->entityClass; } + /** + * Get/set the index class used for this embed. + * + * @param string|null|Index $name The class name to set. + * + * @return string The class name. + */ + public function indexClass($name = null) + { + if ($name === null && !$this->indexClass) { + $alias = Inflector::pluralize($this->alias); + $class = App::className($alias . 'Index', 'Model/Index'); + + if ($class) { + return $this->indexClass = $class; + } else { + return $this->indexClass = '\Cake\ElasticSearch\Index'; + } + } + + if ($name !== null) { + if ($name instanceof Index) { + $this->indexClass = get_class($name); + } elseif (is_string($name)) { + $class = App::className($name, 'Model/Index'); + $this->indexClass = $class; + } + } + + return $this->indexClass; + } + /** * Get the alias for this embed. * diff --git a/tests/TestCase/EmbeddedDocumentTest.php b/tests/TestCase/EmbeddedDocumentTest.php index 5baa9efa..1eda5db1 100644 --- a/tests/TestCase/EmbeddedDocumentTest.php +++ b/tests/TestCase/EmbeddedDocumentTest.php @@ -46,7 +46,9 @@ public function testEmbedOne() $this->assertNull($this->index->embedOne('Address')); $assocs = $this->index->embedded(); $this->assertCount(1, $assocs); + $this->assertInstanceOf('Cake\ElasticSearch\Association\EmbedOne', $assocs[0]); $this->assertEquals('\Cake\ElasticSearch\Document', $assocs[0]->entityClass()); + $this->assertEquals('\Cake\ElasticSearch\Index', $assocs[0]->indexClass()); $this->assertEquals('address', $assocs[0]->property()); } @@ -115,6 +117,22 @@ public function testFindWithEmbedOne() $this->assertCount(1, $rows); } + /** + * Test defining many embedded documents. + * + * @return void + */ + public function testEmbedMany() + { + $this->assertNull($this->index->embedMany('Address')); + $assocs = $this->index->embedded(); + $this->assertCount(1, $assocs); + $this->assertInstanceOf('Cake\ElasticSearch\Association\EmbedMany', $assocs[0]); + $this->assertEquals('\Cake\ElasticSearch\Document', $assocs[0]->entityClass()); + $this->assertEquals('\Cake\ElasticSearch\Index', $assocs[0]->indexClass()); + $this->assertEquals('address', $assocs[0]->property()); + } + /** * Test fetching with embedded has many documents. * From 642beb7f0648633229d94ceb803fcef089146f6d Mon Sep 17 00:00:00 2001 From: lilHermit Date: Fri, 24 Aug 2018 10:59:49 +0100 Subject: [PATCH 5/5] Added support for multilevel embed via 'associated' option Also supports parsing options through to embeds (for example 'accessibleFields') --- src/Marshaller.php | 48 +++-- tests/TestCase/MarshallerTest.php | 173 ++++++++++++++++++ .../TestApp/src/Model/Document/User.php | 9 + .../TestApp/src/Model/Index/AccountsIndex.php | 25 +++ .../TestApp/src/Model/Index/UsersIndex.php | 26 +++ 5 files changed, 261 insertions(+), 20 deletions(-) create mode 100644 tests/testapp/TestApp/src/Model/Document/User.php create mode 100644 tests/testapp/TestApp/src/Model/Index/AccountsIndex.php create mode 100644 tests/testapp/TestApp/src/Model/Index/UsersIndex.php diff --git a/src/Marshaller.php b/src/Marshaller.php index 021a27dd..fd92451c 100644 --- a/src/Marshaller.php +++ b/src/Marshaller.php @@ -59,37 +59,24 @@ public function __construct(Index $index) */ public function one(array $data, array $options = []) { - $options += ['associated' => []]; - $entityClass = $this->index->entityClass(); $entity = $this->createAndHydrate($entityClass, $data, $options); $entity->setSource($this->index->getRegistryAlias()); - foreach ($this->index->embedded() as $embed) { - $property = $embed->property(); - $alias = $embed->getAlias(); - if (isset($data[$property])) { - if (isset($options['associated'][$alias])) { - $entity->set($property, $this->newNested($embed, $data[$property], $options['associated'][$alias])); - } elseif (in_array($alias, $options['associated'])) { - $entity->set($property, $this->newNested($embed, $data[$property])); - } - } - } - return $entity; } /** * Creates and Hydrates Document whilst honouring accessibleFields etc * - * @param string $class Class name of Document to create - * @param array $data The data to hydrate with - * @param array $options Options to control the hydration + * @param string $class Class name of Document to create + * @param array $data The data to hydrate with + * @param array $options Options to control the hydration + * @param string $indexClass Index class to get embeds from (for nesting) * * @return Document */ - protected function createAndHydrate($class, array $data, array $options = []) + protected function createAndHydrate($class, array $data, array $options = [], $indexClass = null) { $entity = new $class(); @@ -108,6 +95,27 @@ protected function createAndHydrate($class, array $data, array $options = []) unset($data[$badKey]); } + if ($indexClass === null) { + $embeds = $this->index->embedded(); + } else { + $index = IndexRegistry::get($indexClass); + $embeds = $index->embedded(); + } + + foreach ($embeds as $embed) { + $property = $embed->property(); + $alias = $embed->getAlias(); + if (isset($data[$property])) { + if (isset($options['associated'][$alias])) { + $entity->set($property, $this->newNested($embed, $data[$property], $options['associated'][$alias])); + unset($data[$property]); + } elseif (in_array($alias, $options['associated'])) { + $entity->set($property, $this->newNested($embed, $data[$property])); + unset($data[$property]); + } + } + } + if (!isset($options['fieldList'])) { $entity->set($data); } else { @@ -134,14 +142,14 @@ protected function newNested(Embedded $embed, array $data, array $options = []) { $class = $embed->entityClass(); if ($embed->type() === Embedded::ONE_TO_ONE) { - return $this->createAndHydrate($class, $data, $options); + return $this->createAndHydrate($class, $data, $options, $embed->indexClass()); } if ($embed->type() === Embedded::ONE_TO_MANY) { $children = []; foreach ($data as $row) { if (is_array($row)) { - $children[] = $this->createAndHydrate($class, $row, $options); + $children[] = $this->createAndHydrate($class, $row, $options, $embed->indexClass()); } } diff --git a/tests/TestCase/MarshallerTest.php b/tests/TestCase/MarshallerTest.php index b6b08ec9..ea3c9aee 100644 --- a/tests/TestCase/MarshallerTest.php +++ b/tests/TestCase/MarshallerTest.php @@ -14,11 +14,13 @@ */ namespace Cake\ElasticSearch\Test; +use Cake\Core\Configure; use Cake\Datasource\ConnectionManager; use Cake\ElasticSearch\Document; use Cake\ElasticSearch\Index; use Cake\ElasticSearch\Marshaller; use Cake\TestSuite\TestCase; +use TestApp\Model\Index\AccountsIndex; /** * Test entity for mass assignment. @@ -804,4 +806,175 @@ public function testMergeManyBadEntityData() $this->assertCount(1, $result); $this->assertEquals($data[0], $result[0]->toArray()); } + + /** + * test marshalling One with multi level embed + * + * @return void + */ + public function testMarshallOneMultiLevelEmbed() + { + Configure::write('App.namespace', 'TestApp'); + + $data = [ + 'address' => '123 West Street', + 'users' => [ + [ + 'first_name' => 'Mark', + 'last_name' => 'Story', + 'user_type' => [ + 'label' => 'Admin' + ] + ], + ['first_name' => 'Clare', 'last_name' => 'Smith'], + ] + ]; + $options = [ + 'associated' => [ + 'User' => [ + 'associated' => [ + 'UserType' => [] + ] + ] + ] + ]; + + $index = new AccountsIndex(); + + $marshaller = new Marshaller($index); + $result = $marshaller->one($data, $options); + + $this->assertCount(2, $result->users); + $this->assertSame('123 West Street', $result->address); + $this->assertInstanceOf('TestApp\Model\Document\User', $result->users[0]); + $this->assertSame('Mark', $result->users[0]->first_name); + $this->assertSame('Story', $result->users[0]->last_name); + $this->assertInstanceOf('TestApp\Model\Document\User', $result->users[1]); + $this->assertSame('Clare', $result->users[1]->first_name); + $this->assertSame('Smith', $result->users[1]->last_name); + $this->assertInstanceOf('Cake\ElasticSearch\Document', $result->users[0]->user_type); + $this->assertSame('Admin', $result->users[0]->user_type->label); + } + + /** + * test marshalling One with multi level embed (with AccessibleFields) + * + * @return void + */ + public function testMarshallOneMultiLevelEmbedWithAccessibleFields() + { + Configure::write('App.namespace', 'TestApp'); + + $data = [ + 'address' => '123 West Street', + 'remove_this' => 'something', + 'users' => [ + [ + 'first_name' => 'Mark', + 'last_name' => 'Story', + 'user_type' => [ + 'label' => 'Admin', + 'level' => 21 + ] + ], + ['first_name' => 'Clare', 'last_name' => 'Smith'], + ] + ]; + $options = [ + 'accessibleFields' => ['remove_this' => false], + 'associated' => [ + 'User' => [ + 'accessibleFields' => ['last_name' => false], + 'associated' => [ + 'UserType' => [ + 'accessibleFields' => ['level' => false] + ] + ] + ] + ] + ]; + + $index = new AccountsIndex(); + + $marshaller = new Marshaller($index); + $result = $marshaller->one($data, $options); + + $this->assertCount(2, $result->users); + $this->assertNull($result->remove_this); + $this->assertInstanceOf('TestApp\Model\Document\User', $result->users[0]); + $this->assertNull($result->users[0]->last_name); + $this->assertInstanceOf('TestApp\Model\Document\User', $result->users[1]); + $this->assertNull($result->users[1]->last_name); + $this->assertInstanceOf('Cake\ElasticSearch\Document', $result->users[0]->user_type); + $this->assertNull($result->users[0]->user_type->level); + } + + /** + * test marshalling Many with multi level embed + * + * @return void + */ + public function testMarshallManyMultiLevelEmbed() + { + Configure::write('App.namespace', 'TestApp'); + + $data = [ + [ + 'address' => '123 West Street', + 'users' => [ + [ + 'first_name' => 'Mark', + 'last_name' => 'Story', + 'user_type' => [ + 'label' => 'Admin' + ] + ], + ['first_name' => 'Clare', 'last_name' => 'Smith'] + ] + ], + [ + 'address' => '87 Grant Avenue', + 'users' => [ + [ + 'first_name' => 'Colin', + 'last_name' => 'Thomas', + 'user_type' => [ + 'label' => 'Admin' + ] + ] + ] + ] + ]; + $options = [ + 'associated' => [ + 'User' => [ + 'associated' => [ + 'UserType' => [] + ] + ] + ] + ]; + + $index = new AccountsIndex(); + + $marshaller = new Marshaller($index); + $result = $marshaller->many($data, $options); + + $this->assertCount(2, $result); + $this->assertCount(2, $result[0]->users); + $this->assertCount(1, $result[1]->users); + $this->assertSame('123 West Street', $result[0]->address); + $this->assertSame('87 Grant Avenue', $result[1]->address); + $this->assertInstanceOf('TestApp\Model\Document\User', $result[0]->users[0]); + $this->assertInstanceOf('TestApp\Model\Document\User', $result[0]->users[1]); + $this->assertInstanceOf('TestApp\Model\Document\User', $result[1]->users[0]); + $this->assertSame('Mark', $result[0]->users[0]->first_name); + $this->assertSame('Story', $result[0]->users[0]->last_name); + $this->assertSame('Clare', $result[0]->users[1]->first_name); + $this->assertSame('Smith', $result[0]->users[1]->last_name); + $this->assertSame('Colin', $result[1]->users[0]->first_name); + $this->assertSame('Thomas', $result[1]->users[0]->last_name); + $this->assertInstanceOf('Cake\ElasticSearch\Document', $result[0]->users[0]->user_type); + $this->assertSame('Admin', $result[0]->users[0]->user_type->label); + } } diff --git a/tests/testapp/TestApp/src/Model/Document/User.php b/tests/testapp/TestApp/src/Model/Document/User.php new file mode 100644 index 00000000..bd6f469c --- /dev/null +++ b/tests/testapp/TestApp/src/Model/Document/User.php @@ -0,0 +1,9 @@ +embedMany('User', ['property' => 'users']); + } + + public function getName() + { + return 'accounts'; + } + + public function getType() + { + return 'accounts'; + } +} diff --git a/tests/testapp/TestApp/src/Model/Index/UsersIndex.php b/tests/testapp/TestApp/src/Model/Index/UsersIndex.php new file mode 100644 index 00000000..ae3e0c25 --- /dev/null +++ b/tests/testapp/TestApp/src/Model/Index/UsersIndex.php @@ -0,0 +1,26 @@ +embedOne('UserType'); + } + + public function getName() + { + return 'users'; + } + + public function getType() + { + return 'users'; + } +}