Skip to content

Commit 9148d5b

Browse files
committed
Add Style::realName so scope names convert to tables at the SQL boundary
The mapper DSL used scope/method names verbatim as DB table names, forcing PHP-side code to submit to DB conventions (snake_case, plural) — the very thing Styles exist to prevent. The one identifier a Style was never consulted for was the table name. Add `Stylable::realName(scope): table`, the missing symmetric partner of `styledName(scope): class`, completing the quadrant (styled*->PHP, real*->DB; *Name->entity, *Property->field). Standard delegates it to realProperty; remoteIdentifier and composed are now expressed through realProperty/realName so they convert their inputs too. InMemoryMapper routes table lookups through realName while keeping scope names as aliases. Because realName is the single point of difference for pluralized tables, Plural collapses from three overrides to one (realName); class resolution, foreign keys, and junction names are now inherited from Standard unchanged. All edits are no-ops on existing snake/plural names; scope-name flips live in the consuming projects.
1 parent 6512ccc commit 9148d5b

7 files changed

Lines changed: 90 additions & 49 deletions

File tree

src/Styles/Plural.php

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
use function implode;
1010
use function preg_match;
1111
use function preg_replace;
12-
use function ucfirst;
1312

1413
/**
1514
* Default plural table style familiar from frameworks such as Rails, Kohana,
@@ -20,32 +19,20 @@
2019
* id id id id
2120
* name author_id name post_id
2221
* title category_id
22+
*
23+
* Scope/method names stay PHP-conventional singular camelCase (`post`,
24+
* `postCategory`); only the *table* name pluralizes. That makes `realName` the
25+
* single point of difference from {@see Standard} — class resolution, foreign
26+
* keys, and junction names are all singular/snake and inherited unchanged
27+
* (`composed` is itself expressed through `realName`).
2328
*/
2429
final class Plural extends Standard
2530
{
26-
public function remoteIdentifier(string $name): string
27-
{
28-
return $this->pluralToSingular($name) . '_id';
29-
}
30-
31-
public function styledName(string $name): string
32-
{
33-
$pieces = array_map($this->pluralToSingular(...), explode('_', $name));
34-
35-
return ucfirst($this->separatorToCamelCase(implode('_', $pieces), '_'));
36-
}
37-
38-
public function composed(string $left, string $right): string
31+
public function realName(string $name): string
3932
{
40-
return $this->singularToPlural($left) . '_' . $this->singularToPlural($right);
41-
}
33+
$pieces = array_map($this->singularToPlural(...), explode('_', $this->realProperty($name)));
4234

43-
private function pluralToSingular(string $name): string
44-
{
45-
return $this->applyFirstMatch($name, [
46-
'/^(.+)ies$/' => '$1y',
47-
'/^(.+)s$/' => '$1',
48-
]);
35+
return implode('_', $pieces);
4936
}
5037

5138
private function singularToPlural(string $name): string

src/Styles/Standard.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,24 @@ public function styledName(string $name): string
2727
return ucfirst($this->separatorToCamelCase($name, '_'));
2828
}
2929

30+
public function realName(string $name): string
31+
{
32+
return $this->realProperty($name);
33+
}
34+
3035
public function identifier(string $name): string
3136
{
3237
return 'id';
3338
}
3439

3540
public function remoteIdentifier(string $name): string
3641
{
37-
return $name . '_id';
42+
return $this->realProperty($name) . '_id';
3843
}
3944

4045
public function composed(string $left, string $right): string
4146
{
42-
return $left . '_' . $right;
47+
return $this->realName($left) . '_' . $this->realName($right);
4348
}
4449

4550
public function isRemoteIdentifier(string $name): bool

src/Styles/Stylable.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,39 @@
44

55
namespace Respect\Data\Styles;
66

