Skip to content
Merged
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
44 changes: 3 additions & 41 deletions src/ORM/AutoHydratorRecursive.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,6 @@ class AutoHydratorRecursive
'belongsToMany',
];

/**
* @var string[]
*/
protected array $aliases;

/** @var array<string,string[]> SQL alias => fields */
protected array $aliasMap = [];

/**
* Precomputed mapping strategy.
*
Expand All @@ -54,40 +46,12 @@ class AutoHydratorRecursive

/**
* @param \Cake\ORM\Table $rootTable
* @param mixed[] $rows The result set of `$this->stmt->fetchAll(\PDO::FETCH_ASSOC);`.
* @param mixed[] $mappingStrategy Mapping strategy.
*/
public function __construct(Table $rootTable, array $rows)
public function __construct(Table $rootTable, array $mappingStrategy)
{
$this->rootTable = $rootTable;
$firstRow = $rows[0] ?? [];
if (!is_array($firstRow)) {
throw new \InvalidArgumentException('First element of the result set is not an array');
}
$keys = array_keys($firstRow);
$this->aliasMap = $this->buildAliasMap($keys);
$this->aliases = array_keys($this->aliasMap);
$strategy = new MappingStrategy($rootTable, $this->aliases);
$this->mappingStrategy = $strategy->build()->toArray();
}

/**
* @param (string|null)[] $keys
* @return string[][]
*/
protected function buildAliasMap(array $keys): array
{
$map = [];
foreach ($keys as $key) {
if (!is_string($key) || !str_contains($key, '__')) {
throw new UnknownAliasException("Alias '$key' is invalid");
}
[$alias, $field] = explode('__', $key, 2);
if (mb_strlen($alias) <= 0 || mb_strlen($field) <= 0) {
throw new UnknownAliasException("Alias '$key' is invalid");
}
$map[$alias][] = $field;
}
return $map;
$this->mappingStrategy = $mappingStrategy;
}

/**
Expand Down Expand Up @@ -153,7 +117,6 @@ protected function map(
if (in_array($parentAssociation, ['hasOne', 'belongsTo'])) {
if (!$parent->has($node['propertyName'])) {
// create new entity
//TODO: do not create entity if all fields are null
$entity = $this->constructEntity($className, $row[$alias]);
$parent->set($node['propertyName'], $entity);
$parent->clean();
Expand All @@ -171,7 +134,6 @@ protected function map(
$hash = $this->computeFieldsHash($row[$alias], $parentHash);
if (!isset($this->entitiesMap[$alias][$hash])) {
// create new entity
//TODO: do not create entity if all fields are null
$entity = $this->constructEntity($className, $row[$alias]);
if ($entity !== null) {
$siblings[] = $entity;
Expand Down
18 changes: 12 additions & 6 deletions src/ORM/MappingStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ public function __construct(Table $rootTable, array $aliases)
$this->unknownAliases = array_combine($aliases, $aliases);
$rootAlias = $rootTable->getAlias();
if (!isset($this->unknownAliases[$rootAlias])) {
throw new UnknownAliasException("The query must use root table alias '$rootAlias'");
$message = "The query must select at least one column from the root table.";
$message .= " The column alias must use {$rootAlias}__{column_name} format";
throw new UnknownAliasException($message);
}
unset($this->unknownAliases[$rootAlias]);
}
Expand All @@ -79,7 +81,8 @@ public function build(): self
}
}
if ($this->unknownAliases !== []) {
throw new UnknownAliasException('Failed to map some aliases: ' . $this->unknownAliasesToString());
$message = sprintf("None of the table associations match alias '%s'", $this->unknownAliasesToString());
throw new UnknownAliasException($message);
}
return $this;
}
Expand Down Expand Up @@ -129,10 +132,11 @@ private function scanRootLevel(Table $table): array
$result[$type][$alias] = $firstLevelAssoc;
}
if ($unknownAliasesCount > 0 && $unknownAliasesCount === count($this->unknownAliases)) {
throw new UnknownAliasException(
'None of the root table associations match any remaining aliases: ' .
$this->unknownAliasesToString()
$message = sprintf(
"None of the root table associations match alias '%s'",
$this->unknownAliasesToString(),
);
throw new UnknownAliasException($message);
}
return $result;
}
Expand Down Expand Up @@ -164,6 +168,8 @@ private function scanTableRecursive(string $alias): array
continue;
}
unset($this->unknownAliases[$childAlias]);
$result[$type][$childAlias]['className'] = $target->getEntityClass();
$result[$type][$childAlias]['propertyName'] = $assoc->getProperty();
if ($assoc instanceof BelongsToMany) {
$through = $assoc->getThrough() ?? $assoc->junction();
if (is_object($through)) {
Expand Down Expand Up @@ -219,6 +225,6 @@ public function toArray(): array

private function unknownAliasesToString(): string
{
return implode(', ', array_keys($this->unknownAliases));
return implode("', '", array_keys($this->unknownAliases));
}
}
1 change: 0 additions & 1 deletion src/ORM/NativeSQLMapperTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace Bancer\NativeQueryMapper\ORM;

use Cake\Database\StatementInterface;
use Cake\ORM\Table;

trait NativeSQLMapperTrait
{
Expand Down
43 changes: 37 additions & 6 deletions src/ORM/StatementQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class StatementQuery
protected bool $isExecuted;

/**
* @var callable|null
* @var mixed[]|null
*/
protected $mapStrategy = null;

