Skip to content

Commit

Permalink
relationships: make HasOne relationship lazy until entity is attached
Browse files Browse the repository at this point in the history
  • Loading branch information
hrach committed Oct 19, 2020
1 parent 734cd96 commit c8f90e3
Show file tree
Hide file tree
Showing 12 changed files with 143 additions and 107 deletions.
4 changes: 2 additions & 2 deletions doc/entity.texy
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ echo $member->hasValue('web') ? 'has web' : '-';
$member->isPersisted(); // false
\--

Attaching entities to the repository is letting Orm know about your entities, it does not store the entity. Attaching to repository injects the required dependencies into your entity (through inject property annotations or inject methods). If you need some dependency before attaching entity to the repository, feel free to pass the dependency through the constructor, which is by default empty.
Attaching entities to the repository lets Orm know about your entities. Attaching to a repository runs the required dependencies injection into your entity (through inject property annotations or inject methods). If you need some dependency before attaching entity to the repository, feel free to pass the dependency via the constructor, which is by default empty.

Each entity can be created "manually". Entities can be simply connected together. Let's see an example:

Expand All @@ -54,7 +54,7 @@ $book->author = $author;
$book->tags->set([new Tag(), new Tag()]);
\--

If a instance Author is attached to the repository, all other new connected entities are automatically attached to their repositories too. See more in [relationships chapter | relationships].
If an Author instance is attached to the repository, all other new connected entities are automatically attached to their repositories too. See more in [relationships chapter | relationships].

-----------

Expand Down
25 changes: 11 additions & 14 deletions src/Entity/AbstractEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,18 @@ public function onFree(): void

public function onAttach(IRepository $repository, EntityMetadata $metadata): void
{
$this->attach($repository);
if ($this->isAttached()) {
return;
}

$this->repository = $repository;
$this->metadata = $metadata;

foreach ($this->data as $property) {
if ($property instanceof IEntityAwareProperty) {
$property->setPropertyEntity($this);
}
}
}


Expand Down Expand Up @@ -504,17 +514,4 @@ private function createPropertyWrapper(PropertyMetadata $metadata): IProperty

return $wrapper;
}


/**
* @param IRepository<IEntity> $repository
*/
private function attach(IRepository $repository): void
{
if ($this->repository !== null && $this->repository !== $repository) {
throw new InvalidStateException('Entity is already attached.');
}

$this->repository = $repository;
}
}
4 changes: 4 additions & 0 deletions src/Entity/IEntityAwareProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@

interface IEntityAwareProperty extends IProperty
{
/**
* Sets entity to property. This methods is called once the relationship is connected to entity.
* May be called second time when the entity gets attached to repository, if it was not attached previously.
*/
public function setPropertyEntity(IEntity $entity): void;
}
160 changes: 106 additions & 54 deletions src/Relationships/HasOne.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Nextras\Orm\Entity\Reflection\PropertyMetadata;
use Nextras\Orm\Entity\Reflection\PropertyRelationshipMetadata;
use Nextras\Orm\Exception\InvalidArgumentException;
use Nextras\Orm\Exception\InvalidStateException;
use Nextras\Orm\Exception\NullValueException;
use Nextras\Orm\Mapper\IRelationshipMapper;
use Nextras\Orm\Repository\IRepository;
Expand All @@ -35,11 +36,14 @@ abstract class HasOne implements IRelationshipContainer
*/
protected $collection;

/** @var mixed|null */
protected $primaryValue;
/** @var bool */
protected $isValueValidated = true;

/** @var bool */
protected $isValueFromStorage = false;

/** @var IEntity|null|false */
protected $value = false;
/** @var IEntity|string|int|null */
protected $value;

