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
5 changes: 5 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@
<rule ref="Respect">
<exclude name="SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming.SuperfluousPrefix" />
</rule>

<!-- Test code and stub entities use snake_case properties matching DB columns -->
<rule ref="Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps">
<exclude-pattern>tests/</exclude-pattern>
</rule>
</ruleset>
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ parameters:
- tests/
ignoreErrors:
- message: '/Call to an undefined (static )?method Respect\\Data\\(AbstractMapper|Collections\\(Collection|Filtered|Mix|Typed))::\w+\(\)\./'
- message: '/Access to an undefined property Respect\\Data\\(AbstractMapper|Collections\\Collection)::\$\w+\./'
- message: '/Access to an undefined property Respect\\Data\\(AbstractMapper|InMemoryMapper|Collections\\Collection)::\$\w+\./'
- message: '/Unsafe usage of new static\(\)\./'
-
message: '/Expression .+ on a separate line does not do anything\./'
Expand Down
48 changes: 5 additions & 43 deletions src/AbstractMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ public function setStyle(Styles\Stylable $style): static

abstract public function flush(): void;

abstract public function fetch(Collection $collection, mixed $extra = null): mixed;

/** @return array<int, mixed> */
abstract public function fetchAll(Collection $collection, mixed $extra = null): array;

public function reset(): void
{
$this->changed = new SplObjectStorage();
Expand All @@ -62,30 +67,6 @@ public function markTracked(object $entity, Collection $collection): bool
return true;
}

public function fetch(Collection $fromCollection, mixed $withExtra = null): mixed
{
$statement = $this->createStatement($fromCollection, $withExtra);
$hydrated = $this->fetchHydrated($fromCollection, $statement);
if (!$hydrated) {
return false;
}

return $this->parseHydrated($hydrated);
}

/** @return array<int, mixed> */
public function fetchAll(Collection $fromCollection, mixed $withExtra = null): array
{
$statement = $this->createStatement($fromCollection, $withExtra);
$entities = [];

while ($hydrated = $this->fetchHydrated($fromCollection, $statement)) {
$entities[] = $this->parseHydrated($hydrated);
}

return $entities;
}

public function persist(object $object, Collection $onCollection): bool
{
$this->changed[$object] = true;
Expand Down Expand Up @@ -125,25 +106,6 @@ public function registerCollection(string $alias, Collection $collection): void
$this->collections[$alias] = $collection;
}

abstract protected function createStatement(Collection $fromCollection, mixed $withExtra = null): mixed;

protected function parseHydrated(SplObjectStorage $hydrated): mixed
{
$this->tracked->addAll($hydrated);
$hydrated->rewind();

return $hydrated->current();
}

protected function fetchHydrated(Collection $collection, mixed $statement): SplObjectStorage|false
{
if (!$collection->hasMore()) {
return $this->fetchSingle($collection, $statement);
}

return $this->fetchMulti($collection, $statement);
}

public function __get(string $name): Collection
{
if (isset($this->collections[$name])) {
Expand Down
10 changes: 8 additions & 2 deletions tests/AbstractMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,15 @@ public function flush(): void
{
}

protected function createStatement(Collection $fromCollection, mixed $withExtra = null): mixed
public function fetch(Collection $collection, mixed $extra = null): mixed
{
return null;
return false;
}

/** @return array<int, mixed> */
public function fetchAll(Collection $collection, mixed $extra = null): array
{
return [];
}
};
}
Expand Down
248 changes: 248 additions & 0 deletions tests/InMemoryMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
<?php

declare(strict_types=1);

namespace Respect\Data;

use Respect\Data\Collections\Collection;
use stdClass;

use function array_filter;
use function array_key_exists;
use function array_values;
use function assert;
use function class_exists;
use function get_object_vars;
use function is_array;
use function reset;