Expand All @@ -28,10 +28,10 @@ public function __construct(Table $rootTable, StatementInterface $stmt)
/**
* Provide a custom mapping strategy.
*
* @param callable $strategy
* @param mixed[] $strategy
* @return $this
*/
public function mapStrategy(callable $strategy): self
public function mapStrategy(array $strategy): self
{
$this->mapStrategy = $strategy;
return $this;
Expand All @@ -52,10 +52,41 @@ public function all(): array
if (!$rows) {
return [];
}
if ($this->mapStrategy !== null) {
return array_map($this->mapStrategy, $rows);
if ($this->mapStrategy === null) {
$aliases = $this->extractAliases($rows);
$strategy = new MappingStrategy($this->rootTable, $aliases);
$this->mapStrategy = $strategy->build()->toArray();
}
$hydrator = new AutoHydratorRecursive($this->rootTable, $rows);
$hydrator = new AutoHydratorRecursive($this->rootTable, $this->mapStrategy);
return $hydrator->hydrateMany($rows);
}

/**
* Extracts aliases of the columns from the query's result set.
*
* @param mixed[] $rows Result set rows.
* @return string[]
*/
protected function extractAliases(array $rows): array
{
$firstRow = $rows[0] ?? [];
if (!is_array($firstRow)) {
throw new \InvalidArgumentException('First element of the result set is not an array');
}
$keys = array_keys($firstRow);
$aliases = [];
foreach ($keys as $key) {
if (!is_string($key) || !str_contains($key, '__')) {
throw new UnknownAliasException("Column '$key' must use an alias in the format {Alias}__$key");
}
[$alias, $field] = explode('__', $key, 2);
if (mb_strlen($alias) <= 0 || mb_strlen($field) <= 0) {
$message = "Alias '$key' is invalid. Column alias must use {Alias}__{column_name} format";
throw new UnknownAliasException($message);
}
$aliases[] = $alias;
}
sort($aliases);
return $aliases;
}
}
1 change: 1 addition & 0 deletions tests/TestApp/Model/Table/UsersTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ public function initialize(array $config): void
parent::initialize($config);
$this->belongsTo('Countries', ['className' => CountriesTable::class]);
$this->hasOne('Profiles', ['className' => ProfilesTable::class]);
$this->hasMany('Articles', ['className' => ArticlesTable::class]);
}
}
Loading