Skip to content

Commit c35dfc1

Browse files
committed
Move MappingStrategy construction to StatementQuery and add more tests
1 parent d767d02 commit c35dfc1

4 files changed

Lines changed: 80 additions & 49 deletions

File tree

src/ORM/AutoHydratorRecursive.php

Lines changed: 3 additions & 39 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
/**

src/ORM/MappingStrategy.php

Lines changed: 3 additions & 1 deletion
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
}

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/TestCase/ORM/NativeQueryMapperTest.php

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Bancer\NativeQueryMapperTest\TestCase;
66

77
use PHPUnit\Framework\TestCase;
8+
use Bancer\NativeQueryMapper\ORM\UnknownAliasException;
89
use Bancer\NativeQueryMapperTest\TestApp\Model\Entity\Article;
910
use Bancer\NativeQueryMapperTest\TestApp\Model\Entity\Comment;
1011
use Bancer\NativeQueryMapperTest\TestApp\Model\Entity\Country;
@@ -14,9 +15,8 @@
1415
use Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable;
1516
use Bancer\NativeQueryMapperTest\TestApp\Model\Table\CommentsTable;
1617
use Bancer\NativeQueryMapperTest\TestApp\Model\Table\CountriesTable;
17-
use Cake\ORM\Locator\LocatorAwareTrait;
18-
use Bancer\NativeQueryMapper\ORM\UnknownAliasException;
1918
use Bancer\NativeQueryMapperTest\TestApp\Model\Table\UsersTable;
19+
use Cake\ORM\Locator\LocatorAwareTrait;
2020

2121
class NativeQueryMapperTest extends TestCase
2222
{
@@ -41,7 +41,9 @@ private function assertEqualsEntities(array $expected, array $actual): void
4141
public function testInvalidAlias(): void
4242
{
4343
$this->expectException(UnknownAliasException::class);
44-
$this->expectExceptionMessage("The query must use root table alias 'Articles'");
44+
$expectedMessage = "The query must select at least one column from the root table.";
45+
$expectedMessage .= " The column alias must use Articles__{column_name} format";
46+
$this->expectExceptionMessage($expectedMessage);
4547
/** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */
4648
$ArticlesTable = $this->fetchTable(ArticlesTable::class);
4749
$stmt = $ArticlesTable->prepareSQL("
@@ -53,6 +55,38 @@ public function testInvalidAlias(): void
5355
$ArticlesTable->fromNativeQuery($stmt)->all();
5456
}
5557

58+
public function testMissingColumnAlias(): void
59+
{
60+
$this->expectException(UnknownAliasException::class);
61+
$this->expectExceptionMessage("Column 'title' must use an alias in the format {Alias}__title");
62+
/** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */
63+
$ArticlesTable = $this->fetchTable(ArticlesTable::class);
64+
$stmt = $ArticlesTable->prepareSQL("
65+
SELECT
66+
id AS Articles__id,
67+
title
68+
FROM articles
69+
");
70+
$ArticlesTable->fromNativeQuery($stmt)->all();
71+
}
72+
73+
public function testIncompleteColumnAlias(): void
74+
{
75+
$this->expectException(UnknownAliasException::class);
76+
$this->expectExceptionMessage(
77+
"Alias 'Articles__' is invalid. Column alias must use {Alias}__{column_name} format",
78+
);
79+
/** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */
80+
$ArticlesTable = $this->fetchTable(ArticlesTable::class);
81+
$stmt = $ArticlesTable->prepareSQL("
82+
SELECT
83+
id AS Articles__id,
84+
title AS Articles__
85+
FROM articles
86+
");
87+
$ArticlesTable->fromNativeQuery($stmt)->all();
88+
}
89+
5690
public function testEmptyResultSet(): void
5791
{
5892
/** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */

0 commit comments

Comments
 (0)