7+
/**
8+
* Maps between PHP-side identifiers (scope names, properties) and DB-side
9+
* identifiers (tables, columns). The naming follows a quadrant: `styled*`
10+
* produces a PHP identifier, `real*` produces a DB identifier; `*Name` works at
11+
* the entity level (class/table), `*Property` at the field level
12+
* (property/column).
13+
*/
714
interface Stylable
815
{
9-
public function styledName(string $entityName): string;
16+
/** Scope name → entity class short name (`postTag` → `PostTag`). */
17+
public function styledName(string $name): string;
1018

19+
/** Scope name → DB table name (`postTag` → `post_tag`, or `posts` under Plural). */
20+
public function realName(string $name): string;
21+
22+
/** DB column → PHP property (`post_id` → `postId`). */
1123
public function styledProperty(string $name): string;
1224

25+
/** PHP property → DB column (`postId` → `post_id`). */
1326
public function realProperty(string $name): string;
1427

28+
/** Scope name → primary key column. */
1529
public function identifier(string $name): string;
1630

31+
/** Scope name → foreign key column (`post` → `post_id`). */
1732
public function remoteIdentifier(string $name): string;
1833

34+
/** Is this column a foreign key? */
1935
public function isRemoteIdentifier(string $name): bool;
2036

37+
/** Foreign key column → relation scope name (`post_id` → `post`), or null. */
2138
public function relationProperty(string $remoteIdentifierField): string|null;
2239

40+
/** Two scope names → junction table name (`post`, `tag` → `post_tag`). */
2341
public function composed(string $left, string $right): string;
2442
}

tests/InMemoryMapper.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@ public function fetch(Scope $scope, mixed $extra = null): mixed
3131
}
3232
}
3333

34-
$row = $this->findRow((string) $scope->name, $scope->filter);
34+
$row = $this->findRow($this->style->realName((string) $scope->name), $scope->filter);
3535

3636
return $row !== null ? $this->hydrateRow($row, $scope) : false;
3737
}
3838

3939
/** @return array<int, mixed> */
4040
public function fetchAll(Scope $scope, mixed $extra = null): array
4141
{
42-
$rows = $this->findRows((string) $scope->name, $scope->filter);
42+
$rows = $this->findRows($this->style->realName((string) $scope->name), $scope->filter);
4343
$result = [];
4444

4545
foreach ($rows as $row) {
@@ -59,7 +59,7 @@ public function flush(): void
5959
foreach ($this->pending as $entity) {
6060
$op = $this->pending[$entity];
6161
$scope = $this->tracked[$entity];
62-
$tableName = (string) $scope->name;
62+
$tableName = $this->style->realName((string) $scope->name);
6363
$id = $this->style->identifier($tableName);
6464

6565
match ($op) {
@@ -166,7 +166,7 @@ private function attachChild(array &$parentRow, Scope $child): void
166166
}
167167

168168
$id = $this->style->identifier($childName);
169-
$childRow = $this->findRowById($childName, $id, $refValue);
169+
$childRow = $this->findRowById($this->style->realName($childName), $id, $refValue);
170170

171171
if ($childRow === null) {
172172
return;

tests/Styles/AbstractStyleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ public function styledName(string $name): string
3232
return $name;
3333
}
3434

35+
public function realName(string $name): string
36+
{
37+
return $name;
38+
}
39+
3540
public function identifier(string $name): string
3641
{
3742
return 'id';

tests/Styles/Plural/PluralIntegrationTest.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use Respect\Data\InMemoryMapper;
1313
use Respect\Data\Styles\Plural;
1414

15+
use function is_object;
16+
1517
#[CoversClass(Plural::class)]
1618
class PluralIntegrationTest extends TestCase
1719
{
@@ -47,10 +49,25 @@ protected function setUp(): void
4749
}
4850

4951
#[Test]
50-
public function fetchAndPersistRoundTrip(): void
52+
public function fetchSingularScopeResolvesToPluralTable(): void
5153
{
52-
$entity = $this->mapper->fetch($this->mapper->posts());
54+
// Scope name is PHP-conventional singular `post`; the Plural style
55+
// resolves it to the `posts` seed table via realName().
56+
$entity = $this->mapper->fetch($this->mapper->post());
5357
$this->assertIsObject($entity);
5458
$this->assertEquals('Post Title', $this->mapper->entityFactory->get($entity, 'title'));
5559
}
60+
61+
#[Test]
62+
public function fetchNestedRelationResolvesEachTable(): void
63+
{
64+
// post (→ posts) nesting author (→ authors): realName() drives both
65+
// table lookups while the scope names stay singular.
66+
$post = $this->mapper->fetch($this->mapper->post([$this->mapper->author()]));
67+
$this->assertIsObject($post);
68+
69+
$author = $this->mapper->entityFactory->get($post, 'author');
70+
$this->assertTrue(is_object($author));
71+
$this->assertEquals('Author 1', $this->mapper->entityFactory->get($author, 'name'));
72+
}
5673
}

tests/Styles/PluralTest.php

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,19 @@ protected function setUp(): void
1818
$this->style = new Plural();
1919
}
2020

21-
/** @return array<int, array<int, string>> */
22-
public static function tableEntityProvider(): array
21+
/**
22+
* Scope name (PHP, singular camelCase) → entity class + DB table.
23+
*
24+
* @return array<int, array<int, string>>
25+
*/
26+
public static function scopeEntityTableProvider(): array
2327
{
2428
return [
25-
['posts', 'Post'],
26-
['comments', 'Comment'],
27-
['categories', 'Category'],
28-
['posts_categories', 'PostCategory'],
29-
['posts_tags', 'PostTag'],
29+
['post', 'Post', 'posts'],
30+
['comment', 'Comment', 'comments'],
31+
['category', 'Category', 'categories'],
32+
['postCategory', 'PostCategory', 'posts_categories'],
33+
['postTag', 'PostTag', 'posts_tags'],
3034
];
3135
}
3236

@@ -52,22 +56,27 @@ public static function columnsPropertyProvider(): array
5256
];
5357
}
5458

