diff --git a/src/Entity/Groups/AccommodationPrice.php b/src/Entity/Groups/AccommodationPrice.php index 0a6a676..751d07c 100644 --- a/src/Entity/Groups/AccommodationPrice.php +++ b/src/Entity/Groups/AccommodationPrice.php @@ -6,6 +6,7 @@ use App\Entity\BlameableEntity; use App\Entity\BlameableEntityInterface; use App\Entity\TimestampableEntity; use App\Entity\TimestampableEntityInterface; +use App\Entity\TimestampableParentAwareInterface; use App\Enum\Groups\PriceType; use App\Enum\Groups\Season; use App\Repository\Groups\AccommodationPriceRepository; @@ -14,7 +15,7 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: AccommodationPriceRepository::class)] -class AccommodationPrice implements BlameableEntityInterface, TimestampableEntityInterface +class AccommodationPrice implements BlameableEntityInterface, TimestampableEntityInterface, TimestampableParentAwareInterface { use BlameableEntity; use TimestampableEntity; @@ -89,6 +90,11 @@ class AccommodationPrice implements BlameableEntityInterface, TimestampableEntit return $this; } + public function getTimestampableParent(): ?TimestampableEntityInterface + { + return $this->accommodation; + } + public function getDateFrom(): ?\DateTimeImmutable { return $this->dateFrom; diff --git a/src/Entity/Groups/AdditionalService.php b/src/Entity/Groups/AdditionalService.php index 7cdba23..7f6dc34 100644 --- a/src/Entity/Groups/AdditionalService.php +++ b/src/Entity/Groups/AdditionalService.php @@ -6,6 +6,7 @@ use App\Entity\BlameableEntity; use App\Entity\BlameableEntityInterface; use App\Entity\TimestampableEntity; use App\Entity\TimestampableEntityInterface; +use App\Entity\TimestampableParentAwareInterface; use App\Enum\Groups\AdditionalServiceType; use App\Repository\Groups\AdditionalServiceRepository; use Doctrine\DBAL\Types\Types; @@ -13,7 +14,7 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: AdditionalServiceRepository::class)] -class AdditionalService implements BlameableEntityInterface, TimestampableEntityInterface +class AdditionalService implements BlameableEntityInterface, TimestampableEntityInterface, TimestampableParentAwareInterface { use BlameableEntity; use TimestampableEntity; @@ -87,6 +88,11 @@ class AdditionalService implements BlameableEntityInterface, TimestampableEntity return $this; } + public function getTimestampableParent(): ?TimestampableEntityInterface + { + return $this->accommodation; + } + public function getLabel(): ?string { return $this->label; diff --git a/src/Entity/Groups/BoardService.php b/src/Entity/Groups/BoardService.php index bbcc5eb..20b9839 100644 --- a/src/Entity/Groups/BoardService.php +++ b/src/Entity/Groups/BoardService.php @@ -6,13 +6,14 @@ use App\Entity\BlameableEntity; use App\Entity\BlameableEntityInterface; use App\Entity\TimestampableEntity; use App\Entity\TimestampableEntityInterface; +use App\Entity\TimestampableParentAwareInterface; use App\Repository\Groups\BoardServiceRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: BoardServiceRepository::class)] -class BoardService implements BlameableEntityInterface, TimestampableEntityInterface +class BoardService implements BlameableEntityInterface, TimestampableEntityInterface, TimestampableParentAwareInterface { use BlameableEntity; use TimestampableEntity; @@ -67,6 +68,11 @@ class BoardService implements BlameableEntityInterface, TimestampableEntityInter return $this; } + public function getTimestampableParent(): ?TimestampableEntityInterface + { + return $this->accommodation; + } + public function getLabel(): ?string { return $this->label; diff --git a/src/Entity/TimestampableParentAwareInterface.php b/src/Entity/TimestampableParentAwareInterface.php new file mode 100644 index 0000000..4802998 --- /dev/null +++ b/src/Entity/TimestampableParentAwareInterface.php @@ -0,0 +1,11 @@ +setUpdatedAt($now); } } + + /** + * Doctrine has no notion of "collection member changed, so the owner changed". Entities that + * only make sense as part of their parent therefore push their own changes upwards, so the + * parent's updatedAt reflects the last change to the aggregate as a whole. + */ + public function onFlush(OnFlushEventArgs $args): void + { + $entityManager = $args->getObjectManager(); + $unitOfWork = $entityManager->getUnitOfWork(); + + $scheduled = [ + ...$unitOfWork->getScheduledEntityInsertions(), + ...$unitOfWork->getScheduledEntityUpdates(), + ...$unitOfWork->getScheduledEntityDeletions(), + ]; + + /** @var array $parents */ + $parents = []; + + foreach ($scheduled as $entity) { + if (!$entity instanceof TimestampableParentAwareInterface) { + continue; + } + + $parent = $entity->getTimestampableParent(); + + // A parent created in this very flush already gets its createdAt, and one being + // removed must not be resurrected by an update. + if (null === $parent + || $unitOfWork->isScheduledForInsert($parent) + || $unitOfWork->isScheduledForDelete($parent)) { + continue; + } + + $parents[spl_object_id($parent)] = $parent; + } + + $now = new \DateTimeImmutable(); + + foreach ($parents as $parent) { + $parent->setUpdatedAt($now); + + // Registers the parent as an update even when it had no changeset of its own. The + // preUpdate listeners run afterwards, so updatedBy stays in sync with updatedAt. + $unitOfWork->recomputeSingleEntityChangeSet( + $entityManager->getClassMetadata($parent::class), + $parent, + ); + } + } }