Skip to content

Commit

Permalink
Merge pull request #417: fix bad public relations hydration on non-pr…
Browse files Browse the repository at this point in the history
…oxy entity
  • Loading branch information
roxblnfk authored Jun 19, 2023
2 parents 1cffe0d + 52e0043 commit 15ea9c0
Show file tree
Hide file tree
Showing 13 changed files with 550 additions and 37 deletions.
76 changes: 40 additions & 36 deletions src/Mapper/Proxy/Hydrator/ClosureHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,56 +83,60 @@ private function setEntityProperties(array $properties, object $object, array &$
}

/**
* Map private entity relations
* Map private relations of non-proxy entity
*/
private function setRelationProperties(array $properties, RelationMap $relMap, object $object, array &$data): void
{
$refl = new \ReflectionClass($object);
$setter = static function (object $object, array $props, array &$data) use ($refl, $relMap): void {
foreach ($props as $property) {
if (!\array_key_exists($property, $data)) {
continue;
}

foreach ($properties as $class => $props) {
if ($class === '') {
continue;
}

Closure::bind(static function (object $object, array $props, array &$data) use ($refl, $relMap): void {
foreach ($props as $property) {
if (!\array_key_exists($property, $data)) {
continue;
}

$value = $data[$property];
$value = $data[$property];

if ($value instanceof ReferenceInterface) {
$prop = $refl->getProperty($property);
if ($value instanceof ReferenceInterface) {
$prop = $refl->getProperty($property);

if ($prop->hasType()) {
/** @var \ReflectionNamedType[] $types */
$types = $prop->getType() instanceof \ReflectionUnionType
? $prop->getType()->getTypes()
: [$prop->getType()];
if ($prop->hasType()) {
// todo: we can cache this
/** @var \ReflectionNamedType[] $types */
$types = $prop->getType() instanceof \ReflectionUnionType
? $prop->getType()->getTypes()
: [$prop->getType()];

foreach ($types as $type) {
$c = $type->getName();
if ($c === 'object' || $value instanceof $c) {
$object->{$property} = $value;
unset($data[$property]);
foreach ($types as $type) {
$c = $type->getName();
if ($c === 'object' || $value instanceof $c) {
$object->{$property} = $value;
unset($data[$property]);

// go to next property
continue 2;
}
// go to next property
continue 2;
}
}

$relation = $relMap->getRelations()[$property] ?? null;
if ($relation !== null) {
$value = $relation->collect($relation->resolve($value, true));
}
$relation = $relMap->getRelations()[$property] ?? null;
if ($relation !== null) {
$value = $relation->collect($relation->resolve($value, true));
}
}

$object->{$property} = $value;
unset($data[$property]);
}
}, null, $class)($object, $props, $data);

$object->{$property} = $value;
unset($data[$property]);
}
};

foreach ($properties as $class => $props) {
if ($class === '') {
// Hydrate public properties
$setter($object, $props, $data);
continue;
}

Closure::bind($setter, null, $class)($object, $props, $data);
}
}
}
2 changes: 1 addition & 1 deletion src/Mapper/StdMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function hydrate($entity, array $data): object
{
$relations = $this->relationMap->getRelations();
foreach ($data as $k => $v) {
if ($v instanceof ReferenceInterface && array_key_exists($k, $relations)) {
if ($v instanceof ReferenceInterface && \array_key_exists($k, $relations)) {
$relation = $relations[$k];
$relation->resolve($v, false);

Expand Down
100 changes: 100 additions & 0 deletions tests/ORM/Functional/Driver/Common/Integration/Case416/CaseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case416;

use Cycle\ORM\Select;
use Cycle\ORM\Tests\Functional\Driver\Common\BaseTest;
use Cycle\ORM\Tests\Functional\Driver\Common\Integration\IntegrationTestTrait;
use Cycle\ORM\Tests\Traits\TableTrait;
use Ramsey\Uuid\Uuid;

abstract class CaseTest extends BaseTest
{
use IntegrationTestTrait;
use TableTrait;

public function setUp(): void
{
// Init DB
parent::setUp();
$this->makeTables();

$this->loadSchema(__DIR__ . '/schema.php');
}

public function testSelect(): void
{
$uuid = Uuid::uuid7();
$identity = new Entity\Identity($uuid);
$profile = new Entity\Profile($uuid);
$account = new Entity\Account($uuid, 'test@mail.com', \md5('password'));
$identity->profile = $profile;
$identity->account = $account;

$this->save($identity);

// Note: heap cleaning fixes this issue
// $this->orm->getHeap()->clean();

// Get entity
(new Select($this->orm, Entity\Account::class))
->load('identity.profile')
->wherePK((string)$uuid)
->fetchOne();

// There is no any exception like this:
// [Cycle\ORM\Exception\MapperException]
// Can't hydrate an entity because property and value types are incompatible.
//
// [TypeError]
// Cannot assign Cycle\ORM\Reference\Reference to property
// Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case416\Entity\Profile::$identity of type
// Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case416\Entity\Identity
$this->assertTrue(true);

// To avoid `Entity and State are not in sync` exception
$this->orm->getHeap()->clean();
}

private function makeTables(): void
{
$this->makeTable(Entity\Identity::ROLE, [
'uuid' => 'string,primary',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime,nullable',
]);

$this->makeTable(Entity\Account::ROLE, [
'uuid' => 'string,primary',
'email' => 'string',
'password_hash' => 'string',
'updated_at' => 'datetime',
]);
$this->makeFK(
Entity\Account::ROLE,
'uuid',
Entity\Identity::ROLE,
'uuid',
'NO ACTION',
'NO ACTION',
);

$this->makeTable(Entity\Profile::ROLE, [
'uuid' => 'string,primary',
'updated_at' => 'datetime',
'first_name' => 'string',
'last_name' => 'string',
]);
$this->makeFK(
Entity\Profile::ROLE,
'uuid',
Entity\Identity::ROLE,
'uuid',
'NO ACTION',
'NO ACTION',
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case416\Entity;

use DateTimeInterface;
use Ramsey\Uuid\UuidInterface;

class Account
{
public const ROLE = 'account';

public const F_UUID = 'uuid';
public const F_EMAIL = 'email';
public const F_PASSWORD_HASH = 'passwordHash';
public const F_UPDATED_AT = 'updatedAt';

/** @readonly */
public DateTimeInterface $updatedAt;

public Identity $identity;

public function __construct(
/** @readonly */
public UuidInterface $uuid,
public string $email,
public string $passwordHash,
) {
$this->updatedAt = new \DateTimeImmutable();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case416\Entity;

use DateTimeInterface;
use Ramsey\Uuid\UuidInterface;

class Identity
{
public const ROLE = 'identity';

public const F_UUID = 'uuid';
public const F_CREATED_AT = 'createdAt';
public const F_UPDATED_AT = 'updatedAt';
public const F_DELETED_AT = 'deletedAt';

/** @readonly */
public DateTimeInterface $createdAt;
/** @readonly */
public DateTimeInterface $updatedAt;
/** @readonly */
public ?DateTimeInterface $deletedAt = null;

public Profile $profile;

public Account $account;

public function __construct(
/** @readonly */
public UuidInterface $uuid,
) {
$now = new \DateTimeImmutable();
$this->createdAt = $now;
$this->updatedAt = $now;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case416\Entity;

use DateTimeInterface;
use Ramsey\Uuid\UuidInterface;

class Profile
{
public const ROLE = 'profile';

public const F_UUID = 'uuid';
public const F_UPDATED_AT = 'updatedAt';

/** @readonly */
public DateTimeInterface $updatedAt;

/** @psalm-suppress InvalidArgument */
public UserName $name;

public Identity $identity;

public function __construct(
/** @readonly */
public UuidInterface $uuid,
) {
$this->name = new UserName();
$this->updatedAt = new \DateTimeImmutable();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case416\Entity;

class UserName
{
public const ROLE = 'UserName';

public const F_FIRST_NAME = 'firstName';
public const F_LAST_NAME = 'lastName';

public function __construct(
public string $firstName = '',
public string $lastName = '',
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case416\Typecast;

use Cycle\ORM\Parser\CastableInterface;
use Cycle\ORM\Parser\UncastableInterface;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;

class UuidTypecast implements CastableInterface, UncastableInterface
{
/** @var non-empty-string[] */
private array $rules = [];

public function setRules(array $rules): array
{
/**
* @var non-empty-string $key
* @var mixed $rule
*/
foreach ($rules as $key => $rule) {
if ($rule === 'uuid') {
unset($rules[$key]);
$this->rules[$key] = $rule;
}
}

return $rules;
}

public function cast(array $data): array
{
foreach ($this->rules as $column => $rule) {
if (!isset($data[$column])) {
continue;
}

\assert(\is_string($data[$column]));
$data[$column] = Uuid::fromString($data[$column]);
}

return $data;
}

public function uncast(array $data): array
{
foreach ($this->rules as $column => $rule) {
if (!isset($data[$column]) || !$data[$column] instanceof UuidInterface) {
continue;
}

$data[$column] = $data[$column]->toString();
}

return $data;
}
}
Loading

0 comments on commit 15ea9c0

Please sign in to comment.