diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/PersonHouseholdAddress.php b/src/Bundle/ChillPersonBundle/Entity/Household/PersonHouseholdAddress.php index 511416a0e..28235b521 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Household/PersonHouseholdAddress.php +++ b/src/Bundle/ChillPersonBundle/Entity/Household/PersonHouseholdAddress.php @@ -8,6 +8,29 @@ use Chill\PersonBundle\Entity\Person; use Doctrine\ORM\Mapping as ORM; /** + * This class links a person to the history of his addresses, through + * household membership. + * + * It is optimized on DB side, and compute the start date and end date + * of each address by the belonging of household. + * + * **note**: the start date and end date are the date of belonging to the address, + * not the belonging of the household. + * + * Example: + * + * * person A is member of household W from 2021-01-01 to 2021-12-01 + * * person A is member of household V from 2021-12-01, still present after + * * household W lives in address Q from 2020-06-01 to 2021-06-01 + * * household W lives in address R from 2021-06-01 to 2022-06-01 + * * household V lives in address T from 2021-12-01 to still living there after + * + * The person A will have those 3 entities: + * + * 1. 1st entity: from 2021-01-01 to 2021-06-01, household W, address Q; + * 2. 2st entity: from 2021-06-01 to 2021-12-01, household W, address R; + * 3. 3st entity: from 2021-12-01 to NULL, household V, address T; + * * @ORM\Entity(readOnly=true) * @ORM\Table(name="view_chill_person_household_address") */ @@ -45,11 +68,23 @@ class PersonHouseholdAddress */ private $address; + /** + * The start date of the intersection address/household + * + * (this is not the startdate of the household, not + * the startdate of the address) + */ public function getValidFrom(): ?\DateTimeInterface { return $this->validFrom; } + /** + * The end date of the intersection address/household + * + * (this is not the enddate of the household, not + * the enddate of the address) + */ public function getValidTo(): ?\DateTimeImmutable { return $this->validTo; diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php index f42ca6f00..bc5c46e08 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Person.php +++ b/src/Bundle/ChillPersonBundle/Entity/Person.php @@ -35,6 +35,7 @@ use Chill\PersonBundle\Entity\Household\HouseholdMember; use Chill\MainBundle\Entity\HasCenterInterface; use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable; +use Chill\PersonBundle\Entity\Person\PersonCurrentAddress; use DateTime; use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\Collection; @@ -412,6 +413,15 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI */ private $addresses; + /** + * The current person address. + * + * This is computed through database and is optimized on database side. + * + * @var PersonCurrentAddress|null + */ + private ?PersonCurrentAddress $currentPersonAddress = null; + /** * fullname canonical. Read-only field, which is calculated by * the database. @@ -564,6 +574,8 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI return $participation->getAccompanyingPeriod(); } } + + return null; } /** @@ -1242,13 +1254,31 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI return $this->addresses; } + /** + * @deprecated Use `getCurrentPersonAddress` instead + * @param DateTime|null $from + * @return false|mixed|null + * @throws \Exception + */ public function getLastAddress(DateTime $from = null) { - $from ??= new DateTime('now'); + return $this->getCurrentPersonAddress($from); + } + + /** + * get the address associated with the person at the given date + * + * @param DateTime|null $at + * @return Address|null + * @throws \Exception + */ + public function getCurrentPersonAddress(?\DateTime $at = null): ?Address + { + $at ??= new DateTime('now'); /** @var ArrayIterator $addressesIterator */ $addressesIterator = $this->getAddresses() - ->filter(static fn (Address $address): bool => $address->getValidFrom() <= $from) + ->filter(static fn (Address $address): bool => $address->getValidFrom() <= $at) ->getIterator(); $addressesIterator->uasort( @@ -1496,7 +1526,16 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI public function getCurrentHouseholdAddress(?\DateTimeImmutable $at = null): ?Address { - $at = $at === null ? new \DateTimeImmutable('today') : $at; + 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(); diff --git a/src/Bundle/ChillPersonBundle/Entity/Person/PersonCurrentAddress.php b/src/Bundle/ChillPersonBundle/Entity/Person/PersonCurrentAddress.php new file mode 100644 index 000000000..21962b700 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Entity/Person/PersonCurrentAddress.php @@ -0,0 +1,81 @@ +person; + } + + /** + * @return Address + */ + public function getAddress(): Address + { + return $this->address; + } + + /** + * This date is the intersection of household membership + * and address validity + * + * @return \DateTimeImmutable + */ + public function getValidFrom(): \DateTimeImmutable + { + return $this->validFrom; + } + + /** + * This date is the intersection of household membership + * and address validity + * + * @return \DateTimeImmutable|null + */ + public function getValidTo(): ?\DateTimeImmutable + { + return $this->validTo; + } +} diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20210915093624.php b/src/Bundle/ChillPersonBundle/migrations/Version20210915093624.php new file mode 100644 index 000000000..f285fde47 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20210915093624.php @@ -0,0 +1,49 @@ +addSql("CREATE VIEW view_chill_person_current_address AS + SELECT + cphm.person_id AS person_id, + cma.id AS address_id, + CASE WHEN cphm.startdate > COALESCE(cma.validfrom, '-infinity'::date) THEN cphm.startdate ELSE cma.validfrom END AS valid_from, + CASE WHEN COALESCE(cphm.enddate, 'infinity'::date) < COALESCE(cma.validto, 'infinity'::date) THEN cphm.enddate ELSE cma.validto END AS valid_to + FROM chill_person_household_members AS cphm + LEFT JOIN chill_person_household_to_addresses AS cphta ON cphta.household_id = cphm.household_id + LEFT JOIN chill_main_address AS cma ON cphta.address_id = cma.id + WHERE + cphm.sharedhousehold IS TRUE + AND + current_date between cphm.startdate AND coalesce(enddate, 'infinity'::date) + AND + current_date between cma.validfrom AND coalesce(validto, 'infinity'::date) + "); + + $this->addSql("CREATE INDEX chill_custom_main_address_filtering_idx ON chill_main_address USING btree (id, validfrom ASC, validto DESC)"); + $this->addSql("CREATE INDEX chill_custom_person_household_members_sharing_idx ON chill_person_household_members USING btree (person_id, startdate ASC, enddate DESC, household_id) WHERE sharedhousehold IS TRUE"); + } + + public function down(Schema $schema): void + { + $this->addSql("DROP VIEW view_chill_person_current_address"); + $this->addSql("DROP INDEX chill_custom_main_address_filtering_idx"); + $this->addSql("DROP INDEX chill_custom_person_household_members_sharing_idx"); + } +}