final class InMemoryMapper extends AbstractMapper
{
/** @var array<string, list<array<string, mixed>>> */
private array $tables = [];

private int $lastInsertId = 1000;

public string $entityNamespace = '\\';

/** @param list<array<string, mixed>> $rows */
public function seed(string $table, array $rows): void
{
$this->tables[$table] = $rows;
}

public function fetch(Collection $collection, mixed $extra = null): mixed
{
$name = (string) $collection->getName();
$rows = $this->tables[$name] ?? [];
$condition = $collection->getCondition();
$style = $this->getStyle();

if ($condition !== null && $condition !== []) {
$pk = $style->identifier($name);
$pkValue = is_array($condition) ? reset($condition) : $condition;
$rows = array_values(array_filter(
$rows,
static fn(array $row): bool => isset($row[$pk]) && $row[$pk] == $pkValue,
));
}

if ($rows === []) {
return false;
}

$row = $rows[0];
$entity = $this->createEntity($name);

foreach ($row as $key => $value) {
$entity->{$key} = $value;
}

if ($collection->hasMore()) {
$this->resolveRelations($entity, $collection);
}

$this->markTracked($entity, $collection);

return $entity;
}

/** @return array<int, mixed> */
public function fetchAll(Collection $collection, mixed $extra = null): array
{
$name = (string) $collection->getName();
$rows = $this->tables[$name] ?? [];
$condition = $collection->getCondition();
$style = $this->getStyle();

if ($condition !== null && $condition !== []) {
$pk = $style->identifier($name);
$pkValue = is_array($condition) ? reset($condition) : $condition;
$rows = array_values(array_filter(
$rows,
static fn(array $row): bool => isset($row[$pk]) && $row[$pk] == $pkValue,
));
}

$entities = [];

foreach ($rows as $row) {
$entity = $this->createEntity($name);

foreach ($row as $key => $value) {
$entity->{$key} = $value;
}

if ($collection->hasMore()) {
$this->resolveRelations($entity, $collection);
}

$this->markTracked($entity, $collection);
$entities[] = $entity;
}

return $entities;
}

public function flush(): void
{
foreach ($this->new as $entity) {
$collection = $this->tracked[$entity];
assert($collection instanceof Collection);
$tableName = (string) $collection->getName();
$pk = $this->getStyle()->identifier($tableName);
$row = get_object_vars($entity);

if (!isset($row[$pk])) {
++$this->lastInsertId;
$entity->{$pk} = $this->lastInsertId;
$row[$pk] = $this->lastInsertId;
}

$this->tables[$tableName][] = $row;
}

foreach ($this->changed as $entity) {
if ($this->new->offsetExists($entity)) {
continue;
}

if ($this->removed->offsetExists($entity)) {
continue;
}

$collection = $this->tracked[$entity];
assert($collection instanceof Collection);
$tableName = (string) $collection->getName();
$pk = $this->getStyle()->identifier($tableName);
$pkValue = $entity->{$pk};
$row = get_object_vars($entity);

foreach ($this->tables[$tableName] as $index => $existing) {
if (isset($existing[$pk]) && $existing[$pk] == $pkValue) {
$this->tables[$tableName][$index] = $row;

break;
}
}
}

foreach ($this->removed as $entity) {
$collection = $this->tracked[$entity];
assert($collection instanceof Collection);
$tableName = (string) $collection->getName();
$pk = $this->getStyle()->identifier($tableName);
$pkValue = $entity->{$pk};

foreach ($this->tables[$tableName] as $index => $existing) {
if (isset($existing[$pk]) && $existing[$pk] == $pkValue) {
unset($this->tables[$tableName][$index]);
$this->tables[$tableName] = array_values($this->tables[$tableName]);

break;
}
}
}

$this->reset();
}

private function resolveRelations(object $entity, Collection $collection): void
{
$style = $this->getStyle();
$next = $collection->getNext();

if ($next !== null) {
$nextName = (string) $next->getName();
$fkCol = $style->remoteIdentifier($nextName);

if (array_key_exists($fkCol, get_object_vars($entity))) {
$fkValue = $entity->{$fkCol};
$childEntity = $this->findRelatedEntity($nextName, $fkValue, $next);

if ($childEntity !== null) {
$entity->{$fkCol} = $childEntity;
}
}
}

foreach ($collection->getChildren() as $child) {
$childName = (string) $child->getName();
$fkCol = $style->remoteIdentifier($childName);

if (!array_key_exists($fkCol, get_object_vars($entity))) {
continue;
}

$fkValue = $entity->{$fkCol};
$childEntity = $this->findRelatedEntity($childName, $fkValue, $child);

if ($childEntity === null) {
continue;
}

$entity->{$fkCol} = $childEntity;
}
}

private function findRelatedEntity(string $tableName, mixed $fkValue, Collection $collection): object|null
{
$style = $this->getStyle();
$pk = $style->identifier($tableName);
$rows = $this->tables[$tableName] ?? [];

foreach ($rows as $row) {
if (!isset($row[$pk]) || $row[$pk] != $fkValue) {
continue;
}

$childEntity = $this->createEntity($tableName);

foreach ($row as $key => $value) {
$childEntity->{$key} = $value;
}

if ($collection->hasMore()) {
$this->resolveRelations($childEntity, $collection);
}

$this->markTracked($childEntity, $collection);

return $childEntity;
}

return null;
}

private function createEntity(string $entityName): object
{
$className = $this->getStyle()->styledName($entityName);
$fullClass = $this->entityNamespace . $className;

if (class_exists($fullClass)) {
return new $fullClass();
}

return new stdClass();
}
}
Loading
Loading