Person::class])] #[ORM\Entity] #[ORM\Index(name: 'person_names', columns: ['firstName', 'lastName'])] // , #[ORM\Index(name: 'person_birthdate', columns: ['birthdate'])] // @ORM\HasLifecycleCallbacks #[ORM\Table(name: 'chill_person_person')] #[ORM\HasLifecycleCallbacks] #[PersonHasCenter] #[HouseholdMembershipSequential(groups: ['household_memberships'])] class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateInterface, \Stringable { // 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 /** * Accept receiving email. */ #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])] private ?bool $acceptEmail = false; /** * Accept short text message (aka SMS). */ #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])] private ?bool $acceptSMS = false; /** * The person's accompanying periods (when the person was accompanied by the center). * * @var Collection */ #[ORM\OneToMany(targetEntity: AccompanyingPeriodParticipation::class, mappedBy: 'person', cascade: ['persist', 'remove', 'merge', 'detach'])] #[ORM\OrderBy(['startDate' => Criteria::DESC])] private Collection $accompanyingPeriodParticipations; /** * The accompanying period requested by the Person. * * @var Collection */ #[ORM\OneToMany(targetEntity: AccompanyingPeriod::class, mappedBy: 'requestorPerson')] private Collection $accompanyingPeriodRequested; /** * Addresses. * * @var Collection */ #[ORM\ManyToMany(targetEntity: Address::class, cascade: ['persist', 'remove', 'merge', 'detach'])] #[ORM\JoinTable(name: 'chill_person_persons_to_addresses')] #[ORM\OrderBy(['validFrom' => Criteria::DESC])] private Collection $addresses; /** * @var Collection */ #[ORM\OneToMany(targetEntity: PersonAltName::class, mappedBy: 'person', cascade: ['persist', 'remove', 'merge', 'detach'], orphanRemoval: true)] private Collection $altNames; /** * The person's birthdate. */ #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATE_MUTABLE, nullable: true)] #[Birthdate] private ?\DateTime $birthdate = null; /** * @var Collection */ #[ORM\OneToMany(targetEntity: Charge::class, mappedBy: 'person')] private Collection $budgetCharges; /** * @var Collection */ #[ORM\OneToMany(targetEntity: Resource::class, mappedBy: 'person')] private Collection $budgetResources; /** * @var Collection */ #[ORM\ManyToMany(targetEntity: Calendar::class, mappedBy: 'persons')] private Collection $calendars; /** * The person's center. * * @deprecated */ #[ORM\ManyToOne(targetEntity: Center::class)] private ?Center $center = null; #[ORM\OneToOne(mappedBy: 'person', targetEntity: PersonCenterCurrent::class)] private ?PersonCenterCurrent $centerCurrent = null; /** * @var Collection&Selectable */ #[ORM\OneToMany(mappedBy: 'person', targetEntity: PersonCenterHistory::class, cascade: ['persist', 'remove'])] private Collection&Selectable $centerHistory; /** * Array where customfield's data are stored. */ #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)] private ?array $cFData = null; /** * The marital status of the person. */ #[ORM\ManyToOne(targetEntity: Civility::class)] #[ORM\JoinColumn(nullable: true)] private ?Civility $civility = null; /** * Contact information for contacting the person. */ #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)] private ?string $contactInfo = ''; /** * The person's country of birth. */ #[ORM\ManyToOne(targetEntity: Country::class)] // sf4 check: option inversedBy="birthsIn" return error mapping !! #[ORM\JoinColumn(nullable: true)] private ?Country $countryOfBirth = null; #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE, 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. */ #[Assert\GreaterThanOrEqual(propertyPath: 'birthdate')] #[Assert\LessThanOrEqual('today')] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATE_IMMUTABLE, nullable: true)] private ?\DateTimeImmutable $deathdate = null; /** * The person's email. */ #[Assert\Email] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)] private ?string $email = ''; /** * The person's first name. */ #[Assert\NotBlank(message: 'The firstname cannot be empty')] #[Assert\Length(max: 255)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)] private string $firstName = ''; /** * fullname canonical. Read-only field, which is calculated by * the database. */ #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)] private ?string $fullnameCanonical = ''; /** * NEW column : The person's gender. */ #[Assert\NotNull(message: 'The gender must be set')] #[ORM\ManyToOne(targetEntity: Gender::class)] private ?Gender $gender = null; /** * Comment on gender. */ #[ORM\Embedded(class: CommentEmbeddable::class, columnPrefix: 'genderComment_')] private CommentEmbeddable $genderComment; /** * Read-only field, computed by the database. * * @var Collection */ #[ORM\OneToMany(targetEntity: PersonHouseholdAddress::class, mappedBy: 'person')] private Collection $householdAddresses; /** * @var Collection */ #[ORM\OneToMany(targetEntity: HouseholdMember::class, mappedBy: 'person')] private Collection $householdParticipations; /** * The person's id. */ #[ORM\Id] #[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)] #[ORM\GeneratedValue(strategy: 'AUTO')] private ?int $id = null; /** * The person's last name. */ #[Assert\NotBlank(message: 'The lastname cannot be empty')] #[Assert\Length(max: 255)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)] private string $lastName = ''; /** * The marital status of the person. */ #[ORM\ManyToOne(targetEntity: MaritalStatus::class)] #[ORM\JoinColumn(nullable: true)] private ?MaritalStatus $maritalStatus = null; /** * Comment on marital status. */ #[ORM\Embedded(class: CommentEmbeddable::class, columnPrefix: 'maritalStatusComment_')] private CommentEmbeddable $maritalStatusComment; /** * The date of the last marital status change of the person. */ #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATE_MUTABLE, nullable: true)] private ?\DateTime $maritalStatusDate = null; /** * A remark over the person. */ #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)] private string $memo = ''; /** * The person's administrative status. */ #[ORM\ManyToOne(targetEntity: AdministrativeStatus::class)] #[ORM\JoinColumn(nullable: true)] private ?AdministrativeStatus $administrativeStatus = null; /** * The person's mobile phone number. */ #[ORM\Column(type: 'phone_number', nullable: true)] #[PhonenumberConstraint(type: 'mobile')] private ?PhoneNumber $mobilenumber = null; /** * The person's professional status. */ #[ORM\ManyToOne(targetEntity: EmploymentStatus::class)] #[ORM\JoinColumn(nullable: true)] private ?EmploymentStatus $employmentStatus = null; /** * The person's nationality. */ #[ORM\ManyToOne(targetEntity: Country::class)] // sf4 check: option inversedBy="nationals" return error mapping !! #[ORM\JoinColumn(nullable: true)] private ?Country $nationality = null; /** * Number of children. */ #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true)] private ?int $numberOfChildren = null; /** * @var Collection */ #[Assert\Valid(traverse: true)] #[ORM\OneToMany(targetEntity: PersonPhone::class, mappedBy: 'person', cascade: ['persist', 'remove', 'merge', 'detach'], orphanRemoval: true)] private Collection $otherPhoneNumbers; /** * @var Collection */ #[ORM\OneToMany(targetEntity: AccompanyingPeriod::class, mappedBy: 'personLocation')] 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: \Doctrine\DBAL\Types\Types::STRING, length: 255, name: 'place_of_birth')] private string $placeOfBirth = ''; /** * @deprecated */ #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)] private bool $proxyAccompanyingPeriodOpenState = false; // TO-DELETE ? /** * @var Collection */ #[ORM\OneToMany(targetEntity: PersonResource::class, mappedBy: 'personOwner')] private Collection $resources; /** * The person's spoken languages. * * @var Collection */ #[ORM\ManyToMany(targetEntity: Language::class)] #[ORM\JoinTable(name: 'persons_spoken_languages', joinColumns: [new ORM\JoinColumn(name: 'person_id', referencedColumnName: 'id')], inverseJoinColumns: [new ORM\JoinColumn(name: 'language_id', referencedColumnName: 'id')])] private Collection $spokenLanguages; #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE, nullable: true, options: ['default' => null])] private ?\DateTimeInterface $updatedAt = null; #[ORM\ManyToOne(targetEntity: User::class)] private ?User $updatedBy = null; /** * @var Collection */ #[ORM\OneToMany(targetEntity: EntityWorkflowStepSignature::class, mappedBy: 'personSigner', orphanRemoval: true)] private Collection $signatures; /** * 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(); $this->signatures = 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; } public function addSignature(EntityWorkflowStepSignature $signature): self { if (!$this->signatures->contains($signature)) { $this->signatures->add($signature); } return $this; } public function removeSignature(EntityWorkflowStepSignature $signature): self { $this->signatures->removeElement($signature); return $this; } /** * @return ReadableCollection */ public function getSignatures(): ReadableCollection { return $this->signatures; } public function getSignaturesPending(): ReadableCollection { return $this->signatures->filter(fn (EntityWorkflowStepSignature $signature) => EntityWorkflowSignatureStateEnum::PENDING === $signature->getState()); } /** * 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; } public function getAdministrativeStatus(): ?AdministrativeStatus { return $this->administrativeStatus; } /** * 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 */ 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 getEmploymentStatus(): ?EmploymentStatus { return $this->employmentStatus; } public function getGender(): ?Gender { return $this->gender; } public function getGenderComment(): ?CommentEmbeddable { return $this->genderComment; } 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' => \Doctrine\Common\Collections\Order::Descending]); 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' => \Doctrine\Common\Collections\Order::Descending, 'id' => \Doctrine\Common\Collections\Order::Descending]); 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 */ 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 setAdministrativeStatus(?AdministrativeStatus $administrativeStatus): self { $this->administrativeStatus = $administrativeStatus; return $this; } public function setAcceptSMS(bool $acceptSMS): self { $this->acceptSMS = $acceptSMS; return $this; } public function setAltNames(Collection $altNames): self { $this->altNames = $altNames; return $this; } public function setBirthdate(?\DateTime $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&Selectable $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(?string $fullnameCanonical): self { $this->fullnameCanonical = $fullnameCanonical; return $this; } public function setGender(?Gender $gender): self { $this->gender = $gender; return $this; } public function setEmploymentStatus(?EmploymentStatus $employmentStatus): self { $this->employmentStatus = $employmentStatus; 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(Collection $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; } }