diff --git a/.phpstan.neon b/.phpstan.neon index bf82db6d..828d8506 100644 --- a/.phpstan.neon +++ b/.phpstan.neon @@ -4,10 +4,5 @@ includes: parameters: ignoreErrors: - # not convenient to mark every place with unnecessary check - - '#Cannot access property \$\w+ on Nextras\\Orm\\Entity\\Reflection\\PropertyRelationshipMetadata\|null\.#' # https://github.com/phpstan/phpstan/issues/587 - '#Constructor of class Nextras\\Orm\\Bridges\\NetteDI\\DIRepositoryFinder has an unused parameter \$modelClass\.#' - # we need a local mute - - '#Cannot call method \w+\(\) on Nextras\\Dbal\\Result\\Result\|null\.#' - - '#Cannot call method \w+\(\) on Nextras\\Dbal\\Result\\Row\|null\.#' diff --git a/.travis.yml b/.travis.yml index a08cce23..1c6af50b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,7 +33,7 @@ before_script: # Install Nette Tester - phpenv config-rm xdebug.ini || true - - if [ "$dependencies" = "lowest" ]; then composer update --prefer-lowest --no-interaction; fi + - if [ "$dependencies" = "lowest" ]; then composer update --prefer-lowest --prefer-stable --no-interaction; fi - if [ "$dependencies" = "highest" ]; then composer update --no-interaction; fi script: diff --git a/composer.json b/composer.json index 8f7079cd..f16fcffb 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "nette/caching": "~2.5 || ~3.0@rc", "nette/utils": "~2.5 || ~3.0@rc", "nette/tokenizer": "~2.3 || ~3.0@rc", - "nextras/dbal": "~3.0" + "nextras/dbal": "~3.0 || ~3.1@dev" }, "require-dev": { "nette/bootstrap": "~2.4 || ~3.0@rc", @@ -33,8 +33,8 @@ "nette/tester": "~2.1", "marc-mabe/php-enum": "~3.0", "mockery/mockery": "~1.2", - "phpstan/phpstan-shim": "0.10.7", - "phpstan/phpstan-nette": "0.10.1", + "phpstan/phpstan-shim": "0.11.1", + "phpstan/phpstan-nette": "0.11", "tracy/tracy": "~2.3" }, "autoload": { diff --git a/src/Entity/AbstractEntity.php b/src/Entity/AbstractEntity.php index bac09692..d7f35df3 100644 --- a/src/Entity/AbstractEntity.php +++ b/src/Entity/AbstractEntity.php @@ -255,8 +255,11 @@ public function onLoad(array $data) } - public function onRefresh(array $data, bool $isPartial = false) + public function onRefresh(?array $data, bool $isPartial = false) { + if ($data === null) { + throw new InvalidStateException('Refetching data failed. Entity is not present in storage anymore.'); + } if ($isPartial) { foreach ($data as $name => $value) { $this->data[$name] = $value; @@ -448,7 +451,7 @@ private function &internalGetValue(PropertyMetadata $metadata, string $name) } else { $value = $this->data[$name]; } - if (!isset($value) && !$metadata->isNullable) { + if ($value === null && !$metadata->isNullable) { $class = get_class($this); throw new InvalidStateException("Property {$class}::\${$name} is not set."); } diff --git a/src/Entity/IEntity.php b/src/Entity/IEntity.php index 04507503..8ef98d52 100644 --- a/src/Entity/IEntity.php +++ b/src/Entity/IEntity.php @@ -125,7 +125,7 @@ public function onLoad(array $data); /** @internal */ - public function onRefresh(array $data, bool $isPartial = false); + public function onRefresh(?array $data, bool $isPartial = false); /** @internal */ diff --git a/src/Mapper/Dbal/DbalMapper.php b/src/Mapper/Dbal/DbalMapper.php index ef585696..ae9f9247 100644 --- a/src/Mapper/Dbal/DbalMapper.php +++ b/src/Mapper/Dbal/DbalMapper.php @@ -319,8 +319,12 @@ protected function processPostgreAutoupdate(IEntity $entity, array $args) $args[] = 'RETURNING %ex'; $args[] = $this->getAutoupdateReselectExpression(); $row = $this->connection->queryArgs($args)->fetch(); - $data = $this->getStorageReflection()->convertStorageToEntity($row->toArray()); - $entity->onRefresh($data, true); + if ($row === null) { + $entity->onRefresh(null, true); + } else { + $data = $this->getStorageReflection()->convertStorageToEntity($row->toArray()); + $entity->onRefresh($data, true); + } } @@ -342,8 +346,12 @@ protected function processMySQLAutoupdate(IEntity $entity, array $args) $this->getTableName(), $primary )->fetch(); - $data = $this->getStorageReflection()->convertStorageToEntity($row->toArray()); - $entity->onRefresh($data, true); + if ($row === null) { + $entity->onRefresh(null, true); + } else { + $data = $this->getStorageReflection()->convertStorageToEntity($row->toArray()); + $entity->onRefresh($data, true); + } } diff --git a/src/Mapper/Dbal/RelationshipMapperManyHasMany.php b/src/Mapper/Dbal/RelationshipMapperManyHasMany.php index 391143bb..8285c641 100644 --- a/src/Mapper/Dbal/RelationshipMapperManyHasMany.php +++ b/src/Mapper/Dbal/RelationshipMapperManyHasMany.php @@ -265,6 +265,7 @@ public function remove(IEntity $parent, array $remove) protected function buildList(IEntity $parent, array $entries): array { + assert($this->metadata->relationship !== null); if (!$this->metadata->relationship->isMain) { throw new LogicException('ManyHasMany relationship has to be persisted in the primary mapper.'); } diff --git a/src/Mapper/Dbal/RelationshipMapperOneHasMany.php b/src/Mapper/Dbal/RelationshipMapperOneHasMany.php index f0a152da..714b88b7 100644 --- a/src/Mapper/Dbal/RelationshipMapperOneHasMany.php +++ b/src/Mapper/Dbal/RelationshipMapperOneHasMany.php @@ -16,6 +16,7 @@ use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Entity\IEntityHasPreloadContainer; use Nextras\Orm\Entity\Reflection\PropertyMetadata; +use Nextras\Orm\Entity\Reflection\PropertyRelationshipMetadata; use Nextras\Orm\Mapper\IRelationshipMapper; @@ -27,6 +28,9 @@ class RelationshipMapperOneHasMany implements IRelationshipMapper /** @var PropertyMetadata */ protected $metadata; + /** @var PropertyRelationshipMetadata */ + protected $metadataRelationship; + /** @var DbalMapper */ protected $targetMapper; @@ -47,6 +51,7 @@ public function __construct(IConnection $connection, DbalMapper $targetMapper, P $this->connection = $connection; $this->targetMapper = $targetMapper; $this->metadata = $metadata; + $this->metadataRelationship = $metadata->relationship; $this->joinStorageKey = $targetMapper->getStorageReflection()->convertEntityToStorageKey($metadata->relationship->property); } @@ -103,7 +108,7 @@ protected function fetchByOnePassStrategy(QueryBuilder $builder, array $values): $result = $this->connection->queryArgs($builder->getQuerySql(), $builder->getQueryParameters()); $entities = []; - $property = $this->metadata->relationship->property; + $property = $this->metadataRelationship->property; assert($property !== null); while (($data = $result->fetch())) { @@ -122,7 +127,7 @@ protected function fetchByTwoPassStrategy(QueryBuilder $builder, array $values): $builder = clone $builder; $targetPrimaryKey = array_map(function ($key) { return $this->targetMapper->getStorageReflection()->convertEntityToStorageKey($key); - }, $this->metadata->relationship->entityMetadata->getPrimaryKey()); + }, $this->metadataRelationship->entityMetadata->getPrimaryKey()); $isComposite = count($targetPrimaryKey) !== 1; foreach (array_unique(array_merge($targetPrimaryKey, [$this->joinStorageKey])) as $key) { @@ -172,7 +177,7 @@ protected function fetchByTwoPassStrategy(QueryBuilder $builder, array $values): foreach ($map as $joiningStorageKey => $primaryValues) { foreach ($primaryValues as $primaryValue) { $entity = $entitiesResult[$primaryValue]; - $entities[$entity->getRawValue($this->metadata->relationship->property)][] = $entity; + $entities[$entity->getRawValue($this->metadataRelationship->property)][] = $entity; } } diff --git a/src/Mapper/Dbal/StorageReflection/StorageReflection.php b/src/Mapper/Dbal/StorageReflection/StorageReflection.php index 9c003e9e..9199f0f6 100644 --- a/src/Mapper/Dbal/StorageReflection/StorageReflection.php +++ b/src/Mapper/Dbal/StorageReflection/StorageReflection.php @@ -229,6 +229,7 @@ public function addModifier(string $storageKey, string $saveModifier): StorageRe protected function findManyHasManyPrimaryColumns($joinTable, $sourceTable, $targetTable): array { + $sourceId = $targetId = null; $useFQN = strpos($sourceTable, '.') !== false; $keys = $this->platform->getForeignKeys($joinTable); foreach ($keys as $column => $meta) { @@ -236,14 +237,14 @@ protected function findManyHasManyPrimaryColumns($joinTable, $sourceTable, $targ ? $meta['ref_table'] : preg_replace('#^(.*\.)?(.*)$#', '$2', $meta['ref_table']); - if ($table === $sourceTable && !isset($sourceId)) { + if ($table === $sourceTable && $sourceId === null) { $sourceId = $column; } elseif ($table === $targetTable) { $targetId = $column; } } - if (!isset($sourceId, $targetId)) { + if ($sourceId === null || $targetId === null) { throw new InvalidStateException("No primary keys detected for many has many '{$joinTable}' join table."); } diff --git a/src/Mapper/Memory/RelationshipMapperManyHasMany.php b/src/Mapper/Memory/RelationshipMapperManyHasMany.php index b2397fb4..a221455a 100644 --- a/src/Mapper/Memory/RelationshipMapperManyHasMany.php +++ b/src/Mapper/Memory/RelationshipMapperManyHasMany.php @@ -27,7 +27,6 @@ class RelationshipMapperManyHasMany implements IRelationshipMapperManyHasMany public function __construct(PropertyMetadata $metadata, ArrayMapper $mapper) { - assert($metadata->relationship !== null); $this->metadata = $metadata; $this->mapper = $mapper; } @@ -43,6 +42,7 @@ public function clearCache() */ public function getIterator(IEntity $parent, ICollection $collection): Iterator { + assert($this->metadata->relationship !== null); if ($this->metadata->relationship->isMain) { $relationshipData = $this->mapper->getRelationshipDataStorage($this->metadata->name); $id = $parent->getValue('id'); diff --git a/src/Mapper/Memory/RelationshipMapperOneHasMany.php b/src/Mapper/Memory/RelationshipMapperOneHasMany.php index 291a073a..1ef97529 100644 --- a/src/Mapper/Memory/RelationshipMapperOneHasMany.php +++ b/src/Mapper/Memory/RelationshipMapperOneHasMany.php @@ -13,6 +13,7 @@ use Nextras\Orm\Collection\ICollection; use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Entity\Reflection\PropertyMetadata; +use Nextras\Orm\Entity\Reflection\PropertyRelationshipMetadata; use Nextras\Orm\Mapper\IRelationshipMapper; @@ -39,11 +40,9 @@ public function clearCache() } - /** - * @return EntityIterator - */ public function getIterator(IEntity $parent, ICollection $collection): Iterator { + assert($this->metadata->relationship !== null); $className = $this->metadata->relationship->entityMetadata->className; $data = $collection->findBy(["$className->{$this->joinStorageKey}->id" => $parent->getValue('id')])->fetchAll(); return new EntityIterator($data); @@ -52,6 +51,8 @@ public function getIterator(IEntity $parent, ICollection $collection): Iterator public function getIteratorCount(IEntity $parent, ICollection $collection): int { - return count($this->getIterator($parent, $collection)); + $iterator = $this->getIterator($parent, $collection); + assert($iterator instanceof \Countable); + return count($iterator); } } diff --git a/src/Relationships/HasMany.php b/src/Relationships/HasMany.php index 662bf0a1..bc7bb321 100644 --- a/src/Relationships/HasMany.php +++ b/src/Relationships/HasMany.php @@ -14,6 +14,7 @@ use Nextras\Orm\Collection\ICollection; use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Entity\Reflection\PropertyMetadata; +use Nextras\Orm\Entity\Reflection\PropertyRelationshipMetadata; use Nextras\Orm\InvalidStateException; use Nextras\Orm\Mapper\IRelationshipMapper; use Nextras\Orm\Repository\IRepository; @@ -30,6 +31,9 @@ abstract class HasMany implements IRelationshipCollection /** @var PropertyMetadata */ protected $metadata; + /** @var PropertyRelationshipMetadata */ + protected $metadataRelationship; + /** @var ICollection|null */ protected $collection; @@ -62,6 +66,7 @@ public function __construct(PropertyMetadata $metadata) { assert($metadata->relationship !== null); $this->metadata = $metadata; + $this->metadataRelationship = $metadata->relationship; } @@ -339,7 +344,7 @@ public function __clone() protected function getTargetRepository(): IRepository { if (!$this->targetRepository) { - $this->targetRepository = $this->parent->getRepository()->getModel()->getRepository($this->metadata->relationship->repository); + $this->targetRepository = $this->parent->getRepository()->getModel()->getRepository($this->metadataRelationship->repository); } return $this->targetRepository; @@ -360,8 +365,8 @@ protected function getRelationshipMapper() protected function applyDefaultOrder(ICollection $collection) { - if ($this->metadata->relationship->order !== null) { - return $collection->orderBy($this->metadata->relationship->order); + if ($this->metadataRelationship->order !== null) { + return $collection->orderBy($this->metadataRelationship->order); } else { return $collection; } diff --git a/src/Relationships/HasOne.php b/src/Relationships/HasOne.php index dc6e59fc..ecee80c0 100644 --- a/src/Relationships/HasOne.php +++ b/src/Relationships/HasOne.php @@ -12,6 +12,7 @@ use Nextras\Orm\Collection\ICollection; use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Entity\Reflection\PropertyMetadata; +use Nextras\Orm\Entity\Reflection\PropertyRelationshipMetadata; use Nextras\Orm\InvalidArgumentException; use Nextras\Orm\Mapper\IRelationshipMapper; use Nextras\Orm\NullValueException; @@ -29,6 +30,9 @@ abstract class HasOne implements IRelationshipContainer /** @var PropertyMetadata */ protected $metadata; + /** @var PropertyRelationshipMetadata */ + protected $metadataRelationship; + /** @var ICollection */ protected $collection; @@ -55,6 +59,7 @@ public function __construct(PropertyMetadata $metadata) { assert($metadata->relationship !== null); $this->metadata = $metadata; + $this->metadataRelationship = $metadata->relationship; } @@ -195,7 +200,7 @@ protected function getPrimaryValue() protected function getTargetRepository(): IRepository { if (!$this->targetRepository) { - $this->targetRepository = $this->parent->getRepository()->getModel()->getRepository($this->metadata->relationship->repository); + $this->targetRepository = $this->parent->getRepository()->getModel()->getRepository($this->metadataRelationship->repository); } return $this->targetRepository; @@ -216,7 +221,7 @@ protected function createEntity($entity, bool $allowNull) { if ($entity instanceof IEntity) { if ($this->parent->isAttached()) { - $repository = $this->parent->getRepository()->getModel()->getRepository($this->metadata->relationship->repository); + $repository = $this->parent->getRepository()->getModel()->getRepository($this->metadataRelationship->repository); $repository->attach($entity); } elseif ($entity->isAttached()) { diff --git a/src/Relationships/ManyHasMany.php b/src/Relationships/ManyHasMany.php index ae515a2b..c35eeecb 100644 --- a/src/Relationships/ManyHasMany.php +++ b/src/Relationships/ManyHasMany.php @@ -44,7 +44,7 @@ public function doPersist() $this->isModified = false; $this->collection = null; - if ($this->metadata->relationship->isMain) { + if ($this->metadataRelationship->isMain) { $this->getRelationshipMapper()->clearCache(); $this->getRelationshipMapper()->remove($this->parent, $toRemove); $this->getRelationshipMapper()->add($this->parent, $toAdd); @@ -60,7 +60,7 @@ protected function modify(): void protected function createCollection(): ICollection { - if ($this->metadata->relationship->isMain) { + if ($this->metadataRelationship->isMain) { $mapperOne = $this->parent->getRepository()->getMapper(); $mapperTwo = $this->getTargetRepository()->getMapper(); } else { @@ -71,11 +71,11 @@ protected function createCollection(): ICollection $collection = $mapperOne->createCollectionManyHasMany($mapperTwo, $this->metadata); $collection = $collection->setRelationshipParent($this->parent); $collection->subscribeOnEntityFetch(function (Traversable $entities) { - if (!$this->metadata->relationship->property) { + if (!$this->metadataRelationship->property) { return; } foreach ($entities as $entity) { - $entity->getProperty($this->metadata->relationship->property)->trackEntity($this->parent); + $entity->getProperty($this->metadataRelationship->property)->trackEntity($this->parent); $this->trackEntity($entity); } }); @@ -85,11 +85,11 @@ protected function createCollection(): ICollection protected function updateRelationshipAdd(IEntity $entity): void { - if (!$this->metadata->relationship->property) { + if (!$this->metadataRelationship->property) { return; } - $otherSide = $entity->getProperty($this->metadata->relationship->property); + $otherSide = $entity->getProperty($this->metadataRelationship->property); assert($otherSide instanceof ManyHasMany); $otherSide->collection = null; $otherSide->toAdd[spl_object_hash($this->parent)] = $this->parent; @@ -99,11 +99,11 @@ protected function updateRelationshipAdd(IEntity $entity): void protected function updateRelationshipRemove(IEntity $entity): void { - if (!$this->metadata->relationship->property) { + if (!$this->metadataRelationship->property) { return; } - $otherSide = $entity->getProperty($this->metadata->relationship->property); + $otherSide = $entity->getProperty($this->metadataRelationship->property); assert($otherSide instanceof ManyHasMany); $otherSide->collection = null; $otherSide->toRemove[spl_object_hash($this->parent)] = $this->parent; diff --git a/src/Relationships/ManyHasOne.php b/src/Relationships/ManyHasOne.php index 6770fba4..f873732d 100644 --- a/src/Relationships/ManyHasOne.php +++ b/src/Relationships/ManyHasOne.php @@ -30,7 +30,7 @@ protected function modify(): void protected function updateRelationship(?IEntity $oldEntity, ?IEntity $newEntity, bool $allowNull): void { - $key = $this->metadata->relationship->property; + $key = $this->metadataRelationship->property; if (!$key) { return; } @@ -49,7 +49,7 @@ protected function updateRelationship(?IEntity $oldEntity, ?IEntity $newEntity, protected function initReverseRelationship(?IEntity $entity) { - $key = $this->metadata->relationship->property; + $key = $this->metadataRelationship->property; if (!$key || !$entity) { return; } diff --git a/src/Relationships/OneHasMany.php b/src/Relationships/OneHasMany.php index b8d161cf..5cd4cd5d 100644 --- a/src/Relationships/OneHasMany.php +++ b/src/Relationships/OneHasMany.php @@ -66,24 +66,24 @@ protected function createCollection(): ICollection protected function updateRelationshipAdd(IEntity $entity): void { - if (!$this->metadata->relationship->property) { + if (!$this->metadataRelationship->property) { return; } $this->updatingReverseRelationship = true; - $entity->getProperty($this->metadata->relationship->property)->setInjectedValue($entity, $this->parent); + $entity->getProperty($this->metadataRelationship->property)->setInjectedValue($entity, $this->parent); $this->updatingReverseRelationship = false; } protected function updateRelationshipRemove(IEntity $entity): void { - if (!$this->metadata->relationship->property) { + if (!$this->metadataRelationship->property) { return; } $this->updatingReverseRelationship = true; - $entity->getProperty($this->metadata->relationship->property)->setInjectedValue($entity, null); + $entity->getProperty($this->metadataRelationship->property)->setInjectedValue($entity, null); $this->updatingReverseRelationship = false; } } diff --git a/src/Relationships/OneHasOne.php b/src/Relationships/OneHasOne.php index 25a3985f..5c20dfd3 100644 --- a/src/Relationships/OneHasOne.php +++ b/src/Relationships/OneHasOne.php @@ -23,7 +23,7 @@ protected function createCollection(): ICollection public function getRawValue() { - if ($this->primaryValue === null && $this->value === false && !$this->metadata->relationship->isMain) { + if ($this->primaryValue === null && $this->value === false && !$this->metadataRelationship->isMain) { $this->getEntity(); // init the value } return parent::getRawValue(); @@ -33,7 +33,7 @@ public function getRawValue() protected function modify(): void { $this->isModified = true; - if ($this->metadata->relationship->isMain) { + if ($this->metadataRelationship->isMain) { $this->parent->setAsModified($this->metadata->name); } } @@ -41,7 +41,7 @@ protected function modify(): void protected function updateRelationship(?IEntity $oldEntity, ?IEntity $newEntity, bool $allowNull): void { - $key = $this->metadata->relationship->property; + $key = $this->metadataRelationship->property; if (!$key) { return; } @@ -59,7 +59,7 @@ protected function updateRelationship(?IEntity $oldEntity, ?IEntity $newEntity, protected function initReverseRelationship(?IEntity $entity) { - $key = $this->metadata->relationship->property; + $key = $this->metadataRelationship->property; if (!$key || !$entity) { return; } diff --git a/src/Repository/IdentityMap.php b/src/Repository/IdentityMap.php index cc015e98..391b2b8f 100644 --- a/src/Repository/IdentityMap.php +++ b/src/Repository/IdentityMap.php @@ -20,7 +20,7 @@ class IdentityMap /** @var IRepository */ private $repository; - /** @var array of IEntity|bool */ + /** @var array */ private $entities = []; /** @var array */ diff --git a/src/TestHelper/EntityCreator.php b/src/TestHelper/EntityCreator.php index ee9319b9..43ff8624 100644 --- a/src/TestHelper/EntityCreator.php +++ b/src/TestHelper/EntityCreator.php @@ -98,7 +98,7 @@ protected function random(PropertyMetadata $property) } } - if (!$possibilities) { + if (count($possibilities) === 0) { return null; } diff --git a/tests/cases/integration/Mapper/DbalPersistAutoupdateMapperTest.phpt b/tests/cases/integration/Mapper/DbalPersistAutoupdateMapperTest.phpt index c8cf5f86..9a15ac4c 100644 --- a/tests/cases/integration/Mapper/DbalPersistAutoupdateMapperTest.phpt +++ b/tests/cases/integration/Mapper/DbalPersistAutoupdateMapperTest.phpt @@ -9,6 +9,9 @@ namespace NextrasTests\Orm\Integration\Mapper; use DateTime; use DateTimeImmutable; +use Nextras\Dbal\Connection; +use Nextras\Dbal\IConnection; +use Nextras\Orm\InvalidStateException; use NextrasTests\Orm\BookCollection; use NextrasTests\Orm\DataTestCase; use NextrasTests\Orm\Helper; @@ -48,6 +51,15 @@ class DbalPersistAutoupdateMapperTest extends DataTestCase Assert::type(DateTimeImmutable::class, $bookCollection->updatedAt); $new = $bookCollection->updatedAt; Assert::notEqual($old->format(DateTime::ISO8601), $new->format(DateTime::ISO8601)); + + /** @var IConnection $connection */ + $connection = $this->container->getByType(Connection::class); + $connection->query('DELETE FROM book_collections WHERE id = %i', $bookCollection->id); + + $bookCollection->name .= '2'; + Assert::exception(function () use ($bookCollection) { + $this->orm->bookColletions->persistAndFlush($bookCollection); + }, InvalidStateException::class, 'Refetching data failed. Entity is not present in storage anymore.'); } }