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
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ parameters:
- src/
- tests/
ignoreErrors:
- message: '/Call to an undefined (static )?method Respect\\Data\\(AbstractMapper|InMemoryMapper|Collections\\Collection)::\w+\(\)\./'
- message: '/Call to an undefined (static )?method Respect\\Data\\(AbstractMapper|InMemoryMapper|Scope)::\w+\(\)\./'
- message: '/Unsafe usage of new static\(\)\./'
-
message: '/Property .+ is never read, only written\./'
Expand Down
116 changes: 40 additions & 76 deletions src/AbstractMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Respect\Data;

use Respect\Data\Collections\Collection;
use SplObjectStorage;

use function count;
Expand All @@ -15,18 +14,15 @@

abstract class AbstractMapper
{
/** @var SplObjectStorage<object, Collection> Maps entity → source Collection */
/** @var SplObjectStorage<object, Scope> Maps entity → source Scope */
protected SplObjectStorage $tracked;

/** @var SplObjectStorage<object, string> Maps entity → 'insert'|'update'|'delete' */
protected SplObjectStorage $pending;

/** @var array<string, array<int|string, object>> Identity-indexed map: [collectionName][idValue] → entity */
/** @var array<string, array<int|string, object>> Identity-indexed map: [scopeName][idValue] → entity */
protected array $identityMap = [];

/** @var array<string, Collection> */
private array $collections = [];

public EntityFactory $entityFactory { get => $this->hydrator->entityFactory; }

public Styles\Stylable $style { get => $this->entityFactory->style; }
Expand All @@ -40,10 +36,10 @@ public function __construct(

abstract public function flush(): void;

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

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

public function reset(): void
{
Expand All @@ -55,29 +51,7 @@ public function clearIdentityMap(): void
$this->identityMap = [];
}

public function trackedCount(): int
{
return count($this->tracked);
}

public function identityMapCount(): int
{
$total = 0;
foreach ($this->identityMap as $entries) {
$total += count($entries);
}

return $total;
}

public function markTracked(object $entity, Collection $collection): bool
{
$this->tracked[$entity] = $collection;

return true;
}

public function persist(object $object, Collection $onCollection): object
public function persist(object $object, Scope $onScope): object
{
if ($this->isTracked($object)) {
$currentOp = $this->pending[$object] ?? null;
Expand All @@ -88,45 +62,56 @@ public function persist(object $object, Collection $onCollection): object
return $object;
}

$merged = $this->tryMergeWithIdentityMap($object, $onCollection);
$merged = $this->tryMergeWithIdentityMap($object, $onScope);
if ($merged !== null) {
return $merged;
}

$this->pending[$object] = 'insert';
$this->markTracked($object, $onCollection);
$this->registerInIdentityMap($object, $onCollection);
$this->markTracked($object, $onScope);
$this->registerInIdentityMap($object, $onScope);

return $object;
}

public function remove(object $object, Collection $fromCollection): bool
public function remove(object $object, Scope $fromScope): void
{
$this->pending[$object] = 'delete';

if (!$this->isTracked($object)) {
$this->markTracked($object, $fromCollection);
if ($this->isTracked($object)) {
return;
}

return true;
$this->markTracked($object, $fromScope);
}

public function isTracked(object $entity): bool
public function trackedCount(): int
{
return $this->tracked->offsetExists($entity);
return count($this->tracked);
}

public function registerCollection(string $alias, Collection $collection): void
public function identityMapCount(): int
{
$this->collections[$alias] = $collection;
$total = 0;
foreach ($this->identityMap as $entries) {
$total += count($entries);
}

return $total;
}

protected function registerInIdentityMap(object $entity, Collection $coll): void
public function markTracked(object $entity, Scope $scope): void
{
if ($coll->name === null) {
return;
}
$this->tracked[$entity] = $scope;
}

public function isTracked(object $entity): bool
{
return $this->tracked->offsetExists($entity);
}

protected function registerInIdentityMap(object $entity, Scope $coll): void
{
$idValue = $this->entityIdValue($entity, $coll->name);
if ($idValue === null) {
return;
Expand All @@ -135,12 +120,8 @@ protected function registerInIdentityMap(object $entity, Collection $coll): void
$this->identityMap[$coll->name][$idValue] = $entity;
}

protected function evictFromIdentityMap(object $entity, Collection $coll): void
protected function evictFromIdentityMap(object $entity, Scope $coll): void
{
if ($coll->name === null) {
return;
}

$idValue = $this->entityIdValue($entity, $coll->name);
if ($idValue === null) {
return;
Expand All @@ -149,26 +130,22 @@ protected function evictFromIdentityMap(object $entity, Collection $coll): void
unset($this->identityMap[$coll->name][$idValue]);
}

protected function findInIdentityMap(Collection $collection): object|null
protected function findInIdentityMap(Scope $scope): object|null
{
if ($collection->name === null || !is_scalar($collection->filter) || $collection->hasChildren) {
if (!is_scalar($scope->filter) || $scope->hasChildren) {
return null;
}

$condition = $this->normalizeIdValue($collection->filter);
$condition = $this->normalizeIdValue($scope->filter);
if ($condition === null) {
return null;
}

return $this->identityMap[$collection->name][$condition] ?? null;
return $this->identityMap[$scope->name][$condition] ?? null;
}

private function tryMergeWithIdentityMap(object $entity, Collection $coll): object|null
private function tryMergeWithIdentityMap(object $entity, Scope $coll): object|null
{
if ($coll->name === null) {
return null;
}

$entityId = $this->entityIdValue($entity, $coll->name);
$idValue = $entityId ?? $this->normalizeIdValue($coll->filter);

Expand Down Expand Up @@ -237,22 +214,9 @@ private function normalizeIdValue(mixed $value): int|string|null
return null;
}

public function __isset(string $alias): bool
/** @param array<string, mixed> $arguments */
public function __call(string $name, array $arguments): Scope
{
return isset($this->collections[$alias]);
}

/** @param list<mixed> $arguments */
public function __call(string $name, array $arguments): Collection
{
if (isset($this->collections[$name])) {
if (empty($arguments)) {
return clone $this->collections[$name];
}

return $this->collections[$name]->derive(...$arguments); // @phpstan-ignore argument.type
}

return new Collection($name, ...$arguments); // @phpstan-ignore argument.type
return new Scope($name, ...$arguments); // @phpstan-ignore argument.type
}
}
12 changes: 6 additions & 6 deletions src/EntityFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,17 +203,17 @@ public function extractProperties(object $entity): array
}

/**
* Enumerate persistable fields for a collection, mapping DB column names to styled property names.
* Enumerate persistable fields for a scope, mapping DB column names to styled property names.
*
* @return array<string, string> DB column name → styled property name
*/
public function enumerateFields(string $collectionName): array
public function enumerateFields(string $scopeName): array
{
if (isset($this->fieldCache[$collectionName])) {
return $this->fieldCache[$collectionName];
if (isset($this->fieldCache[$scopeName])) {
return $this->fieldCache[$scopeName];
}

$class = $this->resolveClass($collectionName);
$class = $this->resolveClass($scopeName);
$relations = $this->detectRelationProperties($class);
$fields = [];

Expand All @@ -225,7 +225,7 @@ public function enumerateFields(string $collectionName): array
$fields[$this->style->realProperty($name)] = $name;
}

return $this->fieldCache[$collectionName] = $fields;
return $this->fieldCache[$scopeName] = $fields;
}

/**
Expand Down
9 changes: 4 additions & 5 deletions src/Hydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,22 @@

namespace Respect\Data;

use Respect\Data\Collections\Collection;
use SplObjectStorage;

/** Transforms raw backend data into entity instances mapped to their collections */
/** Transforms raw backend data into entity instances mapped to their scopes */
interface Hydrator
{
public EntityFactory $entityFactory { get; }

/** Returns just the root entity */
public function hydrate(
mixed $raw,
Collection $collection,
Scope $scope,
): object|false;

/** @return SplObjectStorage<object, Collection>|false */
/** @return SplObjectStorage<object, Scope>|false */
public function hydrateAll(
mixed $raw,
Collection $collection,
Scope $scope,
): SplObjectStorage|false;
}
24 changes: 12 additions & 12 deletions src/Hydrators/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
namespace Respect\Data\Hydrators;

use DomainException;
use Respect\Data\Collections\Collection;
use Respect\Data\EntityFactory;
use Respect\Data\Hydrator;
use Respect\Data\Scope;
use SplObjectStorage;

/** Base hydrator providing collection-tree entity wiring */
/** Base hydrator providing scope-tree entity wiring */
abstract class Base implements Hydrator
{
public function __construct(
Expand All @@ -20,52 +20,52 @@ public function __construct(

public function hydrate(
mixed $raw,
Collection $collection,
Scope $scope,
): object|false {
$entities = $this->hydrateAll($raw, $collection);
$entities = $this->hydrateAll($raw, $scope);
if ($entities === false) {
return false;
}

foreach ($entities as $entity) {
if ($entities[$entity] === $collection) {
if ($entities[$entity] === $scope) {
return $entity;
}
}

throw new DomainException(
'Hydration produced no entity for collection "' . $collection->name . '"',
'Hydration produced no entity for scope "' . $scope->name . '"',
);
}

/** @param SplObjectStorage<object, Collection> $entities */
/** @param SplObjectStorage<object, Scope> $entities */
protected function wireRelationships(SplObjectStorage $entities): void
{
$style = $this->entityFactory->style;
$others = clone $entities;

foreach ($entities as $entity) {
$coll = $entities[$entity];
$scope = $entities[$entity];

foreach ($others as $other) {
if ($other === $entity) {
continue;
}

$otherColl = $others[$other];
if ($otherColl->parent !== $coll || $otherColl->name === null) {
$otherScope = $others[$other];
if ($otherScope->parent !== $scope) {
continue;
}

$relationName = $style->relationProperty(
$style->remoteIdentifier($otherColl->name),
$style->remoteIdentifier($otherScope->name),
);

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

$id = $this->entityFactory->get($other, $style->identifier($otherColl->name));
$id = $this->entityFactory->get($other, $style->identifier($otherScope->name));
if ($id === null) {
continue;
}
Expand Down
Loading
Loading