Skip to content

Commit 69aceb6

Browse files
authored
Merge pull request #4 from bancer/develop
Add more tests and fix deep associations mapping
2 parents 881eb7a + 29989c6 commit 69aceb6

6 files changed

Lines changed: 915 additions & 92 deletions

File tree

src/ORM/AutoHydratorRecursive.php

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,6 @@ class AutoHydratorRecursive
2121
'belongsToMany',
2222
];
2323

24-
/**
25-
* @var string[]
26-
*/
27-
protected array $aliases;
28-
29-
/** @var array<string,string[]> SQL alias => fields */
30-
protected array $aliasMap = [];
31-
3224
/**
3325
* Precomputed mapping strategy.
3426
*
@@ -54,40 +46,12 @@ class AutoHydratorRecursive
5446

5547
/**
5648
* @param \Cake\ORM\Table $rootTable
57-
* @param mixed[] $rows The result set of `$this->stmt->fetchAll(\PDO::FETCH_ASSOC);`.
49+
* @param mixed[] $mappingStrategy Mapping strategy.
5850
*/
59-
public function __construct(Table $rootTable, array $rows)
51+
public function __construct(Table $rootTable, array $mappingStrategy)
6052
{
6153
$this->rootTable = $rootTable;
62-
$firstRow = $rows[0] ?? [];
63-
if (!is_array($firstRow)) {
64-
throw new \InvalidArgumentException('First element of the result set is not an array');
65-
}
66-
$keys = array_keys($firstRow);
67-
$this->aliasMap = $this->buildAliasMap($keys);
68-
$this->aliases = array_keys($this->aliasMap);
69-
$strategy = new MappingStrategy($rootTable, $this->aliases);
70-
$this->mappingStrategy = $strategy->build()->toArray();
71-
}
72-
73-
/**
74-
* @param (string|null)[] $keys
75-
* @return string[][]
76-
*/
77-
protected function buildAliasMap(array $keys): array
78-
{
79-
$map = [];
80-
foreach ($keys as $key) {
81-
if (!is_string($key) || !str_contains($key, '__')) {
82-
throw new UnknownAliasException("Alias '$key' is invalid");
83-
}
84-
[$alias, $field] = explode('__', $key, 2);
85-
if (mb_strlen($alias) <= 0 || mb_strlen($field) <= 0) {
86-
throw new UnknownAliasException("Alias '$key' is invalid");
87-
}
88-
$map[$alias][] = $field;
89-
}
90-
return $map;
54+
$this->mappingStrategy = $mappingStrategy;
9155
}
9256

9357
/**
@@ -153,7 +117,6 @@ protected function map(
153117
if (in_array($parentAssociation, ['hasOne', 'belongsTo'])) {
154118
if (!$parent->has($node['propertyName'])) {
155119
// create new entity
156-
//TODO: do not create entity if all fields are null
157120
$entity = $this->constructEntity($className, $row[$alias]);
158121
$parent->set($node['propertyName'], $entity);
159122
$parent->clean();
@@ -171,7 +134,6 @@ protected function map(
171134
$hash = $this->computeFieldsHash($row[$alias], $parentHash);
172135
if (!isset($this->entitiesMap[$alias][$hash])) {
173136
// create new entity
174-
//TODO: do not create entity if all fields are null
175137
$entity = $this->constructEntity($className, $row[$alias]);
176138
if ($entity !== null) {
177139
$siblings[] = $entity;

src/ORM/MappingStrategy.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ public function __construct(Table $rootTable, array $aliases)
5757
$this->unknownAliases = array_combine($aliases, $aliases);
5858
$rootAlias = $rootTable->getAlias();
5959
if (!isset($this->unknownAliases[$rootAlias])) {
60-
throw new UnknownAliasException("The query must use root table alias '$rootAlias'");
60+
$message = "The query must select at least one column from the root table.";
61+
$message .= " The column alias must use {$rootAlias}__{column_name} format";
62+
throw new UnknownAliasException($message);
6163
}
6264
unset($this->unknownAliases[$rootAlias]);
6365
}
@@ -79,7 +81,8 @@ public function build(): self
7981
}
8082
}
8183
if ($this->unknownAliases !== []) {
82-
throw new UnknownAliasException('Failed to map some aliases: ' . $this->unknownAliasesToString());
84+
$message = sprintf("None of the table associations match alias '%s'", $this->unknownAliasesToString());
85+
throw new UnknownAliasException($message);
8386
}
8487
return $this;
8588
}
@@ -129,10 +132,11 @@ private function scanRootLevel(Table $table): array
129132
$result[$type][$alias] = $firstLevelAssoc;
130133
}
131134
if ($unknownAliasesCount > 0 && $unknownAliasesCount === count($this->unknownAliases)) {
132-
throw new UnknownAliasException(
133-
'None of the root table associations match any remaining aliases: ' .
134-
$this->unknownAliasesToString()
135+
$message = sprintf(
136+
"None of the root table associations match alias '%s'",
137+
$this->unknownAliasesToString(),
135138
);
139+
throw new UnknownAliasException($message);
136140
}
137141
return $result;
138142
}
@@ -164,6 +168,8 @@ private function scanTableRecursive(string $alias): array
164168
continue;
165169
}
166170
unset($this->unknownAliases[$childAlias]);
171+
$result[$type][$childAlias]['className'] = $target->getEntityClass();
172+
$result[$type][$childAlias]['propertyName'] = $assoc->getProperty();
167173
if ($assoc instanceof BelongsToMany) {
168174
$through = $assoc->getThrough() ?? $assoc->junction();
169175
if (is_object($through)) {
@@ -219,6 +225,6 @@ public function toArray(): array
219225

220226
private function unknownAliasesToString(): string
221227
{
222-
return implode(', ', array_keys($this->unknownAliases));
228+
return implode("', '", array_keys($this->unknownAliases));
223229
}
224230
}

src/ORM/NativeSQLMapperTrait.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace Bancer\NativeQueryMapper\ORM;
66

77
use Cake\Database\StatementInterface;
8-
use Cake\ORM\Table;
98

109
trait NativeSQLMapperTrait
1110
{

src/ORM/StatementQuery.php

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class StatementQuery
1414
protected bool $isExecuted;
1515

1616
/**
17-
* @var callable|null
17+
* @var mixed[]|null
1818
*/
1919
protected $mapStrategy = null;
2020

