Skip to content

Commit 23d142f

Browse files
committed
added row mapping support via setRowMapping() callback and mapping config option
1 parent 0186d98 commit 23d142f

3 files changed

Lines changed: 180 additions & 2 deletions

File tree

src/Bridges/DatabaseDI/DatabaseExtension.php

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ public function getConfigSchema(): Nette\Schema\Schema
3636
'explain' => Expect::bool(true),
3737
'reflection' => Expect::string(), // BC
3838
'conventions' => Expect::string('discovered'), // Nette\Database\Conventions\DiscoveredConventions
39+
'mapping' => Expect::structure([
40+
'convention' => Expect::string(''),
41+
'tables' => Expect::arrayOf('string', 'string'),
42+
])->before(fn($v) => is_string($v) ? ['convention' => $v] : $v),
3943
'autowired' => Expect::bool(),
4044
]),
4145
)->before(fn($val) => is_array(reset($val)) || reset($val) === null
@@ -121,8 +125,15 @@ private function setupDatabase(\stdClass $config, string $name): void
121125
$conventions = Nette\DI\Helpers::filterArguments([$config->conventions])[0];
122126
}
123127

128+
$rowMapping = ($config->mapping->convention || $config->mapping->tables)
129+
? new Nette\DI\Definitions\Statement([self::class, 'createRowMapping'], [
130+
$config->mapping->convention,
131+
(array) $config->mapping->tables,
132+
])
133+
: null;
134+
124135
$builder->addDefinition($this->prefix("$name.explorer"))
125-
->setFactory(Nette\Database\Explorer::class, [$connection, $structure, $conventions])
136+
->setFactory(Nette\Database\Explorer::class, [$connection, $structure, $conventions, null, $rowMapping])
126137
->setAutowired($config->autowired);
127138

128139
$builder->addAlias($this->prefix("$name.context"), $this->prefix("$name.explorer"));
@@ -133,4 +144,28 @@ private function setupDatabase(\stdClass $config, string $name): void
133144
$builder->addAlias("nette.database.$name.context", $this->prefix("$name.explorer"));
134145
}
135146
}
147+
148+
149+
/**
150+
* Creates a row mapping closure that resolves an ActiveRow subclass for each table name.
151+
* @param array<string, string> $tables
152+
* @return \Closure(string): string
153+
*/
154+
public static function createRowMapping(string $convention, array $tables): \Closure
155+
{
156+
return static function (string $table) use ($convention, $tables): string {
157+
if (isset($tables[$table])) {
158+
return $tables[$table];
159+
}
160+
161+
if ($convention !== '') {
162+
$class = str_replace('*', str_replace(' ', '', ucwords(strtr($table, '_', ' '))), $convention);
163+
if (class_exists($class)) {
164+
return $class;
165+
}
166+
}
167+
168+
return Nette\Database\Table\ActiveRow::class;
169+
};
170+
}
136171
}

src/Database/Explorer.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public function __construct(
2626
private readonly IStructure $structure,
2727
?Conventions $conventions = null,
2828
private readonly ?Nette\Caching\Storage $cacheStorage = null,
29+
/** @var ?\Closure(string): class-string<Table\ActiveRow> */
30+
private readonly ?\Closure $rowMapping = null,
2931
) {
3032
$this->conventions = $conventions ?: new StaticConventions;
3133
}
@@ -121,7 +123,10 @@ public function getConventions(): Conventions
121123
*/
122124
public function createActiveRow(array $data, Table\Selection $selection): Table\ActiveRow
123125
{
124-
return new Table\ActiveRow($data, $selection);
126+
$class = $this->rowMapping
127+
? ($this->rowMapping)($selection->getName())
128+
: Table\ActiveRow::class;
129+
return new $class($data, $selection);
125130
}
126131

