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
31 changes: 9 additions & 22 deletions src/Styles/Plural.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use function implode;
use function preg_match;
use function preg_replace;
use function ucfirst;

/**
* Default plural table style familiar from frameworks such as Rails, Kohana,
Expand All @@ -20,32 +19,20 @@
* id id id id
* name author_id name post_id
* title category_id
*
* Scope/method names stay PHP-conventional singular camelCase (`post`,
* `postCategory`); only the *table* name pluralizes. That makes `realName` the
* single point of difference from {@see Standard} — class resolution, foreign
* keys, and junction names are all singular/snake and inherited unchanged
* (`composed` is itself expressed through `realName`).
*/
final class Plural extends Standard
{
public function remoteIdentifier(string $name): string
{
return $this->pluralToSingular($name) . '_id';
}

public function styledName(string $name): string
{
$pieces = array_map($this->pluralToSingular(...), explode('_', $name));

return ucfirst($this->separatorToCamelCase(implode('_', $pieces), '_'));
}

public function composed(string $left, string $right): string
public function realName(string $name): string
{
return $this->singularToPlural($left) . '_' . $this->singularToPlural($right);
}
$pieces = array_map($this->singularToPlural(...), explode('_', $this->realProperty($name)));

private function pluralToSingular(string $name): string
{
return $this->applyFirstMatch($name, [
'/^(.+)ies$/' => '$1y',
'/^(.+)s$/' => '$1',
]);
return implode('_', $pieces);
}

private function singularToPlural(string $name): string
Expand Down
9 changes: 7 additions & 2 deletions src/Styles/Standard.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,24 @@ public function styledName(string $name): string
return ucfirst($this->separatorToCamelCase($name, '_'));
}

public function realName(string $name): string
{
return $this->realProperty($name);
}

public function identifier(string $name): string
{
return 'id';
}

public function remoteIdentifier(string $name): string
{
return $name . '_id';
return $this->realProperty($name) . '_id';
}

public function composed(string $left, string $right): string
{
return $left . '_' . $right;
return $this->realName($left) . '_' . $this->realName($right);
}

public function isRemoteIdentifier(string $name): bool
Expand Down
20 changes: 19 additions & 1 deletion src/Styles/Stylable.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,39 @@

namespace Respect\Data\Styles;

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

/** Scope name → DB table name (`postTag` → `post_tag`, or `posts` under Plural). */
public function realName(string $name): string;

/** DB column → PHP property (`post_id` → `postId`). */
public function styledProperty(string $name): string;

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

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

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

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

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

/** Two scope names → junction table name (`post`, `tag` → `post_tag`). */
public function composed(string $left, string $right): string;
}
8 changes: 4 additions & 4 deletions tests/InMemoryMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ public function fetch(Scope $scope, mixed $extra = null): mixed
}
}

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

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

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

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

match ($op) {
Expand Down Expand Up @@ -166,7 +166,7 @@ private function attachChild(array &$parentRow, Scope $child): void
}

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

if ($childRow === null) {
return;
Expand Down
5 changes: 5 additions & 0 deletions tests/Styles/AbstractStyleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public function styledName(string $name): string
return $name;
}

public function realName(string $name): string
{
return $name;
}

public function identifier(string $name): string
{
return 'id';
Expand Down
21 changes: 19 additions & 2 deletions tests/Styles/Plural/PluralIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
use Respect\Data\InMemoryMapper;
use Respect\Data\Styles\Plural;

use function is_object;

#[CoversClass(Plural::class)]
class PluralIntegrationTest extends TestCase
{
Expand Down Expand Up @@ -47,10 +49,25 @@ protected function setUp(): void
}

#[Test]
public function fetchAndPersistRoundTrip(): void
public function fetchSingularScopeResolvesToPluralTable(): void
{
$entity = $this->mapper->fetch($this->mapper->posts());
// Scope name is PHP-conventional singular `post`; the Plural style
// resolves it to the `posts` seed table via realName().
$entity = $this->mapper->fetch($this->mapper->post());
$this->assertIsObject($entity);
$this->assertEquals('Post Title', $this->mapper->entityFactory->get($entity, 'title'));
}

#[Test]
public function fetchNestedRelationResolvesEachTable(): void
{
// post (→ posts) nesting author (→ authors): realName() drives both
// table lookups while the scope names stay singular.
$post = $this->mapper->fetch($this->mapper->post([$this->mapper->author()]));
$this->assertIsObject($post);

$author = $this->mapper->entityFactory->get($post, 'author');
$this->assertTrue(is_object($author));
$this->assertEquals('Author 1', $this->mapper->entityFactory->get($author, 'name'));
}
}
45 changes: 27 additions & 18 deletions tests/Styles/PluralTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ protected function setUp(): void
$this->style = new Plural();
}

/** @return array<int, array<int, string>> */
public static function tableEntityProvider(): array
/**
* Scope name (PHP, singular camelCase) → entity class + DB table.
*
* @return array<int, array<int, string>>
*/
public static function scopeEntityTableProvider(): array
{
return [
['posts', 'Post'],
['comments', 'Comment'],
['categories', 'Category'],
['posts_categories', 'PostCategory'],
['posts_tags', 'PostTag'],
['post', 'Post', 'posts'],
['comment', 'Comment', 'comments'],
['category', 'Category', 'categories'],
['postCategory', 'PostCategory', 'posts_categories'],
['postTag', 'PostTag', 'posts_tags'],
];
}

Expand All @@ -52,22 +56,27 @@ public static function columnsPropertyProvider(): array
];
}

/** @return array<int, array<int, string>> */
/**
* Scope name (PHP, singular camelCase) → foreign key column.
*
* @return array<int, array<int, string>>
*/
public static function foreignProvider(): array
{
return [
['posts', 'post_id'],
['authors', 'author_id'],
['tags', 'tag_id'],
['users', 'user_id'],
['post', 'post_id'],
['author', 'author_id'],
['tag', 'tag_id'],
['user', 'user_id'],
];
}

#[DataProvider('tableEntityProvider')]
public function testTableAndEntitiesMethods(string $table, string $entity): void
#[DataProvider('scopeEntityTableProvider')]
public function testScopeResolvesToEntityClassAndTable(string $scope, string $entity, string $table): void
{
$this->assertEquals($entity, $this->style->styledName($table));
$this->assertEquals('id', $this->style->identifier($table));
$this->assertEquals($entity, $this->style->styledName($scope));
$this->assertEquals($table, $this->style->realName($scope));
$this->assertEquals('id', $this->style->identifier($scope));
}

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

#[DataProvider('foreignProvider')]
public function testForeign(string $table, string $foreign): void
public function testForeign(string $scope, string $foreign): void
{
$this->assertTrue($this->style->isRemoteIdentifier($foreign));
$this->assertEquals($foreign, $this->style->remoteIdentifier($table));
$this->assertEquals($foreign, $this->style->remoteIdentifier($scope));
}
}
Loading