55-
/** @return array<int, array<int, string>> */
59+
/**
60+
* Scope name (PHP, singular camelCase) → foreign key column.
61+
*
62+
* @return array<int, array<int, string>>
63+
*/
5664
public static function foreignProvider(): array
5765
{
5866
return [
59-
['posts', 'post_id'],
60-
['authors', 'author_id'],
61-
['tags', 'tag_id'],
62-
['users', 'user_id'],
67+
['post', 'post_id'],
68+
['author', 'author_id'],
69+
['tag', 'tag_id'],
70+
['user', 'user_id'],
6371
];
6472
}
6573

66-
#[DataProvider('tableEntityProvider')]
67-
public function testTableAndEntitiesMethods(string $table, string $entity): void
74+
#[DataProvider('scopeEntityTableProvider')]
75+
public function testScopeResolvesToEntityClassAndTable(string $scope, string $entity, string $table): void
6876
{
69-
$this->assertEquals($entity, $this->style->styledName($table));
70-
$this->assertEquals('id', $this->style->identifier($table));
77+
$this->assertEquals($entity, $this->style->styledName($scope));
78+
$this->assertEquals($table, $this->style->realName($scope));
79+
$this->assertEquals('id', $this->style->identifier($scope));
7180
}
7281

7382
#[DataProvider('columnsPropertyProvider')]
@@ -85,9 +94,9 @@ public function testTableFromLeftRightTable(string $left, string $right, string
8594
}
8695

8796
#[DataProvider('foreignProvider')]
88-
public function testForeign(string $table, string $foreign): void
97+
public function testForeign(string $scope, string $foreign): void
8998
{
9099
$this->assertTrue($this->style->isRemoteIdentifier($foreign));
91-
$this->assertEquals($foreign, $this->style->remoteIdentifier($table));
100+
$this->assertEquals($foreign, $this->style->remoteIdentifier($scope));
92101
}
93102
}

0 commit comments

Comments
 (0)