127132

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php declare(strict_types=1);
2+
3+
/**
4+
* Test: DatabaseExtension row mapping configuration.
5+
*/
6+
7+
use Nette\Bridges\DatabaseDI\DatabaseExtension;
8+
use Nette\DI;
9+
use Tester\Assert;
10+
11+
require __DIR__ . '/../bootstrap.php';
12+
13+
14+
test('string shortcut mapping', function () {
15+
$loader = new DI\Config\Loader;
16+
$config = $loader->load(Tester\FileMock::create('
17+
database:
18+
dsn: "sqlite::memory:"
19+
mapping: App\Entity\*Row
20+
debugger: no
21+
22+
services:
23+
cache: Nette\Caching\Storages\DevNullStorage
24+
', 'neon'));
25+
26+
$compiler = new DI\Compiler;
27+
$compiler->addExtension('database', new DatabaseExtension(false));
28+
$code = $compiler->addConfig($config)->setClassName('Container1')->compile();
29+
eval($code);
30+
31+
$container = new Container1;
32+
$container->initialize();
33+
34+
$explorer = $container->getService('database.default.explorer');
35+
Assert::type(Nette\Database\Explorer::class, $explorer);
36+
37+
// verify the mapping closure was set by checking generated code
38+
Assert::contains('createRowMapping', $code);
39+
});
40+
41+
42+
test('full mapping with convention and tables', function () {
43+
$loader = new DI\Config\Loader;
44+
$config = $loader->load(Tester\FileMock::create('
45+
database:
46+
dsn: "sqlite::memory:"
47+
mapping:
48+
convention: App\Entity\*Row
49+
tables:
50+
special: App\Entity\SpecialRow
51+
debugger: no
52+
53+
services:
54+
cache: Nette\Caching\Storages\DevNullStorage
55+
', 'neon'));
56+
57+
$compiler = new DI\Compiler;
58+
$compiler->addExtension('database', new DatabaseExtension(false));
59+
$code = $compiler->addConfig($config)->setClassName('Container2')->compile();
60+
eval($code);
61+
62+
$container = new Container2;
63+
$container->initialize();
64+
65+
$explorer = $container->getService('database.default.explorer');
66+
Assert::type(Nette\Database\Explorer::class, $explorer);
67+
Assert::contains('createRowMapping', $code);
68+
});
69+
70+
71+
test('no mapping by default', function () {
72+
$loader = new DI\Config\Loader;
73+
$config = $loader->load(Tester\FileMock::create('
74+
database:
75+
dsn: "sqlite::memory:"
76+
debugger: no
77+
78+
services:
79+
cache: Nette\Caching\Storages\DevNullStorage
80+
', 'neon'));
81+
82+
$compiler = new DI\Compiler;
83+
$compiler->addExtension('database', new DatabaseExtension(false));
84+
$code = $compiler->addConfig($config)->setClassName('Container3')->compile();
85+
eval($code);
86+
87+
$container = new Container3;
88+
$container->initialize();
89+
90+
$explorer = $container->getService('database.default.explorer');
91+
Assert::type(Nette\Database\Explorer::class, $explorer);
92+
93+
// no mapping should be set
94+
Assert::notContains('createRowMapping', $code);
95+
});
96+
97+
98+
test('createRowMapping() with convention', function () {
99+
$mapping = DatabaseExtension::createRowMapping('App\Entity\*Row', []);
100+
101+
// unknown class -> fallback to ActiveRow
102+
Assert::same(Nette\Database\Table\ActiveRow::class, $mapping('nonexistent'));
103+
});
104+
105+
106+
test('createRowMapping() with explicit tables', function () {
107+
$mapping = DatabaseExtension::createRowMapping('', [
108+
'my_table' => 'Nette\Database\Table\ActiveRow',
109+
]);
110+
111+
Assert::same(Nette\Database\Table\ActiveRow::class, $mapping('my_table'));
112+
113+
// not in tables and no convention -> fallback
114+
Assert::same(Nette\Database\Table\ActiveRow::class, $mapping('other'));
115+
});
116+
117+
118+
test('createRowMapping() tables override convention', function () {
119+
$mapping = DatabaseExtension::createRowMapping('Some\*Row', [
120+
'special' => 'Nette\Database\Table\ActiveRow',
121+
]);
122+
123+
// explicit override wins
124+
Assert::same(Nette\Database\Table\ActiveRow::class, $mapping('special'));
125+
});
126+
127+
128+
test('createRowMapping() snake_case to PascalCase', function () {
129+
$mapping = DatabaseExtension::createRowMapping('*', []);
130+
131+
// We can't test actual entity classes (they don't exist in test env),
132+
// but we can verify the fallback for non-existent classes
133+
Assert::same(Nette\Database\Table\ActiveRow::class, $mapping('some_table'));
134+
135+
// Verify convention produces correct class names by using a class that exists
136+
$mapping = DatabaseExtension::createRowMapping('Nette\Database\Table\*', []);
137+
Assert::same('Nette\Database\Table\ActiveRow', $mapping('active_row'));
138+
});

0 commit comments

Comments
 (0)