@@ -28,10 +28,10 @@ public function __construct(Table $rootTable, StatementInterface $stmt)
2828
/**
2929
* Provide a custom mapping strategy.
3030
*
31-
* @param callable $strategy
31+
* @param mixed[] $strategy
3232
* @return $this
3333
*/
34-
public function mapStrategy(callable $strategy): self
34+
public function mapStrategy(array $strategy): self
3535
{
3636
$this->mapStrategy = $strategy;
3737
return $this;
@@ -52,10 +52,41 @@ public function all(): array
5252
if (!$rows) {
5353
return [];
5454
}
55-
if ($this->mapStrategy !== null) {
56-
return array_map($this->mapStrategy, $rows);
55+
if ($this->mapStrategy === null) {
56+
$aliases = $this->extractAliases($rows);
57+
$strategy = new MappingStrategy($this->rootTable, $aliases);
58+
$this->mapStrategy = $strategy->build()->toArray();
5759
}
58-
$hydrator = new AutoHydratorRecursive($this->rootTable, $rows);
60+
$hydrator = new AutoHydratorRecursive($this->rootTable, $this->mapStrategy);
5961
return $hydrator->hydrateMany($rows);
6062
}
63+
64+
/**
65+
* Extracts aliases of the columns from the query's result set.
66+
*
67+
* @param mixed[] $rows Result set rows.
68+
* @return string[]
69+
*/
70+
protected function extractAliases(array $rows): array
71+
{
72+
$firstRow = $rows[0] ?? [];
73+
if (!is_array($firstRow)) {
74+
throw new \InvalidArgumentException('First element of the result set is not an array');
75+
}
76+
$keys = array_keys($firstRow);
77+
$aliases = [];
78+
foreach ($keys as $key) {
79+
if (!is_string($key) || !str_contains($key, '__')) {
80+
throw new UnknownAliasException("Column '$key' must use an alias in the format {Alias}__$key");
81+
}
82+
[$alias, $field] = explode('__', $key, 2);
83+
if (mb_strlen($alias) <= 0 || mb_strlen($field) <= 0) {
84+
$message = "Alias '$key' is invalid. Column alias must use {Alias}__{column_name} format";
85+
throw new UnknownAliasException($message);
86+
}
87+
$aliases[] = $alias;
88+
}
89+
sort($aliases);
90+
return $aliases;
91+
}
6192
}

tests/TestApp/Model/Table/UsersTable.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ public function initialize(array $config): void
2121
parent::initialize($config);
2222
$this->belongsTo('Countries', ['className' => CountriesTable::class]);
2323
$this->hasOne('Profiles', ['className' => ProfilesTable::class]);
24+
$this->hasMany('Articles', ['className' => ArticlesTable::class]);
2425
}
2526
}

0 commit comments

Comments
 (0)