Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 58 additions & 7 deletions src/Association/Embedded.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
namespace Cake\ElasticSearch\Association;

use Cake\Core\App;
use Cake\Datasource\FactoryLocator;
use Cake\ElasticSearch\Document;
use Cake\ElasticSearch\Exception\MissingDocumentException;
use Cake\ElasticSearch\Index;
use Cake\Utility\Inflector;
use InvalidArgumentException;
use function Cake\Core\deprecationWarning;

/**
Expand Down Expand Up @@ -39,7 +41,7 @@
protected string $alias;

/**
* The class to use for the embeded document.
* The class to use for the embedded document.
*
* @var string
*/
Expand All @@ -59,6 +61,13 @@
*/
protected string $indexClass;

/**
* Index instance this embed is linked to
*
* @var \Cake\ElasticSearch\Index|null
*/
protected ?Index $index;

/**
* Constructor
*
Expand Down Expand Up @@ -224,15 +233,28 @@
/**
* Set the index class used for this embed.
*
* @param \Cake\ElasticSearch\Index|string|null $name The class name to set.
* @param \Cake\ElasticSearch\Index|string|null $className The class name to set.
* @return $this
* @throws \InvalidArgumentException In case the class name is set after the target index has been
* resolved, and it doesn't match the target index's class name.
*/
public function setIndexClass(string|Index|null $name)
public function setIndexClass(string|Index|null $className)
{
if ($name instanceof Index) {
$this->indexClass = get_class($name);
} elseif (is_string($name)) {
$class = App::className($name, 'Model/Index');
if ($className instanceof Index) {
$this->index = $className;
$this->indexClass = get_class($className);
} elseif (is_string($className)) {
$class = App::className($className, 'Model/Index');
if (
isset($this->indexClass) &&

Check failure on line 249 in src/Association/Embedded.php

View workflow job for this annotation

GitHub Actions / Coding Standard & Static Analysis

RedundantCondition

src/Association/Embedded.php:249:17: RedundantCondition: Type string for $this->indexClass is always isset (see https://psalm.dev/122)
get_class($this->indexClass) !== $class

Check failure on line 250 in src/Association/Embedded.php

View workflow job for this annotation

GitHub Actions / Coding Standard & Static Analysis

Strict comparison using !== between false and class-string|null will always evaluate to true.

Check failure on line 250 in src/Association/Embedded.php

View workflow job for this annotation

GitHub Actions / Coding Standard & Static Analysis

Parameter #1 $object of function get_class expects object, string given.

Check failure on line 250 in src/Association/Embedded.php

View workflow job for this annotation

GitHub Actions / Coding Standard & Static Analysis

InvalidArgument

src/Association/Embedded.php:250:27: InvalidArgument: Argument 1 of get_class expects object, but string provided (see https://psalm.dev/004)
) {
throw new InvalidArgumentException(sprintf(
"The class name `%s` doesn't match the target index class name of `%s`.",
$className,
$this->index::class,
));
}
$this->indexClass = $class;
}

Expand Down Expand Up @@ -261,6 +283,35 @@
return $this->getIndexClass();
}

/**
* Sets the index instance for the target side of the association.
*
* @param \Cake\ElasticSearch\Index $index the instance to be assigned as target side
* @return $this
*/
public function setIndex(Index $index)
{
$this->index = $index;

return $this;
}

/**
* Gets the index instance for the target side of the association.
*
* @return \Cake\ElasticSearch\Index
*/
public function getIndex(): Index
{
if (!isset($this->index)) {
/** @var \Cake\ElasticSearch\Index $index */
$index = FactoryLocator::get('Elastic')->get($this->getIndexClass());
$this->index = $index;
}

return $this->index;
}

/**
* Get the alias for this embed.
*
Expand Down
133 changes: 52 additions & 81 deletions src/Marshaller.php
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<?php
declare(strict_types=1);

Expand All @@ -19,7 +19,6 @@
use ArrayObject;
use Cake\Collection\Collection;
use Cake\Datasource\EntityInterface;
use Cake\Datasource\FactoryLocator;
use Cake\ElasticSearch\Association\Embedded;
use RuntimeException;

Expand Down Expand Up @@ -52,45 +51,22 @@
*
* ### Options:
*
* * fieldList: A whitelist of fields to be assigned to the entity. If not present,
* - fieldList: A whitelist of fields to be assigned to the entity. If not present,
* the accessible fields list in the entity will be used.
* * accessibleFields: A list of fields to allow or deny in entity accessible fields.
* * associated: A list of embedded documents you want to marshal.
* - accessibleFields: A list of fields to allow or deny in entity accessible fields.
* - associated: A list of embedded documents you want to marshal.
*
* @param array $data The data to hydrate.
* @param array $options List of options
* @param array<string, mixed> $data The data to hydrate.
* @param array<string, mixed> $options List of options
* @return \Cake\ElasticSearch\Document
*/
public function one(array $data, array $options = []): Document
{
$entityClass = $this->index->getEntityClass();
$entity = $this->createAndHydrate($entityClass, $data, $options);
$entity->setSource($this->index->getRegistryAlias());

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 $indexClass Index class to get embeds from (for nesting)
* @return \Cake\ElasticSearch\Document
*/
protected function createAndHydrate(
string $class,
array $data,
array $options = [],
?string $indexClass = null,
): Document {
$entity = new $class();

$options += ['associated' => []];

[$data, $options] = $this->_prepareDataAndOptions($data, $options);

$entity = $this->index->newEmptyEntity();
if (isset($options['accessibleFields'])) {
foreach ((array)$options['accessibleFields'] as $key => $value) {
$entity->setAccess($key, $value);
Expand All @@ -102,13 +78,7 @@
unset($data[$badKey]);
}

if ($indexClass === null) {
$embeds = $this->index->embedded();
} else {
/** @var \Cake\ElasticSearch\Index $index */
$index = FactoryLocator::get('Elastic')->get($indexClass);
$embeds = $index->embedded();
}
$embeds = $this->index->embedded();

foreach ($embeds as $embed) {
$property = $embed->getProperty();
Expand All @@ -117,7 +87,7 @@
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'])) {
} elseif (in_array($alias, $options['associated'], true)) {
$entity->set($property, $this->newNested($embed, $data[$property]));
unset($data[$property]);
}
Expand All @@ -134,7 +104,7 @@
}
}

return $entity;

Check failure on line 107 in src/Marshaller.php

View workflow job for this annotation

GitHub Actions / Coding Standard & Static Analysis

Method Cake\ElasticSearch\Marshaller::one() should return Cake\ElasticSearch\Document but returns Cake\Datasource\EntityInterface.
}

/**
Expand All @@ -147,39 +117,32 @@
*/
protected function newNested(Embedded $embed, array $data, array $options = []): Document|array
{
$class = $embed->getEntityClass();
$marshaller = $embed->getIndex()->marshaller();
if ($embed->type() === Embedded::ONE_TO_ONE) {
return $this->createAndHydrate($class, $data, $options, $embed->getIndexClass());
} else {
$children = [];
foreach ($data as $row) {
if (is_array($row)) {
$children[] = $this->createAndHydrate($class, $row, $options, $embed->getIndexClass());
}
}

return $children;
return $marshaller->one($data, $options);
}

return $marshaller->many($data, $options);
}

/**
* Merge an embedded document.
*
* @param \Cake\ElasticSearch\Association\Embedded $embed The embed definition.
* @param \Cake\ElasticSearch\Document|array $existing The existing entity or entities.
* @param array $data The data to marshal
* @param \Cake\ElasticSearch\Document|array|null $existing The existing entity or entities.
* @param array<string, mixed> $data The data to marshal
* @return \Cake\ElasticSearch\Document|array Either a document or an array of documents.
*/
protected function mergeNested(Embedded $embed, Document|array|null $existing, array $data): Document|array
{
$class = $embed->getEntityClass();
$index = $embed->getIndex();
if ($embed->type() === Embedded::ONE_TO_ONE) {
if (!($existing instanceof EntityInterface)) {
$existing = new $class();
$existing = $index->newEmptyEntity();
}
$existing->set($data);

return $existing;

Check failure on line 145 in src/Marshaller.php

View workflow job for this annotation

GitHub Actions / Coding Standard & Static Analysis

Method Cake\ElasticSearch\Marshaller::mergeNested() should return array|Cake\ElasticSearch\Document but returns Cake\Datasource\EntityInterface.
} else {
if (!is_array($existing)) {
$existing = [];
Expand All @@ -192,7 +155,7 @@
}
foreach ($data as $row) {
if (is_array($row)) {
$new = new $class();
$new = $index->newEmptyEntity();
$new->set($row);
$existing[] = $new;
}
Expand All @@ -207,18 +170,21 @@
*
* ### Options:
*
* * fieldList: A whitelist of fields to be assigned to the entity. If not present,
* - fieldList: A whitelist of fields to be assigned to the entity. If not present,
* the accessible fields list in the entity will be used.
* * accessibleFields: A list of fields to allow or deny in entity accessible fields.
* - accessibleFields: A list of fields to allow or deny in entity accessible fields.
*
* @param array $data A list of entity data you want converted into objects.
* @param array $options Options
* @return array An array of hydrated entities
* @param array $data The data to hydrate.
* @param array<string, mixed> $options List of options
* @return array<\Cake\Datasource\EntityInterface> An array of hydrated records.
*/
public function many(array $data, array $options = []): array
{
$output = [];
foreach ($data as $record) {
if (!is_array($record)) {
continue;
}
$output[] = $this->one($record, $options);
}

Expand All @@ -230,21 +196,23 @@
*
* ### Options:
*
* * fieldList: A whitelist of fields to be assigned to the entity. If not present
* - fieldList: A whitelist of fields to be assigned to the entity. If not present
* the accessible fields list in the entity will be used.
* * associated: A list of embedded documents you want to marshal.
* - associated: A list of embedded documents you want to marshal.
*
* @param \Cake\Datasource\EntityInterface $entity the entity that will get the
* data merged in
* @param array $data key value list of fields to be merged into the entity
* @param array $options List of options.
* @param array<string, mixed> $options List of options.
* @return \Cake\Datasource\EntityInterface
*/
public function merge(EntityInterface $entity, array $data, array $options = []): EntityInterface
{
$options += ['associated' => []];
[$data, $options] = $this->_prepareDataAndOptions($data, $options);
$errors = $this->_validate($data, $options, $entity->isNew());

$isNew = $entity->isNew();
$errors = $this->_validate($data, $options, $isNew);
$entity->setErrors($errors);

foreach (array_keys($errors) as $badKey) {
Expand All @@ -253,7 +221,7 @@

foreach ($this->index->embedded() as $embed) {
$property = $embed->getProperty();
if (in_array($embed->getAlias(), $options['associated']) && isset($data[$property])) {
if (in_array($embed->getAlias(), $options['associated'], true) && isset($data[$property])) {
$data[$property] = $this->mergeNested($embed, $entity->{$property}, $data[$property]);
}
}
Expand All @@ -265,6 +233,7 @@
}

foreach ((array)$options['fieldList'] as $field) {
assert(is_string($field));
if (array_key_exists($field, $data)) {
$entity->set($field, $data[$field]);
}
Expand All @@ -274,9 +243,7 @@
}

/**
* Update a collection of entities.
*
* Merges each of the elements from `$data` into each of the entities in `$entities`.
* Merges each of the elements from `$data` into each of the entities in `$entities`
*
* Records in `$data` are matched against the entities using the id field.
* Entries in `$entities` that cannot be matched to any record in
Expand All @@ -285,13 +252,14 @@
*
* ### Options:
*
* * fieldList: A whitelist of fields to be assigned to the entity. If not present,
* - fieldList: An allowed list of fields to be assigned to the entity. If not present,
* the accessible fields list in the entity will be used.
*
* @param iterable $entities An array of Elasticsearch entities
* @param array $data A list of entity data you want converted into objects.
* @param array $options Options
* @return array An array of merged entities
* @param iterable<\Cake\Datasource\EntityInterface> $entities the entities that will get the
* data merged in
* @param array $data list of arrays to be merged into the entities
* @param array<string, mixed> $options List of options.
* @return array<\Cake\Datasource\EntityInterface>
*/
public function mergeMany(iterable $entities, array $data, array $options = []): array
{
Expand All @@ -306,22 +274,25 @@

$new = $indexed[''] ?? [];
unset($indexed['']);

$output = [];
foreach ($entities as $record) {
if (!($record instanceof EntityInterface)) {

foreach ($entities as $entity) {
if (!($entity instanceof EntityInterface)) {

Check failure on line 280 in src/Marshaller.php

View workflow job for this annotation

GitHub Actions / Coding Standard & Static Analysis

Instanceof between Cake\Datasource\EntityInterface and Cake\Datasource\EntityInterface will always evaluate to true.
continue;
}
$id = $record->id;

$id = $entity->id;

Check failure on line 284 in src/Marshaller.php

View workflow job for this annotation

GitHub Actions / Coding Standard & Static Analysis

NoInterfaceProperties

src/Marshaller.php:284:19: NoInterfaceProperties: Interfaces cannot have properties (see https://psalm.dev/028)
if (!isset($indexed[$id])) {
continue;
}
$output[] = $this->merge($record, $indexed[$id], $options);

$output[] = $this->merge($entity, $indexed[$id], $options);
unset($indexed[$id]);
}

$new = array_merge($indexed, $new);
foreach ($new as $newRecord) {
$output[] = $this->one($newRecord, $options);
foreach ($new as $value) {
$output[] = $this->one($value, $options);
}

return $output;
Expand Down Expand Up @@ -360,8 +331,8 @@
/**
* Returns data and options prepared to validate and marshall.
*
* @param array $data The data to prepare.
* @param array $options The options passed to this marshaller.
* @param array<string, mixed> $data The data to prepare.
* @param array<string, mixed> $options The options passed to this marshaller.
* @return array An array containing prepared data and options.
*/
protected function _prepareDataAndOptions(array $data, array $options): array
Expand Down
Loading
Loading