Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changed behavior of EntityFactory #509

Merged
merged 4 commits into from
Mar 2, 2025
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
4 changes: 4 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ jobs:
- "8.3"
- "8.4"
steps:
- name: Install ODBC driver.
run: |
sudo curl https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list
sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18
- name: Checkout
uses: actions/checkout@v2
- name: Setup DB services
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@
"mockery/mockery": "^1.1",
"phpunit/phpunit": "^9.5",
"ramsey/uuid": "^4.0",
"spiral/tokenizer": "^2.8 || ^3.0",
"spiral/code-style": "~2.2.0",
"spiral/tokenizer": "^2.8 || ^3.0",
"symfony/var-dumper": "^5.2 || ^6.0 || ^7.0",
"vimeo/psalm": "5.21"
"vimeo/psalm": "5.21 || ^6.8"
},
"autoload": {
"psr-4": {
Expand Down
2,813 changes: 1,848 additions & 965 deletions psalm-baseline.xml

Large diffs are not rendered by default.

7 changes: 2 additions & 5 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,14 @@
</ignoreFiles>
</projectFiles>
<issueHandlers>
<MissingClassConstType errorLevel="suppress" />
<UnusedClass errorLevel="suppress" />
<UndefinedAttributeClass>
<errorLevel type="suppress">
<referencedClass name="JetBrains\PhpStorm\ExpectedValues" />
<referencedClass name="JetBrains\PhpStorm\Deprecated" />
<referencedClass name="JetBrains\PhpStorm\Pure" />
</errorLevel>
</UndefinedAttributeClass>
<UndefinedClass>
<errorLevel type="suppress">
<referencedClass name="BackedEnum" />
</errorLevel>
</UndefinedClass>
</issueHandlers>
</psalm>
9 changes: 7 additions & 2 deletions src/ORM.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
use Cycle\ORM\Service\Implementation\MapperProvider;
use Cycle\ORM\Service\Implementation\RelationProvider;
use Cycle\ORM\Service\Implementation\RepositoryProvider;
use Cycle\ORM\Service\Implementation\RoleResolver;
use Cycle\ORM\Service\Implementation\SourceProvider;
use Cycle\ORM\Service\Implementation\TypecastProvider;
use Cycle\ORM\Service\IndexProviderInterface;
use Cycle\ORM\Service\MapperProviderInterface;
use Cycle\ORM\Service\RelationProviderInterface;
use Cycle\ORM\Service\RepositoryProviderInterface;
use Cycle\ORM\Service\RoleResolverInterface;
use Cycle\ORM\Service\SourceProviderInterface;
use Cycle\ORM\Service\TypecastProviderInterface;
use Cycle\ORM\Transaction\CommandGenerator;
Expand All @@ -38,11 +40,12 @@ final class ORM implements ORMInterface
private RelationProvider $relationProvider;
private SourceProvider $sourceProvider;
private TypecastProvider $typecastProvider;
private EntityFactory $entityFactory;
private EntityFactoryInterface $entityFactory;
private IndexProvider $indexProvider;
private MapperProvider $mapperProvider;
private RepositoryProvider $repositoryProvider;
private EntityProvider $entityProvider;
private RoleResolverInterface $roleResolver;

public function __construct(
private FactoryInterface $factory,
Expand All @@ -57,7 +60,7 @@ public function __construct(

public function resolveRole(string|object $entity): string
{
return $this->entityFactory->resolveRole($entity);
return $this->roleResolver->resolveRole($entity);
}

public function get(string $role, array $scope, bool $load = true): ?object
Expand Down Expand Up @@ -242,13 +245,15 @@ private function resetRegistry(): void
$this->factory,
);
$this->entityProvider = new EntityProvider($this->heap, $this->repositoryProvider);
$this->roleResolver = new RoleResolver($this->schema, $this->heap);

$this->entityFactory = new EntityFactory(
$this->heap,
$this->schema,
$this->mapperProvider,
$this->relationProvider,
$this->indexProvider,
$this->roleResolver,
);
}
}
7 changes: 2 additions & 5 deletions src/ORMInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Cycle\ORM\Service\MapperProviderInterface;
use Cycle\ORM\Service\RelationProviderInterface;
use Cycle\ORM\Service\RepositoryProviderInterface;
use Cycle\ORM\Service\RoleResolverInterface;
use Cycle\ORM\Service\SourceProviderInterface;
use Cycle\ORM\Transaction\CommandGeneratorInterface;

Expand All @@ -25,13 +26,9 @@ interface ORMInterface extends
MapperProviderInterface,
RepositoryProviderInterface,
RelationProviderInterface,
RoleResolverInterface,
IndexProviderInterface
{
/**
* Automatically resolve role based on object name or instance.
*/
public function resolveRole(string|object $entity): string;

/**
* Create new entity based on given role and input data. Method will attempt to re-use
* already loaded entity.
Expand Down
10 changes: 8 additions & 2 deletions src/Service/EntityFactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@

namespace Cycle\ORM\Service;

use Cycle\ORM\Exception\MapperException;
use Cycle\ORM\Heap\Node;

interface EntityFactoryInterface
{
/**
* Create new entity based on given role and input data.
*
* @param string $role Entity role.
* @param array<string, mixed> $data Entity data.
* @template T
*
* @param non-empty-string|class-string<T> $role Entity role.
* @param array<non-empty-string, mixed> $data Entity data.
* @param bool $typecast Indicates that data is raw, and typecasting should be applied.
*
* @return T
* @throws MapperException
*/
public function make(string $role, array $data = [], int $status = Node::NEW, bool $typecast = false): object;
}
51 changes: 20 additions & 31 deletions src/Service/Implementation/EntityFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@

namespace Cycle\ORM\Service\Implementation;

use Cycle\ORM\EntityProxyInterface;
use Cycle\ORM\Exception\ORMException;
use Cycle\ORM\Heap\HeapInterface;
use Cycle\ORM\Heap\Node;
use Cycle\ORM\Reference\ReferenceInterface;
use Cycle\ORM\Service\EntityFactoryInterface;
use Cycle\ORM\Service\IndexProviderInterface;
use Cycle\ORM\Service\MapperProviderInterface;
use Cycle\ORM\Service\RelationProviderInterface;
use Cycle\ORM\SchemaInterface;
use Cycle\ORM\Select\LoaderInterface;
use Cycle\ORM\Service\RoleResolverInterface;

/**
* @internal
Expand All @@ -26,6 +26,7 @@ public function __construct(
private MapperProviderInterface $mapperProvider,
private RelationProviderInterface $relationProvider,
private IndexProviderInterface $indexProvider,
private RoleResolverInterface $roleResolver,
) {}

public function make(
Expand All @@ -37,7 +38,7 @@ public function make(
$role = $data[LoaderInterface::ROLE_KEY] ?? $role;
unset($data[LoaderInterface::ROLE_KEY]);
// Resolved role
$rRole = $this->resolveRole($role);
$rRole = $this->roleResolver->resolveRole($role);
$relMap = $this->relationProvider->getRelationMap($rRole);
$mapper = $this->mapperProvider->getMapper($rRole);

Expand All @@ -63,10 +64,25 @@ public function make(
$e = $this->heap->find($rRole, $ids);

if ($e !== null) {
// Get not resolved relations (references)
$refs = \array_filter(
$mapper->fetchRelations($e),
fn($v) => $v instanceof ReferenceInterface,
);

if ($refs === []) {
return $e;
}

$node = $this->heap->get($e);
\assert($node !== null);

return $mapper->hydrate($e, $relMap->init($this, $node, $castedData));
// Replace references with actual relation data
return $mapper->hydrate($e, $relMap->init(
$this,
$node,
\array_intersect_key($castedData, $refs),
));
}
}
}
Expand All @@ -79,31 +95,4 @@ public function make(

return $mapper->hydrate($e, $relMap->init($this, $node, $castedData));
}

public function resolveRole(object|string $entity): string
{
if (\is_object($entity)) {
$node = $this->heap->get($entity);
if ($node !== null) {
return $node->getRole();
}

$class = $entity::class;
if (!$this->schema->defines($class)) {
$parentClass = \get_parent_class($entity);

if ($parentClass === false
|| !$entity instanceof EntityProxyInterface
|| !$this->schema->defines($parentClass)
) {
throw new ORMException("Unable to resolve role of `$class`.");
}
$class = $parentClass;
}

$entity = $class;
}

return $this->schema->resolveAlias($entity) ?? throw new ORMException("Unable to resolve role `$entity`.");
}
}
54 changes: 54 additions & 0 deletions src/Service/Implementation/RoleResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Service\Implementation;

use Cycle\ORM\EntityProxyInterface;
use Cycle\ORM\Exception\ORMException;
use Cycle\ORM\Heap\HeapInterface;
use Cycle\ORM\SchemaInterface;
use Cycle\ORM\Service\RoleResolverInterface;

/**
* @internal
*/
final class RoleResolver implements RoleResolverInterface
{
private SchemaInterface $schema;
private HeapInterface $heap;

public function __construct(SchemaInterface $schema, HeapInterface $heap)
{
$this->schema = $schema;
$this->heap = $heap;
}

public function resolveRole(object|string $entity): string
{
if (\is_object($entity)) {
$node = $this->heap->get($entity);
if ($node !== null) {
return $node->getRole();
}

/** @var class-string $class */
$class = $entity::class;
if (!$this->schema->defines($class)) {
$parentClass = \get_parent_class($entity);

if ($parentClass === false
|| !$entity instanceof EntityProxyInterface
|| !$this->schema->defines($parentClass)
) {
throw new ORMException("Unable to resolve role of `$class`.");

Check warning on line 44 in src/Service/Implementation/RoleResolver.php

View check run for this annotation

Codecov / codecov/patch

src/Service/Implementation/RoleResolver.php#L44

Added line #L44 was not covered by tests
}
$class = $parentClass;
}

$entity = $class;
}

return $this->schema->resolveAlias($entity) ?? throw new ORMException("Unable to resolve role `$entity`.");
}
}
15 changes: 15 additions & 0 deletions src/Service/RoleResolverInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Service;

interface RoleResolverInterface
{
/**
* Automatically resolve role based on object name or instance.
*
* @return non-empty-string
*/
public function resolveRole(string|object $entity): string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -259,11 +259,13 @@ public function testRepositoryFindOneWithWhere(): void
public function testLoadOverwriteValues(): void
{
$u = $this->orm->getRepository('user')->findByPK(1);
$this->assertSame('[email protected]', $u->email);
$u->email = '[email protected]';
$this->assertSame('[email protected]', $u->email);

$u2 = $this->orm->getRepository('user')->findByPK(1);
$this->assertSame('[email protected]', $u2->email);
self::assertSame($u, $u2);
$this->assertSame('[email protected]', $u2->email);

$u3 = $this->orm->withHeap(new Heap())->getRepository('user')->findByPK(1);
$this->assertSame('[email protected]', $u3->email);
Expand All @@ -272,10 +274,10 @@ public function testLoadOverwriteValues(): void
$t = new Transaction($this->orm);
$t->persist($u);
$t->run();
$this->assertNumWrites(0);
$this->assertNumWrites(1);

$u4 = $this->orm->withHeap(new Heap())->getRepository('user')->findByPK(1);
$this->assertSame('hello@world.com', $u4->email);
$this->assertSame('test@email.com', $u4->email);
}

public function testNullableValuesInASndOut(): void
Expand Down
10 changes: 4 additions & 6 deletions tests/ORM/Functional/Driver/Common/Mapper/MapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -328,19 +328,17 @@ public function testLoadOverwriteValues(): void
$this->assertSame('[email protected]', $u->email);

$u2 = $this->orm->getRepository(User::class)->findByPK(1);
$this->assertSame('hello@world.com', $u2->email);
$this->assertSame('test@email.com', $u2->email);

$u3 = $this->orm->withHeap(new Heap())->getRepository(User::class)->findByPK(1);
$this->assertSame('[email protected]', $u3->email);

$this->captureWriteQueries();
$t = new Transaction($this->orm);
$t->persist($u);
$t->run();
$this->assertNumWrites(0);
$this->save($u);
$this->assertNumWrites(1);

$u4 = $this->orm->withHeap(new Heap())->getRepository(User::class)->findByPK(1);
$this->assertSame('hello@world.com', $u4->email);
$this->assertSame('test@email.com', $u4->email);
}

public function testNullableValuesInASndOut(): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ public function testDoNotOverwriteRelation(): void

public function testOverwritePromisedRelation(): void
{
// The relation `child_entity` will not be loaded
$u = (new Select($this->orm, CompositePK::class))->wherePK([1, 1])->fetchOne();

$newCompositePKChild = new CompositePKChild();
Expand All @@ -543,12 +544,13 @@ public function testOverwritePromisedRelation(): void
->wherePK([1, 1])->fetchOne();

$this->assertSame($u, $u2);
// Overwritten
$this->assertSame(self::CHILD_1['key3'], $u2->child_entity->key3);
// Relation was not overwritten
$this->assertSame('new', $u2->child_entity->key3);

$this->captureWriteQueries();
(new Transaction($this->orm))->persist($u)->run();
$this->assertNumWrites(0);
$this->save($u);
// Add a new pivot and delete the old one
$this->assertNumWrites(2);
}

public function setUp(): void
Expand Down
Loading
Loading