1853 lines
48 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;
use Chill\BudgetBundle\Entity\Charge;
use Chill\BudgetBundle\Entity\Resource;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\Country;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\MainBundle\Entity\HasCenterInterface;
use Chill\MainBundle\Entity\Language;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
use Chill\PersonBundle\Entity\Person\PersonCenterCurrent;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Chill\PersonBundle\Entity\Person\PersonCurrentAddress;
use Chill\PersonBundle\Entity\Person\PersonResource;
use Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequential;
use Chill\PersonBundle\Validator\Constraints\Person\Birthdate;
use Chill\PersonBundle\Validator\Constraints\Person\PersonHasCenter;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use libphonenumber\PhoneNumber;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* Person Class.
*
* @ORM\Entity
*
* @ORM\Table(name="chill_person_person",
* indexes={
*
* @ORM\Index(
* name="person_names",
* columns={"firstName", "lastName"}
* ),
* @ORM\Index(
* name="person_birthdate",
* columns={"birthdate"}
* )
* })
*
* @ORM\HasLifecycleCallbacks
*
* @DiscriminatorMap(typeProperty="type", mapping={
* "person": Person::class
* })
*
* @PersonHasCenter
*
* @HouseholdMembershipSequential(
* groups={"household_memberships"}
* )
*/
class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateInterface, \Stringable
{
final public const BOTH_GENDER = 'both';
// have days in commun
final public const ERROR_ADDIND_PERIOD_AFTER_AN_OPEN_PERIOD = 2; // where there exist
final public const ERROR_PERIODS_ARE_COLLAPSING = 1; // when two different periods
final public const FEMALE_GENDER = 'woman';
final public const MALE_GENDER = 'man';
final public const NO_INFORMATION = 'unknown';
/**
* Accept receiving email.
*
* @ORM\Column(type="boolean", options={"default": false})
*/
private ?bool $acceptEmail = false;
/**
* Accept short text message (aka SMS).
*
* @ORM\Column(type="boolean", options={"default": false})
*/
private ?bool $acceptSMS = false;
/**
* The person's accompanying periods (when the person was accompanied by the center).
*
* @var Collection<AccompanyingPeriodParticipation>
*
* @ORM\OneToMany(targetEntity=AccompanyingPeriodParticipation::class,
* mappedBy="person",
* cascade={"persist", "remove", "merge", "detach"})
*
* @ORM\OrderBy({"startDate": "DESC"})
*/
private Collection $accompanyingPeriodParticipations;
/**
* The accompanying period requested by the Person.
*
* @ORM\OneToMany(targetEntity=AccompanyingPeriod::class,
* mappedBy="requestorPerson")
*
* @var Collection<AccompanyingPeriod>
*/
private Collection $accompanyingPeriodRequested;
/**
* Addresses.
*
* @var Collection<Address>
*
* @ORM\ManyToMany(
* targetEntity="Chill\MainBundle\Entity\Address",
* cascade={"persist", "remove", "merge", "detach"})
*
* @ORM\JoinTable(name="chill_person_persons_to_addresses")
*
* @ORM\OrderBy({"validFrom": "DESC"})
*/
private Collection $addresses;
/**
* @var Collection<PersonAltName>
*
* @ORM\OneToMany(
* targetEntity="Chill\PersonBundle\Entity\PersonAltName",
* mappedBy="person",
* cascade={"persist", "remove", "merge", "detach"},
* orphanRemoval=true)
*/
private Collection $altNames;
/**
* The person's birthdate.
*
* @ORM\Column(type="date", nullable=true)
*
* @Birthdate
*/
private ?\DateTime $birthdate = null;
/**
* @var Collection<Charge>
*
* @ORM\OneToMany(
* targetEntity=Charge::class,
* mappedBy="person"
* )
*/
private Collection $budgetCharges;
/**
* @var Collection<resource>
*
* @ORM\OneToMany(
* targetEntity=Resource::class,
* mappedBy="person"
* )
*/
private Collection $budgetResources;
/**
* @var Collection<int, Calendar>
*
* @ORM\ManyToMany(
* targetEntity="Chill\CalendarBundle\Entity\Calendar",
* mappedBy="persons"
* )
*/
private Collection $calendars;
/**
* The person's center.
*
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center")
*
* @deprecated
*/
private ?Center $center = null;
/**
* @ORM\OneToOne(targetEntity=PersonCenterCurrent::class, mappedBy="person")
*/
private ?PersonCenterCurrent $centerCurrent = null;
/**
* @ORM\OneToMany(targetEntity=PersonCenterHistory::class, mappedBy="person", cascade={"persist", "remove"})
*
* @var Collection<PersonCenterHistory>
*/
private Collection $centerHistory;
/**
* Array where customfield's data are stored.
*
* @ORM\Column(type="json")
*/
private ?array $cFData = null;
/**
* The marital status of the person.
*
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Civility")
*
* @ORM\JoinColumn(nullable=true)
*/
private ?Civility $civility = null;
/**
* Contact information for contacting the person.
*
* @ORM\Column(type="text", nullable=true)
*/
private ?string $contactInfo = '';
/**
* The person's country of birth.
*
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Country")
*
* sf4 check: option inversedBy="birthsIn" return error mapping !!
*
* @ORM\JoinColumn(nullable=true)
*/
private ?Country $countryOfBirth = null;
/**
* @ORM\Column(type="datetime", nullable=true, options={"default": NULL})
*/
private ?\DateTimeInterface $createdAt = null;
/**
* @ORM\ManyToOne(targetEntity=User::class)
*
* @ORM\JoinColumn(nullable=true)
*/
private ?User $createdBy = null;
/**
* Cache the computation of household.
*/
private array $currentHouseholdAt = [];
/**
* Cache for the computation of current household participation.
*/
private array $currentHouseholdParticipationAt = [];
/**
* The current person address.
*
* This is computed through database and is optimized on database side.
*
* @ORM\OneToOne(targetEntity=PersonCurrentAddress::class, mappedBy="person")
*/
private ?PersonCurrentAddress $currentPersonAddress = null;
/**
* The person's deathdate.
*
* @ORM\Column(type="date_immutable", nullable=true)
*
* @Assert\Date
*
* @Assert\GreaterThanOrEqual(propertyPath="birthdate")
*
* @Assert\LessThanOrEqual("today")
*/
private ?\DateTimeImmutable $deathdate = null;
/**
* The person's email.
*
* @ORM\Column(type="text", nullable=true)
*
* @Assert\Email()
*/
private ?string $email = '';
/**
* The person's first name.
*
* @ORM\Column(type="string", length=255)
*
* @Assert\NotBlank(message="The firstname cannot be empty")
*
* @Assert\Length(
* max=255,
* )
*/
private string $firstName = '';
/**
* fullname canonical. Read-only field, which is calculated by
* the database.
*
* @ORM\Column(type="text", nullable=true)
*/
private ?string $fullnameCanonical = '';
/**
* The person's gender.
*
* @ORM\Column(type="string", length=9, nullable=true)
*
* @Assert\NotNull(message="The gender must be set")
*/
private ?string $gender = null;
/**
* Comment on gender.
*
* @ORM\Embedded(class="Chill\MainBundle\Entity\Embeddable\CommentEmbeddable", columnPrefix="genderComment_")
*/
private CommentEmbeddable $genderComment;
/**
* Read-only field, computed by the database.
*
* @var Collection<PersonHouseholdAddress>
*
* @ORM\OneToMany(
* targetEntity=PersonHouseholdAddress::class,
* mappedBy="person"
* )
*/
private Collection $householdAddresses;
/**
* @ORM\OneToMany(
* targetEntity=HouseholdMember::class,
* mappedBy="person"
* )
*
* @var Collection<HouseholdMember>
*/
private Collection $householdParticipations;
/**
* The person's id.
*
* @ORM\Id
*
* @ORM\Column(name="id", type="integer")
*
* @ORM\GeneratedValue(strategy="AUTO")
*/
private ?int $id = null;
/**
* The person's last name.
*
* @ORM\Column(type="string", length=255)
*
* @Assert\NotBlank(message="The lastname cannot be empty")
*
* @Assert\Length(
* max=255,
* )
*/
private string $lastName = '';
/**
* The marital status of the person.
*
* @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\MaritalStatus")
*
* @ORM\JoinColumn(nullable=true)
*/
private ?MaritalStatus $maritalStatus = null;
/**
* Comment on marital status.
*
* @ORM\Embedded(class="Chill\MainBundle\Entity\Embeddable\CommentEmbeddable", columnPrefix="maritalStatusComment_")
*/
private CommentEmbeddable $maritalStatusComment;
/**
* The date of the last marital status change of the person.
*
* @ORM\Column(type="date", nullable=true)
*
* @Assert\Date
*/
private ?\DateTime $maritalStatusDate = null;
/**
* A remark over the person.
*
* @ORM\Column(type="text")
*/
private string $memo = '';
/**
* The person's mobile phone number.
*
* @PhonenumberConstraint(type="mobile")
*
* @ORM\Column(type="phone_number", nullable=true)
*/
private ?PhoneNumber $mobilenumber = null;
/**
* The person's nationality.
*
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Country")
*
* sf4 check: option inversedBy="nationals" return error mapping !!
*
* @ORM\JoinColumn(nullable=true)
*/
private ?Country $nationality = null;
/**
* Number of children.
*
* @ORM\Column(type="integer", nullable=true)
*/
private ?int $numberOfChildren = null;
/**
* @var Collection<PersonPhone>
*
* @ORM\OneToMany(
* targetEntity="Chill\PersonBundle\Entity\PersonPhone",
* mappedBy="person",
* cascade={"persist", "remove", "merge", "detach"},
* orphanRemoval=true
* )
*
* @Assert\Valid(
* traverse=true,
* )
*/
private Collection $otherPhoneNumbers;
/**
* @ORM\OneToMany(
* targetEntity=AccompanyingPeriod::class,
* mappedBy="personLocation"
* )
*
* @var Collection<AccompanyingPeriod>
*/
private Collection $periodLocatedOn;
/**
* The person's phonenumber.
*
* @ORM\Column(type="phone_number", nullable=true)
*
* @PhonenumberConstraint(
* type="landline",
* )
*/
private ?PhoneNumber $phonenumber = null;
/**
* The person's place of birth.
*
* @ORM\Column(type="string", length=255, name="place_of_birth")
*/
private string $placeOfBirth = '';
/**
* @deprecated
*
* @ORM\Column(type="boolean")
*/
private bool $proxyAccompanyingPeriodOpenState = false; // TO-DELETE ?
/**
* @ORM\OneToMany(targetEntity=PersonResource::class, mappedBy="personOwner")
*
* @var Collection<PersonResource>
*/
private Collection $resources;
/**
* The person's spoken languages.
*
* @var Collection<Language>
*
* @ORM\ManyToMany(targetEntity="Chill\MainBundle\Entity\Language")
*
* @ORM\JoinTable(
* name="persons_spoken_languages",
* joinColumns={@ORM\JoinColumn(name="person_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="language_id", referencedColumnName="id")}
* )
*/
private Collection $spokenLanguages;
/**
* @ORM\Column(type="datetime", nullable=true, options={"default": NULL})
*/
private ?\DateTimeInterface $updatedAt = null;
/**
* @ORM\ManyToOne(
* targetEntity=User::class
* )
*/
private ?User $updatedBy = null;
/**
* Person constructor.
*/
public function __construct()
{
$this->calendars = new ArrayCollection();
$this->accompanyingPeriodParticipations = new ArrayCollection();
$this->spokenLanguages = new ArrayCollection();
$this->addresses = new ArrayCollection();
$this->altNames = new ArrayCollection();
$this->otherPhoneNumbers = new ArrayCollection();
$this->householdParticipations = new ArrayCollection();
$this->householdAddresses = new ArrayCollection();
$this->genderComment = new CommentEmbeddable();
$this->maritalStatusComment = new CommentEmbeddable();
$this->periodLocatedOn = new ArrayCollection();
$this->accompanyingPeriodRequested = new ArrayCollection();
$this->budgetResources = new ArrayCollection();
$this->budgetCharges = new ArrayCollection();
$this->resources = new ArrayCollection();
$this->centerHistory = new ArrayCollection();
}
public function __toString(): string
{
return $this->getLabel();
}
/**
* Add AccompanyingPeriodParticipation.
*
* @uses AccompanyingPeriod::addPerson
*/
public function addAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): self
{
$participation = new AccompanyingPeriodParticipation($accompanyingPeriod, $this);
$this->accompanyingPeriodParticipations->add($participation);
return $this;
}
public function addAddress(Address $address): self
{
$this->addresses[] = $address;
return $this;
}
public function addAltName(PersonAltName $altName): self
{
if (false === $this->altNames->contains($altName)) {
$this->altNames->add($altName);
$altName->setPerson($this);
}
return $this;
}
public function addBudgetCharge(Charge $budgetCharge): self
{
$this->budgetCharges[] = $budgetCharge;
return $this;
}
public function addBudgetResource(Resource $budgetResource): self
{
$this->budgetResources[] = $budgetResource;
return $this;
}
public function addHouseholdParticipation(HouseholdMember $member): self
{
$this->householdParticipations[] = $member;
return $this;
}
/**
* @return $this
*/
public function addOtherPhoneNumber(PersonPhone $otherPhoneNumber)
{
if (false === $this->otherPhoneNumbers->contains($otherPhoneNumber)) {
$otherPhoneNumber->setPerson($this);
$this->otherPhoneNumbers->add($otherPhoneNumber);
}
return $this;
}
/**
* Function used for validation that check if the accompanying periods of
* the person are not collapsing (i.e. have not shared days) or having
* a period after an open period.
*
* @return true|array True if the accompanying periods are not collapsing,
* an array with data for displaying the error
*/
public function checkAccompanyingPeriodsAreNotCollapsing(): array|bool
{
$periods = $this->getAccompanyingPeriodsOrdered();
$periodsNbr = \count($periods);
$i = 0;
while ($periodsNbr - 1 > $i) {
$periodI = $periods[$i];
$periodAfterI = $periods[$i + 1];
if ($periodI->isOpen()) {
return [
'result' => self::ERROR_ADDIND_PERIOD_AFTER_AN_OPEN_PERIOD,
'dateOpening' => $periodAfterI->getOpeningDate(),
'dateClosing' => $periodAfterI->getClosingDate(),
'date' => $periodI->getOpeningDate(),
];
}
if ($periodI->getClosingDate() >= $periodAfterI->getOpeningDate()) {
return [
'result' => self::ERROR_PERIODS_ARE_COLLAPSING,
'dateOpening' => $periodI->getOpeningDate(),
'dateClosing' => $periodI->getClosingDate(),
'date' => $periodAfterI->getOpeningDate(),
];
}
++$i;
}
return true;
}
/**
* Set the Person file as closed at the given date.
*
* For update a closing date, you should update AccompanyingPeriod instance
* directly.
*
* To check if the Person and its accompanying period are consistent, use validation.
*
* @throws \Exception if two lines of the accompanying period are open
*/
public function close(AccompanyingPeriod $accompanyingPeriod = null): void
{
$this->proxyAccompanyingPeriodOpenState = false;
}
public function countResources(): int
{
return $this->resources->count();
}
/**
* This public function is the same but return only true or false.
*/
public function containsAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool
{
return ($this->participationsContainAccompanyingPeriod($accompanyingPeriod)) ? false : true;
}
/**
* Handy method to get the AccompanyingPeriodParticipation
* matching a given AccompanyingPeriod.
*
* Used in template, to find the participation when iterating on a list
* of period.
*/
public function findParticipationForPeriod(AccompanyingPeriod $period): ?AccompanyingPeriodParticipation
{
$closeCandidates = [];
foreach ($this->getAccompanyingPeriodParticipations() as $participation) {
if ($participation->getAccompanyingPeriod() === $period) {
if ($participation->isOpen()) {
return $participation;
}
$closeCandidates[] = $participation;
}
}
if (0 < \count($closeCandidates)) {
return $closeCandidates[0];
}
return null;
}
public function getAcceptEmail(): ?bool
{
return $this->acceptEmail;
}
public function getAcceptSMS(): ?bool
{
return $this->acceptSMS;
}
/**
* Return a list of all accompanying period where the person is involved:.
*
* * as requestor;
* * as participant, only for opened participation;
*
* @param bool $asParticipantOpen add participation which are still opened
* @param bool $asRequestor add accompanying period where the person is requestor
*
* @return AccompanyingPeriod[]|Collection
*/
public function getAccompanyingPeriodInvolved(
bool $asParticipantOpen = true,
bool $asRequestor = true
): Collection {
$result = new ArrayCollection();
if ($asParticipantOpen) {
foreach ($this->getAccompanyingPeriodParticipations()
->map(fn (AccompanyingPeriodParticipation $app) => $app->getAccompanyingPeriod()) as $period
) {
if (!$result->contains($period)) {
$result->add($period);
}
}
}
if ($asRequestor) {
foreach ($this->accompanyingPeriodRequested as $period) {
if (!$result->contains($period)) {
$result->add($period);
}
}
}
return $result;
}
public function countAccompanyingPeriodInvolved(
bool $asParticipantOpen = true,
bool $asRequestor = true
): int {
// TODO should be optimized to avoid loading accompanying period ?
return $this->getAccompanyingPeriodInvolved($asParticipantOpen, $asRequestor)
->filter(fn (AccompanyingPeriod $p) => AccompanyingPeriod::STEP_DRAFT !== $p->getStep())
->count();
}
/**
* Get AccompanyingPeriodParticipations Collection.
*
* @return AccompanyingPeriodParticipation[]|Collection
*/
public function getAccompanyingPeriodParticipations(): Collection
{
return $this->accompanyingPeriodParticipations;
}
/**
* @return AccompanyingPeriod[]|Collection
*/
public function getAccompanyingPeriodRequested(): Collection
{
return $this->accompanyingPeriodRequested;
}
/**
* Get AccompanyingPeriods array.
*/
public function getAccompanyingPeriods(): array
{
$accompanyingPeriods = [];
foreach ($this->accompanyingPeriodParticipations as $participation) {
/* @var AccompanyingPeriodParticipation $participation */
$accompanyingPeriods[] = $participation->getAccompanyingPeriod();
}
return $accompanyingPeriods;
}
/**
* Get the accompanying periods of a give person with the chronological order.
*/
public function getAccompanyingPeriodsOrdered(): array
{
$periods = $this->getAccompanyingPeriods();
// order by date :
usort($periods, static function ($a, $b) {
$dateA = $a->getOpeningDate();
$dateB = $b->getOpeningDate();
if ($dateA === $dateB) {
$dateEA = $a->getClosingDate();
$dateEB = $b->getClosingDate();
if ($dateEA === $dateEB) {
return 0;
}
if ($dateEA < $dateEB) {
return -1;
}
return +1;
}
if ($dateA < $dateB) {
return -1;
}
return 1;
});
return $periods;
}
/**
* get the address associated with the person at the given date.
*
* If the `$at` parameter is now, use the method `getCurrentPersonAddress`, which is optimized
* on database side.
*
* @deprecated since chill2.0, address is linked to the household. Use @see{Person::getCurrentHouseholdAddress}
*
* @throws \Exception
*/
public function getAddressAt(\DateTimeInterface $at = null): ?Address
{
$at ??= new \DateTime('now');
if ($at instanceof \DateTimeImmutable) {
$at = \DateTime::createFromImmutable($at);
}
/** @var \ArrayIterator $addressesIterator */
$addressesIterator = $this->getAddresses()
->filter(static fn (Address $address): bool => $address->getValidFrom() <= $at)
->getIterator();
$addressesIterator->uasort(
static fn (Address $left, Address $right): int => $right->getValidFrom() <=> $left->getValidFrom()
);
return [] === ($addresses = iterator_to_array($addressesIterator)) ?
null :
current($addresses);
}
/**
* By default, the addresses are ordered by date, descending (the most
* recent first).
*/
public function getAddresses(): Collection
{
return $this->addresses;
}
/**
* Return the age of a person, calculated at the date 'now'.
*
* If the person has a deathdate, calculate the age at the deathdate.
*
* @param string $at a valid string to create a DateTime
*/
public function getAge(string $at = 'now'): ?int
{
if ($this->birthdate instanceof \DateTimeInterface) {
if ($this->deathdate instanceof \DateTimeInterface) {
return (int) date_diff($this->birthdate, $this->deathdate)->format('%y');
}
return (int) date_diff($this->birthdate, date_create($at))->format('%y');
}
return null;
}
public function getAltNames(): Collection
{
return $this->altNames;
}
public function getBirthdate(): ?\DateTime
{
return $this->birthdate;
}
/**
* @return Collection|Charge[]
*/
public function getBudgetCharges(): Collection
{
return $this->budgetCharges;
}
/**
* @return Collection|\Chill\BudgetBundle\Entity\Resource[]
*/
public function getBudgetResources(): Collection
{
return $this->budgetResources;
}
/**
* @return Collection<int, Calendar>
*/
public function getCalendars(): Collection
{
return $this->calendars;
}
public function getCenter(): ?Center
{
if (null !== $this->centerCurrent) {
return $this->centerCurrent->getCenter();
}
if (null === $currentCenterHistory = $this->getCurrentCenterHistory()) {
return null;
}
return $currentCenterHistory->getCenter();
}
public function getCenterCurrent(): ?PersonCenterCurrent
{
if (null !== $this->centerCurrent) {
return $this->centerCurrent;
}
if (null === $currentCenterHistory = $this->getCurrentCenterHistory()) {
return null;
}
return new PersonCenterCurrent($currentCenterHistory);
}
public function getCenterHistory(): Collection
{
return $this->centerHistory;
}
public function getCFData(): ?array
{
if (null === $this->cFData) {
$this->cFData = [];
}
return $this->cFData;
}
public function getCivility(): ?Civility
{
return $this->civility;
}
public function getcontactInfo(): ?string
{
return $this->contactInfo;
}
public function getCountryOfBirth(): ?Country
{
return $this->countryOfBirth;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function getCreatedBy(): ?User
{
return $this->createdBy;
}
/**
* Returns the opened accompanying period.
*
* @deprecated since 1.1 use `getOpenedAccompanyingPeriod instead
*/
public function getCurrentAccompanyingPeriod(): ?AccompanyingPeriod
{
return $this->getOpenedAccompanyingPeriod();
}
/**
* Get current accompanyingPeriods array.
*
* @return AccompanyingPeriod[]|array
*/
public function getCurrentAccompanyingPeriods(): array
{
$currentAccompanyingPeriods = [];
$currentDate = new \DateTime();
foreach ($this->accompanyingPeriodParticipations as $participation) {
$endDate = $participation->getEndDate();
if (null === $endDate || $endDate > $currentDate) {
$currentAccompanyingPeriods[] = $participation->getAccompanyingPeriod();
}
}
return $currentAccompanyingPeriods;
}
public function getCurrentHousehold(\DateTimeImmutable $at = null): ?Household
{
$participation = $this->getCurrentHouseholdParticipationShareHousehold($at);
return $participation instanceof HouseholdMember ?
$participation->getHousehold()
: null;
}
/**
* Get the household address at the given date.
*
* if the given date is 'now', use instead @see{getCurrentPersonAddress}, which is optimized on
* database side.
*/
public function getCurrentHouseholdAddress(\DateTimeImmutable $at = null): ?Address
{
if (
null === $at
|| $at->format('Ymd') === (new \DateTime('today'))->format('Ymd')
) {
return $this->currentPersonAddress instanceof PersonCurrentAddress
? $this->currentPersonAddress->getAddress() : null;
}
// if not now, compute the date from history
$criteria = new Criteria();
$expr = Criteria::expr();
$criteria->where(
$expr->lte('validFrom', $at)
)
->andWhere(
$expr->orX(
$expr->isNull('validTo'),
$expr->gte('validTo', $at)
)
);
$addrs = $this->getHouseholdAddresses()
->matching($criteria);
if ($addrs->count() > 0) {
return $addrs->first()->getAddress();
}
return null;
}
public function getCurrentHouseholdParticipationShareHousehold(\DateTimeImmutable $at = null): ?HouseholdMember
{
$criteria = new Criteria();
$expr = Criteria::expr();
$date = $at ?? new \DateTimeImmutable('today');
$datef = $date->format('Y-m-d');
if (
null !== ($this->currentHouseholdParticipationAt[$datef] ?? null)) {
return $this->currentHouseholdParticipationAt[$datef];
}
$criteria
->where(
$expr->andX(
$expr->lte('startDate', $date),
$expr->orX(
$expr->isNull('endDate'),
$expr->gt('endDate', $date)
),
$expr->eq('shareHousehold', true)
)
);
$participations = $this->getHouseholdParticipations()
->matching($criteria);
return $participations->count() > 0 ?
$this->currentHouseholdParticipationAt[$datef] = $participations->first()
: null;
}
/**
* Get the current person address.
*/
public function getCurrentPersonAddress(): ?Address
{
if (null === $this->currentPersonAddress) {
return null;
}
return $this->currentPersonAddress->getAddress();
}
public function getDeathdate(): ?\DateTimeInterface
{
return $this->deathdate;
}
public function getEmail(): ?string
{
return $this->email;
}
public function getFirstName(): string
{
return $this->firstName;
}
public function getFullnameCanonical(): string
{
return $this->fullnameCanonical;
}
public function getGender(): ?string
{
return $this->gender;
}
public function getGenderComment(): ?CommentEmbeddable
{
return $this->genderComment;
}
/**
* return gender as a Numeric form.
* This is used for translations.
*
* @return int
*
* @deprecated Keep for legacy. Used in Chill 1.5 for feminize before icu translations
*/
public function getGenderNumeric()
{
return match ($this->getGender()) {
self::FEMALE_GENDER => 1,
self::MALE_GENDER => 0,
self::BOTH_GENDER => 2,
default => -1,
};
}
public function getHouseholdAddresses(): Collection
{
return $this->householdAddresses;
}
/**
* @return Collection|HouseholdMember[]
*/
public function getHouseholdParticipations(): Collection
{
return $this->householdParticipations;
}
/**
* Get participation where the person does not share the household.
*
* Order by startDate, desc
*
* @return HouseholdMember[]
*/
public function getHouseholdParticipationsNotShareHousehold(): Collection
{
$criteria = new Criteria();
$expr = Criteria::expr();
$criteria
->where(
$expr->eq('shareHousehold', false)
)
->orderBy(['startDate' => Criteria::DESC]);
return $this->getHouseholdParticipations()
->matching($criteria);
}
/**
* Get participation where the person does share the household.
*
* Order by startDate, desc
*
* @return Collection|HouseholdMember[]
*/
public function getHouseholdParticipationsShareHousehold(): Collection
{
$criteria = new Criteria();
$expr = Criteria::expr();
$criteria
->where(
$expr->eq('shareHousehold', true)
)
->orderBy(['startDate' => Criteria::DESC, 'id' => Criteria::DESC]);
return $this->getHouseholdParticipations()
->matching($criteria);
}
public function getId(): ?int
{
return $this->id;
}
/**
* @return string
*/
public function getLabel()
{
return $this->getFirstName().' '.$this->getLastName();
}
/**
* @deprecated Use @see{Person::getCurrentPersonAddress} or @see{Person::getCurrentHouseholdAddress} instead
*
* @return false|mixed|null
*
* @throws \Exception
*/
public function getLastAddress(\DateTime $from = null)
{
return $this->getCurrentHouseholdAddress(
null !== $from ? \DateTimeImmutable::createFromMutable($from) : null
);
}
public function getLastName(): string
{
return $this->lastName;
}
public function getMaritalStatus(): ?MaritalStatus
{
return $this->maritalStatus;
}
public function getMaritalStatusComment(): CommentEmbeddable
{
return $this->maritalStatusComment;
}
public function getMaritalStatusDate(): ?\DateTimeInterface
{
return $this->maritalStatusDate;
}
public function getMemo(): ?string
{
return $this->memo;
}
public function getMobilenumber(): ?PhoneNumber
{
return $this->mobilenumber;
}
public function getNationality(): ?Country
{
return $this->nationality;
}
public function getNumberOfChildren(): ?int
{
return $this->numberOfChildren;
}
/**
* Return the opened accompanying period.
*/
public function getOpenedAccompanyingPeriod(): ?AccompanyingPeriod
{
if (false === $this->isOpen()) {
return null;
}
foreach ($this->accompanyingPeriodParticipations as $participation) {
/** @var AccompanyingPeriodParticipation $participation */
if ($participation->getAccompanyingPeriod()->isOpen()) {
return $participation->getAccompanyingPeriod();
}
}
return null;
}
/**
* Return a collection of participation, where the participation
* is still opened or in draft state.
*
* @return AccompanyingPeriodParticipation[]|Collection
*/
public function getOpenedParticipations(): Collection
{
// create a criteria for filtering easily
$criteria = Criteria::create();
$criteria
->andWhere(Criteria::expr()->eq('endDate', null))
->orWhere(Criteria::expr()->gt('endDate', new \DateTime('now')));
return $this->getAccompanyingPeriodParticipations()
->matching($criteria)
->filter(static fn (AccompanyingPeriodParticipation $app) => AccompanyingPeriod::STEP_CLOSED !== $app->getAccompanyingPeriod()->getStep());
}
public function getOtherPhoneNumbers(): Collection
{
return $this->otherPhoneNumbers;
}
public function getPhonenumber(): ?PhoneNumber
{
return $this->phonenumber;
}
public function getPlaceOfBirth(): ?string
{
return $this->placeOfBirth;
}
/**
* @return PersonResource[]|Collection
*/
public function getResources(): array|Collection
{
return $this->resources;
}
/**
* Get spokenLanguages.
*
* @return Collection<Language>
*/
public function getSpokenLanguages()
{
return $this->spokenLanguages;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updatedAt;
}
public function getUpdatedBy(): ?User
{
return $this->updatedBy;
}
public function hasCurrentHouseholdAddress(\DateTimeImmutable $at = null): bool
{
return null !== $this->getCurrentHouseholdAddress($at);
}
/**
* Return true if the person has two addresses with the
* same validFrom date (in format 'Y-m-d').
*/
public function hasTwoAdressWithSameValidFromDate()
{
$validYMDDates = [];
foreach ($this->addresses as $ad) {
$validDate = $ad->getValidFrom()->format('Y-m-d');
if (\in_array($validDate, $validYMDDates, true)) {
return true;
}
$validYMDDates[] = $validDate;
}
return false;
}
/**
* Validation callback that checks if the accompanying periods are valid.
*
* This method add violation errors.
*
* @Assert\Callback(
* groups={"accompanying_period_consistent"}
* )
*/
public function isAccompanyingPeriodValid(ExecutionContextInterface $context)
{
$r = $this->checkAccompanyingPeriodsAreNotCollapsing();
if (true !== $r) {
if (self::ERROR_PERIODS_ARE_COLLAPSING === $r['result']) {
$context->buildViolation('Two accompanying periods have days in commun')
->atPath('accompanyingPeriods')
->addViolation();
}
if (self::ERROR_ADDIND_PERIOD_AFTER_AN_OPEN_PERIOD === $r['result']) {
$context->buildViolation('A period is opened and a period is added after it')
->atPath('accompanyingPeriods')
->addViolation();
}
}
}
/**
* Validation callback that checks if the addresses are valid (do not have
* two addresses with the same validFrom date).
*
* This method add violation errors.
*
* @Assert\Callback(
* groups={"addresses_consistent"}
* )
*/
public function isAddressesValid(ExecutionContextInterface $context)
{
if ($this->hasTwoAdressWithSameValidFromDate()) {
$context
->buildViolation('Two addresses has the same validFrom date')
->atPath('addresses')
->addViolation();
}
}
/**
* Check if the person is opened.
*/
public function isOpen(): bool
{
foreach ($this->getAccompanyingPeriods() as $period) {
if ($period->isOpen()) {
return true;
}
}
return false;
}
public function isSharingHousehold(\DateTimeImmutable $at = null): bool
{
return null !== $this->getCurrentHousehold($at);
}
/**
* set the Person file as open at the given date.
*
* For updating a opening's date, you should update AccompanyingPeriod instance
* directly.
*
* For closing a file, @see this::close
*
* To check if the Person and its accompanying period is consistent, use validation.
*/
public function open(AccompanyingPeriod $accompanyingPeriod): void
{
$this->proxyAccompanyingPeriodOpenState = true;
$this->addAccompanyingPeriod($accompanyingPeriod);
}
/**
* Remove AccompanyingPeriod.
*/
public function removeAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): void
{
$participation = $this->participationsContainAccompanyingPeriod($accompanyingPeriod);
if (!null === $participation) {
$participation->setEndDate(new \DateTime());
$this->accompanyingPeriodParticipations->removeElement($participation);
}
}
public function removeAddress(Address $address)
{
$this->addresses->removeElement($address);
}
public function removeAltName(PersonAltName $altName): self
{
if ($this->altNames->contains($altName)) {
$altName->setPerson(null);
$this->altNames->removeElement($altName);
}
return $this;
}
public function removeBudgetCharge(Charge $budgetCharge): self
{
$this->budgetCharges->removeElement($budgetCharge);
return $this;
}
public function removeBudgetResource(Resource $budgetResource): self
{
$this->budgetResources->removeElement($budgetResource);
return $this;
}
public function removeOtherPhoneNumber(PersonPhone $otherPhoneNumber): self
{
if ($this->otherPhoneNumbers->contains($otherPhoneNumber)) {
$this->otherPhoneNumbers->removeElement($otherPhoneNumber);
}
return $this;
}
public function setAcceptEmail(bool $acceptEmail): self
{
$this->acceptEmail = $acceptEmail;
return $this;
}
public function setAcceptSMS(bool $acceptSMS): self
{
$this->acceptSMS = $acceptSMS;
return $this;
}
public function setAltNames(Collection $altNames): self
{
$this->altNames = $altNames;
return $this;
}
/**
* @param \DateTime $birthdate
*/
public function setBirthdate($birthdate): self
{
$this->birthdate = $birthdate;
return $this;
}
/**
* Associate the center with the person. The association start on 'now'.
*
* @return $this
*/
public function setCenter(?Center $center): self
{
$modification = new \DateTimeImmutable('now');
foreach ($this->centerHistory as $centerHistory) {
if (null === $centerHistory->getEndDate()) {
$centerHistory->setEndDate($modification);
}
}
if (null === $center) {
return $this;
}
$this->centerHistory[] = new PersonCenterHistory($this, $center, $modification);
return $this;
}
public function setCenterHistory(Collection $centerHistory): Person
{
$this->centerHistory = $centerHistory;
return $this;
}
public function addCenterHistory(PersonCenterHistory $newCenterHistory): self
{
if (!$this->centerHistory->contains($newCenterHistory)) {
$this->centerHistory[] = $newCenterHistory;
$newCenterHistory->setPerson($this);
}
return $this;
}
public function setCFData(?array $cFData): self
{
$this->cFData = $cFData;
return $this;
}
public function setCivility(Civility $civility = null): self
{
$this->civility = $civility;
return $this;
}
public function setcontactInfo($contactInfo): self
{
if (null === $contactInfo) {
$contactInfo = '';
}
$this->contactInfo = $contactInfo;
return $this;
}
public function setCountryOfBirth(Country $countryOfBirth = null): self
{
$this->countryOfBirth = $countryOfBirth;
return $this;
}
public function setCreatedAt(\DateTimeInterface $datetime): self
{
$this->createdAt = $datetime;
return $this;
}
public function setCreatedBy(User $createdBy): self
{
$this->createdBy = $createdBy;
return $this;
}
public function setDeathdate(?\DateTimeInterface $deathdate): self
{
$this->deathdate = $deathdate;
return $this;
}
public function setEmail(?string $email): self
{
$this->email = trim((string) $email);
return $this;
}
public function setFirstName(?string $firstName): self
{
$this->firstName = (string) $firstName;
return $this;
}
public function setFullnameCanonical($fullnameCanonical): self
{
$this->fullnameCanonical = $fullnameCanonical;
return $this;
}
public function setGender(?string $gender): self
{
$this->gender = $gender;
return $this;
}
public function setGenderComment(CommentEmbeddable $genderComment): self
{
$this->genderComment = $genderComment;
return $this;
}
public function setLastName(?string $lastName): self
{
$this->lastName = (string) $lastName;
return $this;
}
public function setMaritalStatus(MaritalStatus $maritalStatus = null): self
{
$this->maritalStatus = $maritalStatus;
return $this;
}
public function setMaritalStatusComment(CommentEmbeddable $maritalStatusComment): self
{
$this->maritalStatusComment = $maritalStatusComment;
return $this;
}
public function setMaritalStatusDate(?\DateTimeInterface $maritalStatusDate): self
{
$this->maritalStatusDate = $maritalStatusDate;
return $this;
}
public function setMemo(?string $memo): self
{
if (null === $memo) {
$memo = '';
}
if ($this->memo !== $memo) {
$this->memo = $memo;
}
return $this;
}
public function setMobilenumber(?PhoneNumber $mobilenumber): self
{
$this->mobilenumber = $mobilenumber;
return $this;
}
public function setNationality(Country $nationality = null): self
{
$this->nationality = $nationality;
return $this;
}
public function setNumberOfChildren(?int $numberOfChildren): self
{
$this->numberOfChildren = $numberOfChildren;
return $this;
}
public function setOtherPhoneNumbers(Collection $otherPhoneNumbers): self
{
$this->otherPhoneNumbers = $otherPhoneNumbers;
return $this;
}
public function setPhonenumber(?PhoneNumber $phonenumber): self
{
$this->phonenumber = $phonenumber;
return $this;
}
public function setPlaceOfBirth(?string $placeOfBirth): self
{
if (null === $placeOfBirth) {
$placeOfBirth = '';
}
$this->placeOfBirth = $placeOfBirth;
return $this;
}
/**
* @param Collection $spokenLanguages
*/
public function setSpokenLanguages($spokenLanguages): self
{
$this->spokenLanguages = $spokenLanguages;
return $this;
}
public function setUpdatedAt(\DateTimeInterface $datetime): self
{
$this->updatedAt = $datetime;
return $this;
}
public function setUpdatedBy(User $user): self
{
$this->updatedBy = $user;
return $this;
}
private function getCurrentCenterHistory(): ?PersonCenterHistory
{
if (0 === $this->centerHistory->count()) {
return null;
}
$criteria = Criteria::create();
$now = new \DateTimeImmutable('now');
$criteria->where(Criteria::expr()->lte('startDate', $now))
->andWhere(Criteria::expr()->orX(
Criteria::expr()->isNull('endDate'),
Criteria::expr()->gt('endDate', $now)
));
$histories = $this->centerHistory->matching($criteria);
return match ($histories->count()) {
0 => null,
1 => $histories->first(),
default => throw new \UnexpectedValueException('It should not contains more than one center at a time'),
};
}
/**
* This private function scan accompanyingPeriodParticipations Collection,
* searching for a given AccompanyingPeriod.
*/
private function participationsContainAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): ?AccompanyingPeriodParticipation
{
foreach ($this->accompanyingPeriodParticipations as $participation) {
/** @var AccompanyingPeriodParticipation $participation */
if ($participation->getAccompanyingPeriod() === $accompanyingPeriod) {
return $participation;
}
}
return null;
}
}