615 lines
18 KiB
PHP

<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Entity\Household;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Validator\Constraints\Household\MaxHolder;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\ReadableCollection;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* @MaxHolder(groups={"household_memberships"})
*/
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['household' => Household::class])]
#[ORM\Entity]
#[ORM\Table(name: 'chill_person_household')]
class Household
{
/**
* Addresses.
*
* @var Collection<Address>
*/
#[Serializer\Groups(['write'])]
#[ORM\ManyToMany(targetEntity: Address::class, cascade: ['persist', 'remove', 'merge', 'detach'])]
#[ORM\JoinTable(name: 'chill_person_household_to_addresses')]
#[ORM\OrderBy(['validFrom' => Criteria::DESC, 'id' => 'DESC'])]
private Collection $addresses;
#[ORM\Embedded(class: CommentEmbeddable::class, columnPrefix: 'comment_members_')]
private CommentEmbeddable $commentMembers;
/**
* @var Collection&Selectable<int, HouseholdComposition>
*/
#[Assert\Valid(traverse: true, groups: ['household_composition'])]
#[ORM\OneToMany(targetEntity: HouseholdComposition::class, mappedBy: 'household', orphanRemoval: true, cascade: ['persist'])]
#[ORM\OrderBy(['startDate' => Criteria::DESC])]
private Collection&Selectable $compositions;
#[Serializer\Groups(['read', 'docgen:read'])]
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
private ?int $id = null;
/**
* @var Collection<HouseholdMember>
*/
#[Serializer\Groups(['read', 'docgen:read'])]
#[ORM\OneToMany(targetEntity: HouseholdMember::class, mappedBy: 'household')]
private Collection $members;
#[Serializer\Groups(['docgen:read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, name: 'waiting_for_birth', options: ['default' => false])]
private bool $waitingForBirth = false;
#[Serializer\Groups(['docgen:read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATE_IMMUTABLE, name: 'waiting_for_birth_date', nullable: true, options: ['default' => null])]
private ?\DateTimeImmutable $waitingForBirthDate = null;
public function __construct()
{
$this->addresses = new ArrayCollection();
$this->members = new ArrayCollection();
$this->commentMembers = new CommentEmbeddable();
$this->compositions = new ArrayCollection();
}
public function addAddress(Address $address): self
{
if (!$this->addresses->contains($address)) {
$this->addresses[] = $address;
$this->makeAddressConsistent();
}
return $this;
}
public function addComposition(HouseholdComposition $composition): self
{
if (!$this->compositions->contains($composition)) {
$composition->setHousehold($this);
$this->compositions[] = $composition;
}
$this->householdCompositionConsistency();
return $this;
}
public function addMember(HouseholdMember $member): self
{
if (!$this->members->contains($member)) {
$this->members[] = $member;
$member->setHousehold($this);
}
return $this;
}
/**
* By default, the addresses are ordered by date, descending (the most
* recent first).
*
* @return Collection<Address>
*/
public function getAddresses(): Collection
{
return $this->addresses;
}
/**
* @return array|Address[]
*/
public function getAddressesOrdered(): array
{
$addresses = $this->getAddresses()->toArray();
usort($addresses, static function (Address $a, Address $b) {
$validFromA = $a->getValidFrom()->format('Y-m-d');
$validFromB = $b->getValidFrom()->format('Y-m-d');
if ($a === $b) {
if (null === $a->getId()) {
return 1;
}
if (null === $b->getId()) {
return -1;
}
return $a->getId() <=> $b->getId();
}
return $validFromA <=> $validFromB;
});
return $addresses;
}
public function getCommentMembers(): CommentEmbeddable
{
return $this->commentMembers;
}
/**
* @return ArrayCollection|Collection|HouseholdComposition[]
*/
public function getCompositions(): Collection
{
return $this->compositions;
}
#[Serializer\Groups(['read', 'docgen:read'])]
#[Serializer\SerializedName('current_address')]
public function getCurrentAddress(?\DateTime $at = null): ?Address
{
$at ??= new \DateTime('today');
$addrs = $this->getAddresses()->filter(static fn (Address $a) => $a->getValidFrom() <= $at && (
null === $a->getValidTo() || $a->getValidTo() > $at
));
if ($addrs->count() > 0) {
return $addrs->first();
}
return null;
}
#[Serializer\Groups(['docgen:read'])]
#[Serializer\SerializedName('current_composition')]
public function getCurrentComposition(?\DateTimeImmutable $at = null): ?HouseholdComposition
{
$at ??= new \DateTimeImmutable('today');
$criteria = new Criteria();
$expr = Criteria::expr();
$criteria->where(
$expr->andX(
$expr->orX(
$expr->isNull('endDate'),
$expr->gt('endDate', $at)
),
$expr->lte('startDate', $at)
)
);
$compositions = $this->compositions->matching($criteria);
if ($compositions->count() > 0) {
return $compositions->first();
}
return null;
}
#[Serializer\Groups(['docgen:read'])]
public function getCurrentMembers(?\DateTimeImmutable $now = null): Collection
{
return $this->getMembers()->matching($this->buildCriteriaCurrentMembers($now));
}
public function getCurrentMembersByPosition(Position $position, ?\DateTimeInterface $now = null)
{
$criteria = new Criteria();
$expr = Criteria::expr();
$criteria->where($expr->eq('position', $position));
return $this->getCurrentMembers($now)->matching($criteria);
}
/**
* get current members ids.
*
* Used in serialization
*/
#[Serializer\Groups(['read'])]
#[Serializer\SerializedName('current_members_id')]
public function getCurrentMembersIds(?\DateTimeImmutable $now = null): ReadableCollection
{
return $this->getCurrentMembers($now)->map(
static fn (HouseholdMember $m) => $m->getId()
);
}
/**
* @return HouseholdMember[]
*/
public function getCurrentMembersOrdered(?\DateTimeImmutable $now = null): Collection
{
$members = $this->getCurrentMembers($now);
$members->getIterator()
->uasort(
static function (HouseholdMember $a, HouseholdMember $b) {
if (null === $a->getPosition()) {
if (null === $b->getPosition()) {
return 0;
}
return -1;
}
if (null === $b->getPosition()) {
return 1;
}
if ($a->getPosition()->getOrdering() < $b->getPosition()->getOrdering()) {
return -1;
}
if ($a->getPosition()->getOrdering() > $b->getPosition()->getOrdering()) {
return 1;
}
if ($a->isHolder() && !$b->isHolder()) {
return 1;
}
if (!$a->isHolder() && $b->isHolder()) {
return -1;
}
return 0;
}
);
return $members;
}
public function getCurrentMembersWithoutPosition(?\DateTimeInterface $now = null)
{
$criteria = new Criteria();
$expr = Criteria::expr();
$criteria->where($expr->isNull('position'));
return $this->getCurrentMembers($now)->matching($criteria);
}
/**
* Get the persons currently associated to the household.
*
* Return a list of Person, instead of a list of HouseholdMembers
*
* @return ReadableCollection<(int|string), Person>
*/
public function getCurrentPersons(?\DateTimeImmutable $now = null): ReadableCollection
{
return $this->getCurrentMembers($now)
->map(static fn (HouseholdMember $m) => $m->getPerson());
}
public function getId(): ?int
{
return $this->id;
}
/**
* @return Collection|HouseholdMember[]
*/
public function getMembers(): Collection
{
return $this->members;
}
/**
* get all the members during a given membership.
*
* @return ReadableCollection<(int|string), HouseholdMember>
*/
public function getMembersDuringMembership(HouseholdMember $membership): ReadableCollection
{
return $this->getMembersOnRange(
$membership->getStartDate(),
$membership->getEndDate()
)->filter(
static fn (HouseholdMember $m) => $m->getPerson() !== $membership->getPerson()
);
}
public function getMembersHolder(): Collection
{
$criteria = new Criteria();
$expr = Criteria::expr();
$criteria->where(
$expr->eq('holder', true)
);
return $this->getMembers()->matching($criteria);
}
public function getMembersOnRange(\DateTimeImmutable $from, ?\DateTimeImmutable $to): ReadableCollection
{
return $this->getMembers()->filter(static function (HouseholdMember $m) use ($from, $to) {
if (null === $m->getEndDate() && null !== $to) {
return $m->getStartDate() <= $to;
}
if (null === $to) {
return $m->getStartDate() >= $from || null === $m->getEndDate();
}
if (null !== $m->getEndDate() && $m->getEndDate() < $from) {
return false;
}
if ($m->getStartDate() <= $to) {
return true;
}
return false;
});
}
public function getNonCurrentMembers(?\DateTimeImmutable $now = null): Collection
{
$criteria = new Criteria();
$expr = Criteria::expr();
$date = $now ?? new \DateTimeImmutable('today');
$criteria
->where(
$expr->gt('startDate', $date)
)
->orWhere(
$expr->andX(
$expr->lte('endDate', $date),
$expr->neq('endDate', null)
)
);
return $this->getMembers()->matching($criteria);
}
public function getNonCurrentMembersByPosition(Position $position, ?\DateTimeInterface $now = null)
{
$criteria = new Criteria();
$expr = Criteria::expr();
$criteria->where($expr->eq('position', $position));
return $this->getNonCurrentMembers($now)->matching($criteria);
}
public function getNonCurrentMembersWithoutPosition(?\DateTimeInterface $now = null)
{
$criteria = new Criteria();
$expr = Criteria::expr();
$criteria->where($expr->isNull('position'));
return $this->getNonCurrentMembers($now)->matching($criteria);
}
/**
* get all the unique persons during a given membership.
*
* same as @see(self::getMembersDuringMembership}, except that the collection is filtered to
* return unique members.
*
* @return Collection|Person[]
*/
public function getPersonsDuringMembership(HouseholdMember $member): Collection
{
// make list unique
$membersByHash = [];
foreach ($this->getMembersDuringMembership($member) as $m) {
if (null !== $m && null !== $m->getPerson()) {
$membersByHash[spl_object_hash($m->getPerson())] = $m->getPerson();
}
}
return new ArrayCollection(array_values($membersByHash));
}
public function getPreviousAddressOf(Address $address): ?Address
{
$iterator = new \ArrayIterator($this->getAddressesOrdered());
$iterator->rewind();
while ($iterator->valid()) {
$current = $iterator->current();
$iterator->next();
if ($iterator->valid()) {
if ($iterator->current() === $address) {
return $current;
}
}
}
return null;
}
public function getWaitingForBirth(): bool
{
return $this->waitingForBirth;
}
public function getWaitingForBirthDate(): ?\DateTimeImmutable
{
return $this->waitingForBirthDate;
}
/**
* @internal
*/
public function householdCompositionConsistency(): void
{
$compositionOrdered = $this->compositions->toArray();
usort(
$compositionOrdered,
static fn (HouseholdComposition $a, HouseholdComposition $b) => $a->getStartDate() <=> $b->getStartDate()
);
$iterator = new \ArrayIterator($compositionOrdered);
$iterator->rewind();
/** @var ?HouseholdComposition $previous */
$previous = null;
do {
/** @var ?HouseholdComposition $current */
$current = $iterator->current();
if (null !== $previous) {
if (null === $previous->getEndDate() || $previous->getEndDate() > $current->getStartDate()) {
$previous->setEndDate($current->getStartDate());
}
}
$previous = $current;
$iterator->next();
} while ($iterator->valid());
}
public function makeAddressConsistent(): void
{
$iterator = new \ArrayIterator($this->getAddressesOrdered());
$iterator->rewind();
while ($iterator->valid()) {
$current = $iterator->current();
$iterator->next();
if ($iterator->valid()) {
$current->setValidTo($iterator->current()->getValidFrom());
} else {
$current->setValidTo(null);
}
}
}
public function removeAddress(Address $address)
{
$this->addresses->removeElement($address);
}
public function removeComposition(HouseholdComposition $composition): self
{
if ($this->compositions->removeElement($composition)) {
$composition->setHousehold(null);
}
return $this;
}
public function removeMember(HouseholdMember $member): self
{
if ($this->members->removeElement($member)) {
// set the owning side to null (unless already changed)
if ($member->getHousehold() === $this) {
$member->setHousehold(null);
}
}
return $this;
}
public function setCommentMembers(CommentEmbeddable $commentMembers): self
{
$this->commentMembers = $commentMembers;
return $this;
}
/**
* Force an address starting at the current day
* on the Household.
*
* This will force the startDate's address on today.
*
* Used on household creation.
*/
#[Serializer\Groups(['create'])]
public function setForceAddress(Address $address)
{
$address->setValidFrom(new \DateTime('today'));
$this->addAddress($address);
}
public function setWaitingForBirth(bool $waitingForBirth): self
{
$this->waitingForBirth = $waitingForBirth;
return $this;
}
public function setWaitingForBirthDate(?\DateTimeImmutable $waitingForBirthDate): self
{
$this->waitingForBirthDate = $waitingForBirthDate;
return $this;
}
#[Assert\Callback]
public function validate(ExecutionContextInterface $context, $payload)
{
$addresses = $this->getAddresses();
$cond = true;
for ($i = 0; \count($addresses) - 1 > $i; ++$i) {
if ($addresses[$i]->getValidFrom() !== $addresses[$i + 1]->getValidTo()) {
$cond = false;
$context->buildViolation('The address are not sequentials. The validFrom date of one address should be equal to the validTo date of the previous address.')
->atPath('addresses')
->addViolation();
}
}
}
private function buildCriteriaCurrentMembers(?\DateTimeImmutable $now = null): Criteria
{
$criteria = new Criteria();
$expr = Criteria::expr();
$date = $now ?? new \DateTimeImmutable('today');
$criteria
->where($expr->orX(
$expr->isNull('startDate'),
$expr->lte('startDate', $date)
))
->andWhere($expr->orX(
$expr->isNull('endDate'),
$expr->gt('endDate', $date)
));
return $criteria;
}
}