/**
* @var IRepository|null
Expand Down Expand Up @@ -72,6 +76,13 @@ public function __construct(PropertyMetadata $metadata)
public function setPropertyEntity(IEntity $parent): void
{
$this->parent = $parent;

if (!$this->isValueValidated) {
$this->getEntity();
if ($this->value instanceof IEntity) {
$this->attachIfPossible($this->value);
}
}
}


Expand All @@ -86,7 +97,10 @@ public function convertToRawValue($value)

public function setRawValue($value): void
{
$this->primaryValue = $value;
$isChanged = $this->getPrimaryValue() !== $value;
$this->value = $value;
$this->isValueValidated = !$isChanged && $value === null;
$this->isValueFromStorage = true;
}


Expand All @@ -98,6 +112,7 @@ public function getRawValue()

public function setInjectedValue($value): bool
{
$this->isValueFromStorage = false;
return $this->set($value);
}

Expand All @@ -111,61 +126,64 @@ public function &getInjectedValue()

public function hasInjectedValue(): bool
{
return $this->value instanceof IEntity || $this->getPrimaryValue() !== null;
return $this->value !== null;
}


public function isLoaded(): bool
{
return $this->value !== false;
return !$this->isValueFromStorage || $this->isValueValidated;
}


/**
* Sets the relationship value to passed entity.
* Returns true if the setter has modified property value.
* @param IEntity|null|int|string $value Accepts also a primary key, if any of the entities is attached to repository.
* @param IEntity|int|string|null $value Accepts also a primary key value.
*/
public function set($value, bool $allowNull = false): bool
{
if ($this->updatingReverseRelationship) {
return false;
}

$value = $this->createEntity($value, $allowNull);
if (($this->parent !== null && $this->parent->isAttached()) || $value === null) {
$entity = $this->createEntity($value, $allowNull);
$isValueValidated = true;
} else {
$entity = $value;
$isValueValidated = false;
}

$isChanged = $this->isChanged($value);
if ($isChanged) {
$this->modify();
$oldValue = $this->value;
if ($oldValue === false) {
$primaryValue = $this->getPrimaryValue();
$oldValue = $primaryValue !== null ? $this->getTargetRepository()->getById($primaryValue) : null;
if ($entity instanceof IEntity || $entity === null) {
$isChanged = $this->isChanged($entity);
if ($isChanged) {
$this->modify();
$this->updateRelationship($this->getValue(false), $entity, $allowNull);
} else {
$this->initReverseRelationship($entity);
}
$this->updateRelationship($oldValue, $value, $allowNull);

} else {
$this->initReverseRelationship($value);
$this->modify();
$isChanged = true;
}

$this->primaryValue = $value !== null && $value->isPersisted() ? $value->getValue('id') : null;
$this->value = $value;
$this->value = $entity;
$this->isValueValidated = $isValueValidated;
$this->isValueFromStorage = false;
return $isChanged;
}


public function getEntity(): ?IEntity
{
if ($this->value === false) {
$this->set($this->fetchValue());
}
$value = $this->getValue();

if ($this->value === null && !$this->metadata->isNullable) {
if ($value === null && !$this->metadata->isNullable) {
throw new NullValueException($this->parent, $this->metadata);
}

assert($this->value === null || $this->value instanceof IEntity);
return $this->value;
return $value;
}


Expand All @@ -175,27 +193,55 @@ public function isModified(): bool
}


protected function fetchValue(): ?IEntity
/**
* @return mixed|null
*/
protected function getPrimaryValue()
{
if (!$this->parent->isAttached()) {
return null;
if ($this->value instanceof IEntity) {
if ($this->value->hasValue('id')) {
return $this->value->getValue('id');
} else {
return null;
}
} else {
$collection = $this->getCollection();
return iterator_to_array($collection->getIterator())[0] ?? null;
return $this->value;
}
}


/**
* @return mixed|null
*/
protected function getPrimaryValue()
protected function getValue(bool $allowPreloadContainer = true): ?IEntity
{
if ($this->primaryValue === null && $this->value instanceof IEntity && $this->value->hasValue('id')) {
$this->primaryValue = $this->value->getValue('id');
if (!$this->isValueValidated && $this->value !== null) {
$this->initValue($allowPreloadContainer);
}

return $this->primaryValue;
assert($this->value instanceof IEntity || $this->value === null);
return $this->value;
}


protected function initValue(bool $allowPreloadContainer = true): void
{
if ($this->parent === null) {
throw new InvalidStateException('Relationship is not attached to a parent entity.');
}

if ($this->isValueFromStorage && $allowPreloadContainer) {
// load the value using relationship mapper to utilize preload container and not to validate if
// relationship's entity is really present in the database;
$this->set($this->fetchValue());

} else {
$this->set($this->value);
}
}


protected function fetchValue(): ?IEntity
{
$collection = $this->getCollection();
return iterator_to_array($collection->getIterator())[0] ?? null;
}


Expand Down Expand Up @@ -232,15 +278,7 @@ protected function getCollection(): ICollection
protected function createEntity($entity, bool $allowNull): ?IEntity
{
if ($entity instanceof IEntity) {
if ($this->parent->isAttached()) {
$repository = $this->parent->getRepository()->getModel()
->getRepository($this->metadataRelationship->repository);
$repository->attach($entity);

} elseif ($entity->isAttached()) {
$repository = $entity->getRepository()->getModel()->getRepositoryForEntity($this->parent);
$repository->attach($this->parent);
}
$this->attachIfPossible($entity);
return $entity;

} elseif ($entity === null) {
Expand All @@ -250,18 +288,32 @@ protected function createEntity($entity, bool $allowNull): ?IEntity
return null;

} elseif (is_scalar($entity)) {
return $this->getTargetRepository()->getById($entity);
return $this->getTargetRepository()->getByIdChecked($entity);

} else {
throw new InvalidArgumentException('Value is not a valid entity representation.');
}
}


/**
* @param IEntity|null $newValue
*/
protected function isChanged($newValue): bool
protected function attachIfPossible(IEntity $entity): void
{
if ($this->parent === null) return;

if ($this->parent->isAttached() && !$entity->isAttached()) {
$model = $this->parent->getRepository()->getModel();
$repository = $model->getRepository($this->metadataRelationship->repository);
$repository->attach($entity);

} elseif ($entity->isAttached() && !$this->parent->isAttached()) {
$model = $entity->getRepository()->getModel();
$repository = $model->getRepositoryForEntity($this->parent);
$repository->attach($this->parent);
}
}


protected function isChanged(?IEntity $newValue): bool
{
if ($this->value instanceof IEntity && $newValue instanceof IEntity) {
return $this->value !== $newValue;
Expand All @@ -272,7 +324,7 @@ protected function isChanged($newValue): bool
return true;

} elseif ($newValue instanceof IEntity && $newValue->isPersisted()) {
// value is persited entity or null
// value is persisted entity or null
// newValue is persisted entity
return $this->getPrimaryValue() !== $newValue->getValue('id');

Expand Down
17 changes: 13 additions & 4 deletions src/Relationships/OneHasOne.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,28 @@ protected function createCollection(): ICollection
}


public function setRawValue($value): void
{
parent::setRawValue($value);
if (!$this->metadataRelationship->isMain) {
$this->isValueValidated = false;
}
}


public function getRawValue()
{
if ($this->primaryValue === null && $this->value === false && !$this->metadataRelationship->isMain) {
$this->getEntity(); // init the value
if (!$this->isValueValidated && !$this->metadataRelationship->isMain) {
$this->initValue();
}
return parent::getRawValue();
}


public function hasInjectedValue(): bool
{
if ($this->primaryValue === null && $this->value === false && !$this->metadataRelationship->isMain) {
return $this->fetchValue() !== null;
if (!$this->isValueValidated && !$this->metadataRelationship->isMain) {
$this->initValue();
}
return parent::hasInjectedValue();
}
Expand Down
4 changes: 0 additions & 4 deletions tests/cases/integration/Collection/collection.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,6 @@ class CollectionTest extends DataTestCase
public function testConditionsInDifferentJoinsAndSameTable(): void
{
$book = new Book();
$this->orm->books->attach($book);

$book->title = 'Books 5';
$book->author = 1;
$book->translator = 2;
Expand All @@ -216,8 +214,6 @@ class CollectionTest extends DataTestCase
$this->orm->persistAndFlush($book3);

$book5 = new Book();
$this->orm->books->attach($book5);

$book5->title = 'Book 5';
$book5->author = 1;
$book5->publisher = 1;
Expand Down
Loading

0 comments on commit c8f90e3

Please sign in to comment.