From 49d2e98a1a82d8a25c1bed0a9f73b52b279fe8bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 26 Sep 2022 21:11:01 +0200 Subject: [PATCH] [person] Feature: add a person's center history The association between Person and Center is now stored in a dedicated Entity: `PersonCenterHistory`, which have a date interval (start date and endDate). The SQL counterpart is a table, with a constraint which ensure that no person might be associated with two center at the same time. For ease, a view is created to get the current center associated with the person. The dedicated migration creates also: * indexes for a rapid search for person at current date; * and populate the table from current data, setting the startdate to the person's creation date and time if any, `NOW()` unless. The `Person` entity is also updated to use the information from the PersonCenterHistory classes, but this commit does not yet delete the `Center` column. --- .../ChillPersonBundle/Entity/Person.php | 64 +++++++- .../Entity/Person/PersonCenterCurrent.php | 112 ++++++++++++++ .../Entity/Person/PersonCenterHistory.php | 142 ++++++++++++++++++ .../Person/PersonCenterHistoryInterface.php | 10 ++ .../Person/PersonCenterHistoryRepository.php | 48 ++++++ .../Tests/Entity/PersonTest.php | 26 ++++ .../migrations/Version20220926154347.php | 69 +++++++++ 7 files changed, 468 insertions(+), 3 deletions(-) create mode 100644 src/Bundle/ChillPersonBundle/Entity/Person/PersonCenterCurrent.php create mode 100644 src/Bundle/ChillPersonBundle/Entity/Person/PersonCenterHistory.php create mode 100644 src/Bundle/ChillPersonBundle/Repository/Person/PersonCenterHistoryInterface.php create mode 100644 src/Bundle/ChillPersonBundle/Repository/Person/PersonCenterHistoryRepository.php create mode 100644 src/Bundle/ChillPersonBundle/migrations/Version20220926154347.php diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php index 3a197e414..5d9f6a2cb 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Person.php +++ b/src/Bundle/ChillPersonBundle/Entity/Person.php @@ -27,6 +27,8 @@ 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; @@ -180,9 +182,21 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI * The person's center. * * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center") + * @deprecated */ private ?Center $center = null; + /** + * @ORM\OneToMany(targetEntity=PersonCenterHistory::class, mappedBy="person") + * @var Collection|PersonCenterHistory[] + */ + private Collection $centerHistory; + + /** + * @ORM\OneToOne(targetEntity=PersonCenterCurrent::class, mappedBy="person") + */ + private ?PersonCenterCurrent $centerCurrent = null; + /** * Array where customfield's data are stored. * @@ -523,6 +537,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI $this->budgetResources = new ArrayCollection(); $this->budgetCharges = new ArrayCollection(); $this->resources = new ArrayCollection(); + $this->centerHistory = new ArrayCollection(); } /** @@ -897,7 +912,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI public function getCenter(): ?Center { - return $this->center; + return null === $this->centerCurrent ? null : $this->centerCurrent->getCenter(); } public function getCFData(): ?array @@ -1510,17 +1525,60 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI return $this; } + /** + * Associate the center with the person. The association start on 'now'. + * + * @param Center $center + * @return $this + */ public function setCenter(Center $center): self { $this->center = $center; + $modification = new DateTimeImmutable('now'); + + foreach ($this->centerHistory as $centerHistory) { + if (null === $centerHistory->getEndDate()) { + $centerHistory->setEndDate($modification); + } + } + + $this->centerHistory[] = $new = new PersonCenterHistory($this, $center, $modification); + $this->centerCurrent = new PersonCenterCurrent($new); + return $this; } /** - * @return Report + * @return Collection */ - public function setCFData(?array $cFData) + public function getCenterHistory(): Collection + { + return $this->centerHistory; + } + + /** + * @param Collection $centerHistory + * @return Person + */ + public function setCenterHistory(Collection $centerHistory): Person + { + $this->centerHistory = $centerHistory; + return $this; + } + + /** + * @return PersonCenterCurrent|null + */ + public function getCenterCurrent(): ?PersonCenterCurrent + { + return $this->centerCurrent; + } + + /** + * @return Person + */ + public function setCFData(?array $cFData): self { $this->cFData = $cFData; diff --git a/src/Bundle/ChillPersonBundle/Entity/Person/PersonCenterCurrent.php b/src/Bundle/ChillPersonBundle/Entity/Person/PersonCenterCurrent.php new file mode 100644 index 000000000..690ff025e --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Entity/Person/PersonCenterCurrent.php @@ -0,0 +1,112 @@ +person = $history->getPerson(); + $this->center = $history->getCenter(); + $this->startDate = $history->getStartDate(); + $this->endDate = $history->getEndDate(); + $this->id = $history->getId(); + } + + /** + * The id will be the same as the current @link{PersonCenterHistory::class} + * + * @return int + */ + public function getId(): int + { + return $this->id; + } + + /** + * @return Person + */ + public function getPerson(): Person + { + return $this->person; + } + + /** + * @return Center + */ + public function getCenter(): Center + { + return $this->center; + } + + /** + * @return \DateTimeImmutable + */ + public function getStartDate(): \DateTimeImmutable + { + return $this->startDate; + } + + /** + * @return \DateTimeImmutable|null + */ + public function getEndDate(): ?\DateTimeImmutable + { + return $this->endDate; + } + +} diff --git a/src/Bundle/ChillPersonBundle/Entity/Person/PersonCenterHistory.php b/src/Bundle/ChillPersonBundle/Entity/Person/PersonCenterHistory.php new file mode 100644 index 000000000..6bfb28cc6 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Entity/Person/PersonCenterHistory.php @@ -0,0 +1,142 @@ +person = $person; + $this->center = $center; + $this->startDate = $startDate; + } + + + /** + * @return int|null + */ + public function getId(): ?int + { + return $this->id; + } + + /** + * @return Person|null + */ + public function getPerson(): ?Person + { + return $this->person; + } + + /** + * @param Person|null $person + */ + public function setPerson(?Person $person): self + { + $this->person = $person; + + return $this; + } + + /** + * @return Center|null + */ + public function getCenter(): ?Center + { + return $this->center; + } + + /** + * @param Center|null $center + */ + public function setCenter(?Center $center): self + { + $this->center = $center; + + return $this; + } + + /** + * @return \DateTimeImmutable|null + */ + public function getStartDate(): ?\DateTimeImmutable + { + return $this->startDate; + } + + /** + * @param \DateTimeImmutable|null $startDate + */ + public function setStartDate(?\DateTimeImmutable $startDate): self + { + $this->startDate = $startDate; + return $this; + } + + /** + * @return \DateTimeImmutable|null + */ + public function getEndDate(): ?\DateTimeImmutable + { + return $this->endDate; + } + + /** + * @param \DateTimeImmutable|null $endDate + */ + public function setEndDate(?\DateTimeImmutable $endDate): self + { + $this->endDate = $endDate; + + return $this; + } +} diff --git a/src/Bundle/ChillPersonBundle/Repository/Person/PersonCenterHistoryInterface.php b/src/Bundle/ChillPersonBundle/Repository/Person/PersonCenterHistoryInterface.php new file mode 100644 index 000000000..089add06a --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Repository/Person/PersonCenterHistoryInterface.php @@ -0,0 +1,10 @@ +repository = $em->getRepository($this->getClassName()); + } + + public function find($id): ?PersonCenterHistory + { + return $this->repository->find($id); + } + + /** + * @return array|PersonCenterHistory[] + */ + public function findAll(): array + { + return $this->repository->findAll(); + } + + /** + * @return array|PersonCenterHistory[] + */ + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria): ?PersonCenterHistory + { + return $this->repository->findOneBy($criteria); + } + + public function getClassName(): string + { + return PersonCenterHistory::class; + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Entity/PersonTest.php b/src/Bundle/ChillPersonBundle/Tests/Entity/PersonTest.php index 18149c824..4633c3fcb 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Entity/PersonTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Entity/PersonTest.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Tests\Entity; +use Chill\MainBundle\Entity\Center; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Household\HouseholdMember; @@ -191,4 +192,29 @@ final class PersonTest extends \PHPUnit\Framework\TestCase $this->assertEquals($r['result'], Person::ERROR_ADDIND_PERIOD_AFTER_AN_OPEN_PERIOD); } + + public function testSetCenter() + { + $person = new Person(); + $centerA = new Center(); + $centerB = new Center(); + + $this->assertCount(0, $person->getCenterHistory()); + $this->assertNull($person->getCenter()); + $this->assertNull($person->getCenterCurrent()); + + $person->setCenter($centerA); + + $this->assertCount(1, $person->getCenterHistory()); + $this->assertSame($centerA, $person->getCenter()); + $this->assertInstanceOf(Person\PersonCenterCurrent::class, $person->getCenterCurrent()); + $this->assertSame($centerA, $person->getCenterCurrent()->getCenter()); + + $person->setCenter($centerB); + + $this->assertCount(2, $person->getCenterHistory()); + $this->assertSame($centerB, $person->getCenter()); + $this->assertInstanceOf(Person\PersonCenterCurrent::class, $person->getCenterCurrent()); + $this->assertSame($centerB, $person->getCenterCurrent()->getCenter()); + } } diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20220926154347.php b/src/Bundle/ChillPersonBundle/migrations/Version20220926154347.php new file mode 100644 index 000000000..b308c22e3 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20220926154347.php @@ -0,0 +1,69 @@ +addSql('CREATE SEQUENCE chill_person_person_center_history_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_person_person_center_history ( + id INT NOT NULL, person_id INT DEFAULT NULL, center_id INT DEFAULT NULL, + startDate DATE NOT NULL, endDate DATE DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, + updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, + updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_CACA67FA217BBB47 ON chill_person_person_center_history (person_id)'); + $this->addSql('CREATE INDEX IDX_CACA67FA5932F377 ON chill_person_person_center_history (center_id)'); + $this->addSql('CREATE INDEX IDX_CACA67FA3174800F ON chill_person_person_center_history (createdBy_id)'); + $this->addSql('CREATE INDEX IDX_CACA67FA65FF1AEC ON chill_person_person_center_history (updatedBy_id)'); + $this->addSql('COMMENT ON COLUMN chill_person_person_center_history.startDate IS \'(DC2Type:date_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_person_person_center_history.endDate IS \'(DC2Type:date_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_person_person_center_history.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_person_person_center_history.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_person_person_center_history ADD CONSTRAINT FK_CACA67FA217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_person_person_center_history ADD CONSTRAINT FK_CACA67FA5932F377 FOREIGN KEY (center_id) REFERENCES centers (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_person_person_center_history ADD CONSTRAINT FK_CACA67FA3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_person_person_center_history ADD CONSTRAINT FK_CACA67FA65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + + // check consistency of history on database side + $this->addSql('ALTER TABLE chill_person_person_center_history ADD CHECK (startdate <= enddate)'); + $this->addSql('ALTER TABLE chill_person_person_center_history ADD CONSTRAINT ' . + 'chill_internal_person_person_center_history_not_overlaps EXCLUDE USING GIST( + -- extension btree_gist required to include comparaison with integer + person_id WITH =, + daterange(startdate, enddate, \'[)\') WITH && + ) + INITIALLY DEFERRED'); + + // create index on search by person and date + $this->addSql('CREATE INDEX chill_internal_person_person_center_history_by_date ON chill_person_person_center_history (person_id DESC, startdate DESC, enddate DESC NULLS FIRST)'); + + // create a view to get the current center on a person + $this->addSql( + 'CREATE VIEW view_chill_person_person_center_history_current AS + SELECT id, person_id, center_id, startDate, endDate, createdAt, updatedAt, createdBy_id, updatedBy_id + FROM chill_person_person_center_history WHERE startDate <= NOW() AND (enddate IS NULL OR enddate > NOW())' + ); + + $this->addSql('INSERT INTO chill_person_person_center_history (id, person_id, center_id, startdate) + SELECT nextval(\'chill_person_person_center_history_id_seq\'), id, center_id, COALESCE(createdat, NOW()) + FROM chill_person_person WHERE center_id IS NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP VIEW view_chill_person_person_center_history_current'); + $this->addSql('DROP SEQUENCE chill_person_person_center_history_id_seq CASCADE'); + $this->addSql('DROP TABLE chill_person_person_center_history'); + } +}