From dd48795f6470edc2824407ada83c0f3300377552 Mon Sep 17 00:00:00 2001 From: nobohan Date: Wed, 14 Apr 2021 14:49:19 +0200 Subject: [PATCH 01/42] #23 enable postgis in a chil main migration --- .../migrations/Version20210414091001.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20210414091001.php diff --git a/src/Bundle/ChillMainBundle/migrations/Version20210414091001.php b/src/Bundle/ChillMainBundle/migrations/Version20210414091001.php new file mode 100644 index 000000000..7881d9558 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20210414091001.php @@ -0,0 +1,32 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('CREATE EXTENSION IF NOT EXISTS postgis;'); + + } + + public function down(Schema $schema) : void + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('DROP EXTENSION IF NOT EXISTS postgis;'); + } + + public function getDescription(): string + { + return "Enable the postgis extension in public schema"; + } +} From ebd58d422985aee19f183c5e86d08e7ad2e23bd5 Mon Sep 17 00:00:00 2001 From: nobohan Date: Thu, 15 Apr 2021 10:46:29 +0200 Subject: [PATCH 02/42] add more fields to Address entity + rename streetAddress1 and streetAddress2 --- src/Bundle/ChillMainBundle/Entity/Address.php | 235 +++++++++++++++--- 1 file changed, 207 insertions(+), 28 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Entity/Address.php b/src/Bundle/ChillMainBundle/Entity/Address.php index 69c39c7b6..1286ba2e0 100644 --- a/src/Bundle/ChillMainBundle/Entity/Address.php +++ b/src/Bundle/ChillMainBundle/Entity/Address.php @@ -28,14 +28,14 @@ class Address * * @ORM\Column(type="string", length=255) */ - private $streetAddress1 = ''; + private $street = ''; /** * @var string * * @ORM\Column(type="string", length=255) */ - private $streetAddress2 = ''; + private $number = ''; /** * @var PostalCode @@ -43,7 +43,56 @@ class Address * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode") */ private $postcode; - + + /** + * @var string + * + * @ORM\Column(type="string", length=16, nullable=true) + */ + private $floor; + + /** + * @var string + * + * @ORM\Column(type="string", length=16, nullable=true) + */ + private $corridor; + + /** + * @var string + * + * @ORM\Column(type="string", length=16, nullable=true) + */ + private $steps; + + /** + * @var string + * + * @ORM\Column(type="string", length=255, nullable=true) + */ + private $buildingName; + + /** + * @var string + * + * @ORM\Column(type="string", length=16, nullable=true) + */ + private $flat; + + /** + * @var string + * + * @ORM\Column(type="string", length=255, nullable=true) + */ + private $distribution; + + /** + * @var string + * + * @ORM\Column(type="string", length=255, nullable=true) + */ + private $extra; + /** * Indicates when the address starts validation. Used to build an history * of address. By default, the current date. @@ -53,21 +102,31 @@ class Address * @ORM\Column(type="date") */ private $validFrom; - + + /** + * Indicates when the address ends. Used to build an history + * of address. + * + * @var \DateTime + * + * @ORM\Column(type="date") + */ + private $validTo; + /** * True if the address is a "no address", aka homeless person, ... * * @var bool */ private $isNoAddress = false; - + /** * A list of metadata, added by customizable fields - * + * * @var array */ private $customs = []; - + public function __construct() { $this->validFrom = new \DateTime(); @@ -105,7 +164,7 @@ class Address */ public function getStreetAddress1() { - return $this->streetAddress1; + return $this->street; } /** @@ -129,7 +188,7 @@ class Address */ public function getStreetAddress2() { - return $this->streetAddress2; + return $this->number; } /** @@ -155,7 +214,7 @@ class Address { return $this->postcode; } - + /** * @return \DateTime */ @@ -173,19 +232,19 @@ class Address $this->validFrom = $validFrom; return $this; } - + /** * Get IsNoAddress - * + * * Indicate true if the address is a fake address (homeless, ...) - * + * * @return bool */ public function getIsNoAddress(): bool { return $this->isNoAddress; } - + /** * @return bool */ @@ -196,9 +255,9 @@ class Address /** * Set IsNoAddress - * + * * Indicate true if the address is a fake address (homeless, ...) - * + * * @param bool $isNoAddress * @return $this */ @@ -207,10 +266,10 @@ class Address $this->isNoAddress = $isNoAddress; return $this; } - + /** * Get customs informations in the address - * + * * @return array */ public function getCustoms(): array @@ -220,27 +279,27 @@ class Address /** * Store custom informations in the address - * + * * @param array $customs * @return $this */ public function setCustoms(array $customs): self { $this->customs = $customs; - + return $this; } - + /** * Validate the address. - * + * * Check that: - * + * * * if the address is not home address: * * the postal code is present * * the valid from is not null * * the address street 1 is greater than 2 - * + * * @param ExecutionContextInterface $context * @param array $payload */ @@ -252,18 +311,18 @@ class Address ->atPath('validFrom') ->addViolation(); } - + if ($this->isNoAddress()) { return; } - + if (empty($this->getStreetAddress1())) { $context ->buildViolation("address.street1-should-be-set") ->atPath('streetAddress1') ->addViolation(); } - + if (!$this->getPostcode() instanceof PostalCode) { $context ->buildViolation("address.postcode-should-be-set") @@ -271,7 +330,7 @@ class Address ->addViolation(); } } - + /** * @param Address $original * @return Address @@ -286,5 +345,125 @@ class Address ; } + public function getStreet(): ?string + { + return $this->street; + } + + public function setStreet(string $street): self + { + $this->street = $street; + + return $this; + } + + public function getNumber(): ?string + { + return $this->number; + } + + public function setNumber(string $number): self + { + $this->number = $number; + + return $this; + } + + public function getFloor(): ?string + { + return $this->floor; + } + + public function setFloor(?string $floor): self + { + $this->floor = $floor; + + return $this; + } + + public function getCorridor(): ?string + { + return $this->corridor; + } + + public function setCorridor(?string $corridor): self + { + $this->corridor = $corridor; + + return $this; + } + + public function getSteps(): ?string + { + return $this->steps; + } + + public function setSteps(?string $steps): self + { + $this->steps = $steps; + + return $this; + } + + public function getBuildingName(): ?string + { + return $this->buildingName; + } + + public function setBuildingName(?string $buildingName): self + { + $this->buildingName = $buildingName; + + return $this; + } + + public function getFlat(): ?string + { + return $this->flat; + } + + public function setFlat(?string $flat): self + { + $this->flat = $flat; + + return $this; + } + + public function getDistribution(): ?string + { + return $this->distribution; + } + + public function setDistribution(?string $distribution): self + { + $this->distribution = $distribution; + + return $this; + } + + public function getExtra(): ?string + { + return $this->extra; + } + + public function setExtra(?string $extra): self + { + $this->extra = $extra; + + return $this; + } + + public function getValidTo(): ?\DateTimeInterface + { + return $this->validTo; + } + + public function setValidTo(\DateTimeInterface $validTo): self + { + $this->validTo = $validTo; + + return $this; + } + } From 7d1a1c4004b7d4a6a6489be959a89ae3be91ee5c Mon Sep 17 00:00:00 2001 From: nobohan Date: Mon, 19 Apr 2021 12:12:55 +0200 Subject: [PATCH 03/42] #32 add point type + Address: add Point field + add null on nullable fields --- .../ChillMainExtension.php | 3 +- .../Doctrine/Type/PointType.php | 72 ++++++++++++++ src/Bundle/ChillMainBundle/Entity/Address.php | 23 +++-- src/Bundle/ChillMainBundle/Entity/Point.php | 96 +++++++++++++++++++ .../ChillMainBundle/Entity/PointException.php | 28 ++++++ 5 files changed, 214 insertions(+), 8 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Doctrine/Type/PointType.php create mode 100644 src/Bundle/ChillMainBundle/Entity/Point.php create mode 100644 src/Bundle/ChillMainBundle/Entity/PointException.php diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 234e1203d..ee9608d82 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -188,7 +188,8 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface, $container->prependExtensionConfig('doctrine', array( 'dbal' => [ 'types' => [ - 'dateinterval' => \Chill\MainBundle\Doctrine\Type\NativeDateIntervalType::class + 'dateinterval' => \Chill\MainBundle\Doctrine\Type\NativeDateIntervalType::class, + 'json' => \Chill\MainBundle\Doctrine\Type\PointType::class ] ] )); diff --git a/src/Bundle/ChillMainBundle/Doctrine/Type/PointType.php b/src/Bundle/ChillMainBundle/Doctrine/Type/PointType.php new file mode 100644 index 000000000..ba84177ac --- /dev/null +++ b/src/Bundle/ChillMainBundle/Doctrine/Type/PointType.php @@ -0,0 +1,72 @@ +toWKT(); + } + + public function canRequireSQLConversion() + { + return true; + } + + public function convertToPHPValueSQL($sqlExpr, $platform) + { + return 'ST_AsGeoJSON('.$sqlExpr.') '; + } + + public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform) + { + return $sqlExpr; + } + + + + + +} + diff --git a/src/Bundle/ChillMainBundle/Entity/Address.php b/src/Bundle/ChillMainBundle/Entity/Address.php index 1286ba2e0..6592c6a3d 100644 --- a/src/Bundle/ChillMainBundle/Entity/Address.php +++ b/src/Bundle/ChillMainBundle/Entity/Address.php @@ -45,49 +45,49 @@ class Address private $postcode; /** - * @var string + * @var string|null * * @ORM\Column(type="string", length=16, nullable=true) */ private $floor; /** - * @var string + * @var string|null * * @ORM\Column(type="string", length=16, nullable=true) */ private $corridor; /** - * @var string + * @var string|null * * @ORM\Column(type="string", length=16, nullable=true) */ private $steps; /** - * @var string + * @var string|null * * @ORM\Column(type="string", length=255, nullable=true) */ private $buildingName; /** - * @var string + * @var string|null * * @ORM\Column(type="string", length=16, nullable=true) */ private $flat; /** - * @var string + * @var string|null * * @ORM\Column(type="string", length=255, nullable=true) */ private $distribution; /** - * @var string + * @var string|null * * @ORM\Column(type="string", length=255, nullable=true) */ @@ -120,6 +120,15 @@ class Address */ private $isNoAddress = false; + /** + * A geospatial field storing the coordinates of the Address + * + * @var Point|null + * + * @ORM\Column(type="Point", nullable=true) + */ + private $point; + /** * A list of metadata, added by customizable fields * diff --git a/src/Bundle/ChillMainBundle/Entity/Point.php b/src/Bundle/ChillMainBundle/Entity/Point.php new file mode 100644 index 000000000..528834e42 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/Point.php @@ -0,0 +1,96 @@ +lat = $lat; + $this->lon = $lon; + } + + public function toGeoJson() { + $array = $this->toArrayGeoJson(); + return \json_encode($array); + } + + public function jsonSerialize() { + return $this->toArrayGeoJson(); + } + + public function toArrayGeoJson() { + return array("type" => "Point", + "coordinates" => array ($this->lon, $this->lat)); + } + + /** + * + * @return string + */ + public function toWKT() { + return 'SRID='.self::$SRID.';POINT('.$this->lon.' '.$this->lat.')'; + } + + /** + * + * @param type $geojson + * @return Point + */ + public static function fromGeoJson($geojson) + { + $a = json_decode($geojson); + //check if the geojson string is correct + if ($a == null or !isset($a->type) or !isset($a->coordinates)){ + throw PointException::badJsonString($geojson); + } + + if ($a->type != "Point"){ + throw PointException::badGeoType(); + } else { + $lat = $a->coordinates[1]; + $lon = $a->coordinates[0]; + return Point::fromLonLat($lon, $lat); + } + + } + + public static function fromLonLat($lon, $lat) + { + if (($lon > -180 && $lon < 180) && ($lat > -90 && $lat < 90)) + { + return new Point($lon, $lat); + } else { + throw PointException::badCoordinates($lon, $lat); + } + } + + public static function fromArrayGeoJson($array) + { + if ($array['type'] == 'Point' && + isset($array['coordinates'])) + { + return self::fromLonLat($array['coordinates'][0], $array['coordinates'][1]); + } + } + + public function getLat() + { + return $this->lat; + } + + public function getLon() + { + return $this->lon; + } +} + + diff --git a/src/Bundle/ChillMainBundle/Entity/PointException.php b/src/Bundle/ChillMainBundle/Entity/PointException.php new file mode 100644 index 000000000..ac024eebf --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/PointException.php @@ -0,0 +1,28 @@ + Date: Tue, 20 Apr 2021 10:55:45 +0200 Subject: [PATCH 04/42] add point type + Address: add Point field : fix dependency injection of the Point type --- .../DependencyInjection/ChillMainExtension.php | 8 ++++++-- .../{Entity => Doctrine/Model}/Point.php | 2 +- .../{Entity => Doctrine/Model}/PointException.php | 2 +- .../ChillMainBundle/Doctrine/Type/PointType.php | 4 ++-- src/Bundle/ChillMainBundle/Entity/Address.php | 15 ++++++++++++++- 5 files changed, 24 insertions(+), 7 deletions(-) rename src/Bundle/ChillMainBundle/{Entity => Doctrine/Model}/Point.php (97%) rename src/Bundle/ChillMainBundle/{Entity => Doctrine/Model}/PointException.php (93%) diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index ee9608d82..831853611 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -188,8 +188,12 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface, $container->prependExtensionConfig('doctrine', array( 'dbal' => [ 'types' => [ - 'dateinterval' => \Chill\MainBundle\Doctrine\Type\NativeDateIntervalType::class, - 'json' => \Chill\MainBundle\Doctrine\Type\PointType::class + 'dateinterval' => [ + 'class' => \Chill\MainBundle\Doctrine\Type\NativeDateIntervalType::class + ], + 'point' => [ + 'class' => \Chill\MainBundle\Doctrine\Type\PointType::class + ] ] ] )); diff --git a/src/Bundle/ChillMainBundle/Entity/Point.php b/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php similarity index 97% rename from src/Bundle/ChillMainBundle/Entity/Point.php rename to src/Bundle/ChillMainBundle/Doctrine/Model/Point.php index 528834e42..869c4c3a6 100644 --- a/src/Bundle/ChillMainBundle/Entity/Point.php +++ b/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php @@ -1,6 +1,6 @@ point; + } + + public function setPoint(?Point $point): self + { + $this->point = $point; + + return $this; + } + } From 0b2f29f1e86996bb7a1f7b8614820b08053fb4cb Mon Sep 17 00:00:00 2001 From: nobohan Date: Tue, 20 Apr 2021 16:12:17 +0200 Subject: [PATCH 05/42] add migration for address --- src/Bundle/ChillMainBundle/Entity/Address.php | 14 +++--- .../migrations/Version20210420115006.php | 50 +++++++++++++++++++ 2 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20210420115006.php diff --git a/src/Bundle/ChillMainBundle/Entity/Address.php b/src/Bundle/ChillMainBundle/Entity/Address.php index 8a8761ca5..43512a5b2 100644 --- a/src/Bundle/ChillMainBundle/Entity/Address.php +++ b/src/Bundle/ChillMainBundle/Entity/Address.php @@ -36,7 +36,7 @@ class Address * * @ORM\Column(type="string", length=255) */ - private $number = ''; + private $streetNumber = ''; /** * @var PostalCode @@ -108,9 +108,9 @@ class Address * Indicates when the address ends. Used to build an history * of address. * - * @var \DateTime + * @var \DateTime|null * - * @ORM\Column(type="date") + * @ORM\Column(type="date", nullable=true) */ private $validTo; @@ -198,7 +198,7 @@ class Address */ public function getStreetAddress2() { - return $this->number; + return $this->streetNumber; } /** @@ -369,12 +369,12 @@ class Address public function getNumber(): ?string { - return $this->number; + return $this->streetNumber; } - public function setNumber(string $number): self + public function setNumber(string $streetNumber): self { - $this->number = $number; + $this->streetNumber = $streetNumber; return $this; } diff --git a/src/Bundle/ChillMainBundle/migrations/Version20210420115006.php b/src/Bundle/ChillMainBundle/migrations/Version20210420115006.php new file mode 100644 index 000000000..79dea4853 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20210420115006.php @@ -0,0 +1,50 @@ +addSql('ALTER TABLE chill_main_address RENAME COLUMN streetaddress1 TO street;'); + $this->addSql('ALTER TABLE chill_main_address RENAME COLUMN streetaddress2 TO streetNumber;'); + $this->addSql('ALTER TABLE chill_main_address ADD floor VARCHAR(16) DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_address ADD corridor VARCHAR(16) DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_address ADD steps VARCHAR(16) DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_address ADD buildingName VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_address ADD flat VARCHAR(16) DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_address ADD distribution VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_address ADD extra VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_address ADD validTo DATE DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_address ADD point geometry(POINT,4326) DEFAULT NULL'); + } + + + public function down(Schema $schema) : void + { + $this->addSql('ALTER TABLE chill_main_address RENAME COLUMN street TO streetaddress1;'); + $this->addSql('ALTER TABLE chill_main_address RENAME COLUMN streetNumber TO streetaddress2;'); + $this->addSql('ALTER TABLE chill_main_address DROP floor'); + $this->addSql('ALTER TABLE chill_main_address DROP corridor'); + $this->addSql('ALTER TABLE chill_main_address DROP steps'); + $this->addSql('ALTER TABLE chill_main_address DROP buildingName'); + $this->addSql('ALTER TABLE chill_main_address DROP flat'); + $this->addSql('ALTER TABLE chill_main_address DROP distribution'); + $this->addSql('ALTER TABLE chill_main_address DROP extra'); + $this->addSql('ALTER TABLE chill_main_address DROP validTo'); + $this->addSql('ALTER TABLE chill_main_address DROP point'); + } +} From c089960707fd92b4f2a26573a33cc0841a598745 Mon Sep 17 00:00:00 2001 From: nobohan Date: Thu, 22 Apr 2021 09:32:46 +0200 Subject: [PATCH 06/42] add type hinting in Point and PointException --- .../ChillMainBundle/Doctrine/Model/Point.php | 31 +++++++++++-------- .../Doctrine/Model/PointException.php | 7 ++--- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php b/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php index 869c4c3a6..a69d3282d 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php +++ b/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php @@ -9,25 +9,29 @@ use \JsonSerializable; * */ class Point implements JsonSerializable { - private $lat; - private $lon; - public static $SRID = '4326'; + private float $lat; + private float $lon; + public static string $SRID = '4326'; - private function __construct($lon, $lat) { + private function __construct($lon, $lat) + { $this->lat = $lat; $this->lon = $lon; } - public function toGeoJson() { + public function toGeoJson(): string + { $array = $this->toArrayGeoJson(); return \json_encode($array); } - public function jsonSerialize() { + public function jsonSerialize(): array + { return $this->toArrayGeoJson(); } - public function toArrayGeoJson() { + public function toArrayGeoJson(): array + { return array("type" => "Point", "coordinates" => array ($this->lon, $this->lat)); } @@ -36,7 +40,8 @@ class Point implements JsonSerializable { * * @return string */ - public function toWKT() { + public function toWKT(): string + { return 'SRID='.self::$SRID.';POINT('.$this->lon.' '.$this->lat.')'; } @@ -45,7 +50,7 @@ class Point implements JsonSerializable { * @param type $geojson * @return Point */ - public static function fromGeoJson($geojson) + public static function fromGeoJson($geojson): Point { $a = json_decode($geojson); //check if the geojson string is correct @@ -63,7 +68,7 @@ class Point implements JsonSerializable { } - public static function fromLonLat($lon, $lat) + public static function fromLonLat($lon, $lat): Point { if (($lon > -180 && $lon < 180) && ($lat > -90 && $lat < 90)) { @@ -73,7 +78,7 @@ class Point implements JsonSerializable { } } - public static function fromArrayGeoJson($array) + public static function fromArrayGeoJson($array): Point { if ($array['type'] == 'Point' && isset($array['coordinates'])) @@ -82,12 +87,12 @@ class Point implements JsonSerializable { } } - public function getLat() + public function getLat(): float { return $this->lat; } - public function getLon() + public function getLon(): float { return $this->lon; } diff --git a/src/Bundle/ChillMainBundle/Doctrine/Model/PointException.php b/src/Bundle/ChillMainBundle/Doctrine/Model/PointException.php index 64a3c4c1f..48d6b9aea 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/Model/PointException.php +++ b/src/Bundle/ChillMainBundle/Doctrine/Model/PointException.php @@ -10,18 +10,17 @@ use \Exception; */ class PointException extends Exception { - public static function badCoordinates($lon, $lat) + public static function badCoordinates($lon, $lat): self { return new self("les coordonnées fournies sont invalides dans le système de projection utilisé (longitude = $lon , latitude = $lat)"); } - public static function badJsonString($str) + public static function badJsonString($str): self { return new self("la chaine JSON n'est pas valide : $str"); - } - public static function badGeoType() + public static function badGeoType(): self { return new self("le type de l'objet GeoJSON est invalide"); } From 7c99f0b3e0a3bfb4b7455c8b270b37b7867bfe72 Mon Sep 17 00:00:00 2001 From: juminet Date: Thu, 22 Apr 2021 07:41:36 +0000 Subject: [PATCH 07/42] array synatx: array() -> [] --- src/Bundle/ChillMainBundle/Doctrine/Model/Point.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php b/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php index a69d3282d..3ef5fe768 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php +++ b/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php @@ -32,8 +32,10 @@ class Point implements JsonSerializable { public function toArrayGeoJson(): array { - return array("type" => "Point", - "coordinates" => array ($this->lon, $this->lat)); + return [ + "type" => "Point", + "coordinates" => [ $this->lon, $this->lat ] + ]; } /** From e90ea3168367c8f3e2ba0e3f73f51a8024da808d Mon Sep 17 00:00:00 2001 From: nobohan Date: Thu, 22 Apr 2021 09:45:12 +0200 Subject: [PATCH 08/42] add type hinting in Point and PointException --- src/Bundle/ChillMainBundle/Doctrine/Model/Point.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php b/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php index 3ef5fe768..d987637ce 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php +++ b/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php @@ -13,7 +13,7 @@ class Point implements JsonSerializable { private float $lon; public static string $SRID = '4326'; - private function __construct($lon, $lat) + private function __construct(float $lon, float $lat) { $this->lat = $lat; $this->lon = $lon; @@ -52,7 +52,7 @@ class Point implements JsonSerializable { * @param type $geojson * @return Point */ - public static function fromGeoJson($geojson): Point + public static function fromGeoJson(string $geojson): Point { $a = json_decode($geojson); //check if the geojson string is correct @@ -70,7 +70,7 @@ class Point implements JsonSerializable { } - public static function fromLonLat($lon, $lat): Point + public static function fromLonLat(float $lon, float $lat): Point { if (($lon > -180 && $lon < 180) && ($lat > -90 && $lat < 90)) { @@ -80,7 +80,7 @@ class Point implements JsonSerializable { } } - public static function fromArrayGeoJson($array): Point + public static function fromArrayGeoJson(array $array): Point { if ($array['type'] == 'Point' && isset($array['coordinates'])) From bc4e29141b1535bdf631bb9d7a6f2e04504b51f1 Mon Sep 17 00:00:00 2001 From: juminet Date: Thu, 22 Apr 2021 07:46:22 +0000 Subject: [PATCH 09/42] syntax (null -> NULL and == -> ===) --- src/Bundle/ChillMainBundle/Doctrine/Model/Point.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php b/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php index d987637ce..67cf434c0 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php +++ b/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php @@ -56,7 +56,7 @@ class Point implements JsonSerializable { { $a = json_decode($geojson); //check if the geojson string is correct - if ($a == null or !isset($a->type) or !isset($a->coordinates)){ + if (NULL === $a or !isset($a->type) or !isset($a->coordinates)){ throw PointException::badJsonString($geojson); } From 1ec2fbcc161c942a51bb67c8dd57f8e6cbe89610 Mon Sep 17 00:00:00 2001 From: nobohan Date: Thu, 22 Apr 2021 09:49:02 +0200 Subject: [PATCH 10/42] translated exception messages in English --- .../ChillMainBundle/Doctrine/Model/PointException.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Doctrine/Model/PointException.php b/src/Bundle/ChillMainBundle/Doctrine/Model/PointException.php index 48d6b9aea..4e3101435 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/Model/PointException.php +++ b/src/Bundle/ChillMainBundle/Doctrine/Model/PointException.php @@ -12,16 +12,16 @@ class PointException extends Exception { public static function badCoordinates($lon, $lat): self { - return new self("les coordonnées fournies sont invalides dans le système de projection utilisé (longitude = $lon , latitude = $lat)"); + return new self("Input coordinates are not valid in the used coordinate system (longitude = $lon , latitude = $lat)"); } public static function badJsonString($str): self { - return new self("la chaine JSON n'est pas valide : $str"); + return new self("The JSON string is not valid: $str"); } public static function badGeoType(): self { - return new self("le type de l'objet GeoJSON est invalide"); + return new self("The geoJSON object type is not valid"); } } From 8c33d876e80f61a23c3d2c6f46a7710a3749162b Mon Sep 17 00:00:00 2001 From: juminet Date: Thu, 22 Apr 2021 07:53:05 +0000 Subject: [PATCH 11/42] code formatting in PointType --- src/Bundle/ChillMainBundle/Doctrine/Type/PointType.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Doctrine/Type/PointType.php b/src/Bundle/ChillMainBundle/Doctrine/Type/PointType.php index eab3261dd..6b6f7c113 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/Type/PointType.php +++ b/src/Bundle/ChillMainBundle/Doctrine/Type/PointType.php @@ -63,10 +63,5 @@ class PointType extends Type { { return $sqlExpr; } - - - - - } From 69a3c6a9b2bcf7fdfbb5e85eb787c4e52bcaf0b9 Mon Sep 17 00:00:00 2001 From: nobohan Date: Thu, 22 Apr 2021 09:59:46 +0200 Subject: [PATCH 12/42] refine the migration comment --- src/Bundle/ChillMainBundle/migrations/Version20210414091001.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillMainBundle/migrations/Version20210414091001.php b/src/Bundle/ChillMainBundle/migrations/Version20210414091001.php index 7881d9558..db207c594 100644 --- a/src/Bundle/ChillMainBundle/migrations/Version20210414091001.php +++ b/src/Bundle/ChillMainBundle/migrations/Version20210414091001.php @@ -6,7 +6,7 @@ use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; /** - * Auto-generated Migration: Please modify to your needs! + * Create the postgis extension */ final class Version20210414091001 extends AbstractMigration { From a709b3afb60255ec36aca890d4101e0465e71f6e Mon Sep 17 00:00:00 2001 From: nobohan Date: Thu, 22 Apr 2021 12:25:34 +0200 Subject: [PATCH 13/42] add addressReference in Chill Main + migration --- .../Entity/AddressReference.php | 147 ++++++++++++++++++ .../Repository/AddressReferenceRepository.php | 50 ++++++ .../migrations/Version20210422101743.php | 33 ++++ 3 files changed, 230 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/Entity/AddressReference.php create mode 100644 src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20210422101743.php diff --git a/src/Bundle/ChillMainBundle/Entity/AddressReference.php b/src/Bundle/ChillMainBundle/Entity/AddressReference.php new file mode 100644 index 000000000..8f1678425 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/AddressReference.php @@ -0,0 +1,147 @@ +id; + } + + public function getRefId(): ?string + { + return $this->refId; + } + + public function setRefId(string $refId): self + { + $this->refId = $refId; + + return $this; + } + + public function getStreet(): ?string + { + return $this->street; + } + + public function setStreet(?string $street): self + { + $this->street = $street; + + return $this; + } + + public function getStreetNumber(): ?string + { + return $this->streetNumber; + } + + public function setStreetNumber(?string $streetNumber): self + { + $this->streetNumber = $streetNumber; + + return $this; + } + + public function getPostcode(): ?string + { + return $this->postcode; + } + + public function setPostcode(?string $postcode): self + { + $this->postcode = $postcode; + + return $this; + } + + public function getMunicipalityCode(): ?string + { + return $this->municipalityCode; + } + + public function setMunicipalityCode(?string $municipalityCode): self + { + $this->municipalityCode = $municipalityCode; + + return $this; + } + + public function getSource(): ?string + { + return $this->source; + } + + public function setSource(?string $source): self + { + $this->source = $source; + + return $this; + } + + public function getPoint(): ?string + { + return $this->point; + } + + public function setPoint(string $point): self + { + $this->point = $point; + + return $this; + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php b/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php new file mode 100644 index 000000000..208151420 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php @@ -0,0 +1,50 @@ +createQueryBuilder('a') + ->andWhere('a.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('a.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?AddressReference + { + return $this->createQueryBuilder('a') + ->andWhere('a.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Bundle/ChillMainBundle/migrations/Version20210422101743.php b/src/Bundle/ChillMainBundle/migrations/Version20210422101743.php new file mode 100644 index 000000000..2d4341155 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20210422101743.php @@ -0,0 +1,33 @@ +addSql('CREATE SEQUENCE chill_main_address_reference_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_main_address_reference (id INT NOT NULL, postcode_id INT DEFAULT NULL, refId VARCHAR(255) NOT NULL, street VARCHAR(255) DEFAULT NULL, streetNumber VARCHAR(255) DEFAULT NULL, municipalityCode VARCHAR(255) DEFAULT NULL, source VARCHAR(255) DEFAULT NULL, point VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_CA6C1BD7EECBFDF1 ON chill_main_address_reference (postcode_id)'); + $this->addSql('ALTER TABLE chill_main_address_reference ADD CONSTRAINT FK_CA6C1BD7EECBFDF1 FOREIGN KEY (postcode_id) REFERENCES chill_main_postal_code (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema) : void + { + $this->addSql('DROP SEQUENCE chill_main_address_reference_id_seq CASCADE'); + $this->addSql('DROP TABLE chill_main_address_reference'); + } +} From ebff36d2571347fca776cdf704b090d559583cac Mon Sep 17 00:00:00 2001 From: nobohan Date: Fri, 23 Apr 2021 15:13:04 +0200 Subject: [PATCH 14/42] add unit test for class Point --- .../Tests/Doctrine/Model/PointTest.php | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/Tests/Doctrine/Model/PointTest.php diff --git a/src/Bundle/ChillMainBundle/Tests/Doctrine/Model/PointTest.php b/src/Bundle/ChillMainBundle/Tests/Doctrine/Model/PointTest.php new file mode 100644 index 000000000..5632bf96a --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Doctrine/Model/PointTest.php @@ -0,0 +1,119 @@ + + */ +class ExportControllerTest extends KernelTestCase +{ + + public function testToWKT() + { + $lon = 4.8634; + $lat = 50.47382; + $point = $this->preparePoint($lon, $lat); + + $this->assertEquals($point->toWKT(),'SRID=4326;POINT(4.8634 50.47382)'); + } + + public function testToGeojson() + { + $lon = 4.8634; + $lat = 50.47382; + $point = $this->preparePoint($lon, $lat); + + $this->assertEquals($point->toGeoJson(),'{"type":"Point","coordinates":[4.8634,50.47382]}'); + } + + public function testToArrayGeoJson() + { + $lon = 4.8634; + $lat = 50.47382; + $point = $this->preparePoint($lon, $lat); + + $this->assertEquals( + $point->toArrayGeoJson(), + [ + 'type' => 'Point', + 'coordinates' => [4.8634, 50.47382] + ] + ); + } + + public function testJsonSerialize() + { + $lon = 4.8634; + $lat = 50.47382; + $point = $this->preparePoint($lon, $lat); + + $this->assertEquals( + $point->jsonSerialize(), + [ + 'type' => 'Point', + 'coordinates' => [4.8634, 50.47382] + ] + ); + } + + public function testFromGeoJson() + { + $geojson = '{"type":"Point","coordinates":[4.8634,50.47382]}'; + $lon = 4.8634; + $lat = 50.47382; + $point = $this->preparePoint($lon, $lat);; + + $this->assertEquals($point, Point::fromGeoJson($geojson)); + } + + public function testFromLonLat() + { + $lon = 4.8634; + $lat = 50.47382; + $point = $this->preparePoint($lon, $lat);; + + $this->assertEquals($point, Point::fromLonLat($lon, $lat)); + } + + public function testFromArrayGeoJson() + { + $array = [ + 'type' => 'Point', + 'coordinates' => [4.8634, 50.47382] + ]; + $lon = 4.8634; + $lat = 50.47382; + $point = $this->preparePoint($lon, $lat);; + + $this->assertEquals($point, Point::fromArrayGeoJson($array)); + } + + public function testGetLat() + { + $lon = 4.8634; + $lat = 50.47382; + $point = $this->preparePoint($lon, $lat);; + + $this->assertEquals($lat, $point->getLat()); + } + + public function testGetLon() + { + $lon = 4.8634; + $lat = 50.47382; + $point = $this->preparePoint($lon, $lat);; + + $this->assertEquals($lon, $point->getLon()); + } + + private function preparePoint($lon, $lat) + { + return Point::fromLonLat($lon, $lat); + } + +} From 05d3d7f5c71b102f599f2273fa708932e2a6f3cf Mon Sep 17 00:00:00 2001 From: nobohan Date: Fri, 23 Apr 2021 16:48:27 +0200 Subject: [PATCH 15/42] update twig template and AddressType following the changes in Address entity --- src/Bundle/ChillMainBundle/Entity/Address.php | 4 ++-- .../ChillMainBundle/Form/Type/AddressType.php | 16 +++++++------- .../Resources/views/Address/macro.html.twig | 4 ++-- .../Resources/views/Address/edit.html.twig | 18 +++++++-------- .../Resources/views/Address/new.html.twig | 22 +++++++++---------- 5 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Entity/Address.php b/src/Bundle/ChillMainBundle/Entity/Address.php index 43512a5b2..e01c3f540 100644 --- a/src/Bundle/ChillMainBundle/Entity/Address.php +++ b/src/Bundle/ChillMainBundle/Entity/Address.php @@ -367,12 +367,12 @@ class Address return $this; } - public function getNumber(): ?string + public function getStreetNumber(): ?string { return $this->streetNumber; } - public function setNumber(string $streetNumber): self + public function setStreetNumber(string $streetNumber): self { $this->streetNumber = $streetNumber; diff --git a/src/Bundle/ChillMainBundle/Form/Type/AddressType.php b/src/Bundle/ChillMainBundle/Form/Type/AddressType.php index 4a0e222b8..68cfdda09 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/AddressType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/AddressType.php @@ -33,8 +33,8 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType; * A type to create/update Address entity * * Options: - * - * - `has_valid_from` (boolean): show if an entry "has valid from" must be + * + * - `has_valid_from` (boolean): show if an entry "has valid from" must be * shown. * - `null_if_empty` (boolean): replace the address type by null if the street * or the postCode is empty. This is useful when the address is not required and @@ -45,10 +45,10 @@ class AddressType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options) { $builder - ->add('streetAddress1', TextType::class, array( + ->add('street', TextType::class, array( 'required' => !$options['has_no_address'] // true if has no address is false )) - ->add('streetAddress2', TextType::class, array( + ->add('streetNumber', TextType::class, array( 'required' => false )) ->add('postCode', PostalCodeType::class, array( @@ -57,7 +57,7 @@ class AddressType extends AbstractType 'required' => !$options['has_no_address'] // true if has no address is false )) ; - + if ($options['has_valid_from']) { $builder ->add('validFrom', DateType::class, array( @@ -67,7 +67,7 @@ class AddressType extends AbstractType ) ); } - + if ($options['has_no_address']) { $builder ->add('isNoAddress', ChoiceType::class, [ @@ -79,12 +79,12 @@ class AddressType extends AbstractType 'label' => 'address.address_homeless' ]); } - + if ($options['null_if_empty'] === TRUE) { $builder->setDataMapper(new AddressDataMapper()); } } - + public function configureOptions(OptionsResolver $resolver) { $resolver diff --git a/src/Bundle/ChillMainBundle/Resources/views/Address/macro.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Address/macro.html.twig index 165d7c280..5de175169 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Address/macro.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Address/macro.html.twig @@ -6,8 +6,8 @@
{{ 'address.consider homeless'|trans }}
{% endif %}
- {% if address.streetAddress1 is not empty %}

{{ address.streetAddress1 }}

{% endif %} - {% if address.streetAddress2 is not empty %}

{{ address.streetAddress2 }}

{% endif %} + {% if address.street is not empty %}

{{ address.street }}

{% endif %} + {% if address.streetNumber is not empty %}

{{ address.streetNumber }}

{% endif %} {% if address.postCode is not empty %}

{{ address.postCode.code }} {{ address.postCode.name }}

{{ address.postCode.country.name|localize_translatable_string }}

diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Address/edit.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Address/edit.html.twig index eea76b5fe..b516dfb83 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Address/edit.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Address/edit.html.twig @@ -21,17 +21,17 @@ {% block title 'Update address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) %} {% block personcontent %} - +

{{ 'Update address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}

- + {{ form_start(form) }} - + {{ form_row(form.isNoAddress) }} - {{ form_row(form.streetAddress1) }} - {{ form_row(form.streetAddress2) }} + {{ form_row(form.street) }} + {{ form_row(form.streetNumber) }} {{ form_row(form.postCode) }} {{ form_row(form.validFrom) }} - + - + {{ form_end(form) }} - -{% endblock personcontent %} \ No newline at end of file + +{% endblock personcontent %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Address/new.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Address/new.html.twig index 9e94d7804..70cc51e53 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Address/new.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Address/new.html.twig @@ -21,21 +21,21 @@ {% block title %}{{ 'New address for %name%'|trans({ '%name%': person.firstName|capitalize ~ ' ' ~ person.lastName } )|capitalize }}{% endblock %} {% block personcontent %} - +

{{ 'New address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}

- + {{ form_start(form) }} - + {{ form_row(form.isNoAddress) }} - {{ form_row(form.streetAddress1) }} - {{ form_errors(form.streetAddress1) }} - {{ form_row(form.streetAddress2) }} - {{ form_errors(form.streetAddress2) }} + {{ form_row(form.street) }} + {{ form_errors(form.street) }} + {{ form_row(form.streetNumber) }} + {{ form_errors(form.streetNumber) }} {{ form_row(form.postCode) }} {{ form_errors(form.postCode) }} {{ form_row(form.validFrom) }} {{ form_errors(form.validFrom) }} - +
- + {{ form_end(form) }} - -{% endblock personcontent %} \ No newline at end of file + +{% endblock personcontent %} From bec0700d396fe5e8934cb12dd87c2c3a23d4ec3d Mon Sep 17 00:00:00 2001 From: nobohan Date: Fri, 30 Apr 2021 13:27:33 +0200 Subject: [PATCH 16/42] Point model: better handling of exception --- src/Bundle/ChillMainBundle/Doctrine/Model/Point.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php b/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php index 67cf434c0..43c21ae59 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php +++ b/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php @@ -60,14 +60,14 @@ class Point implements JsonSerializable { throw PointException::badJsonString($geojson); } - if ($a->type != "Point"){ + if ($a->type != 'Point'){ throw PointException::badGeoType(); - } else { - $lat = $a->coordinates[1]; - $lon = $a->coordinates[0]; - return Point::fromLonLat($lon, $lat); } + $lat = $a->coordinates[1]; + $lon = $a->coordinates[0]; + + return Point::fromLonLat($lon, $lat); } public static function fromLonLat(float $lon, float $lat): Point From 91860afd8036150b977d72a55a4cf36c88c2a768 Mon Sep 17 00:00:00 2001 From: nobohan Date: Fri, 30 Apr 2021 13:51:45 +0200 Subject: [PATCH 17/42] check if point value is null for 2 methods in PointType --- .../ChillMainBundle/Doctrine/Type/PointType.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Doctrine/Type/PointType.php b/src/Bundle/ChillMainBundle/Doctrine/Type/PointType.php index 6b6f7c113..086b566da 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/Type/PointType.php +++ b/src/Bundle/ChillMainBundle/Doctrine/Type/PointType.php @@ -36,7 +36,11 @@ class PointType extends Type { */ public function convertToPHPValue($value, AbstractPlatform $platform) { - return Point::fromGeoJson($value); + if ($value === NULL){ + return NULL; + } else { + return Point::fromGeoJson($value); + } } public function getName() @@ -46,7 +50,11 @@ class PointType extends Type { public function convertToDatabaseValue($value, AbstractPlatform $platform) { - return $value->toWKT(); + if ($value === NULL){ + return NULL; + } else { + return $value->toWKT(); + } } public function canRequireSQLConversion() From e9d142f3e8c9ae0d375467c170e20a308762c552 Mon Sep 17 00:00:00 2001 From: nobohan Date: Fri, 30 Apr 2021 14:15:20 +0200 Subject: [PATCH 18/42] define legacy functions for Address --- src/Bundle/ChillMainBundle/Entity/Address.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Entity/Address.php b/src/Bundle/ChillMainBundle/Entity/Address.php index e01c3f540..9e9022e31 100644 --- a/src/Bundle/ChillMainBundle/Entity/Address.php +++ b/src/Bundle/ChillMainBundle/Entity/Address.php @@ -154,7 +154,7 @@ class Address } /** - * Set streetAddress1 + * Set streetAddress1 (legacy function) * * @param string $streetAddress1 * @@ -162,13 +162,13 @@ class Address */ public function setStreetAddress1($streetAddress1) { - $this->streetAddress1 = $streetAddress1 === NULL ? '' : $streetAddress1; + $this->street = $streetAddress1 === NULL ? '' : $streetAddress1; return $this; } /** - * Get streetAddress1 + * Get streetAddress1 (legacy function) * * @return string */ @@ -178,7 +178,7 @@ class Address } /** - * Set streetAddress2 + * Set streetAddress2 (legacy function) * * @param string $streetAddress2 * @@ -186,13 +186,13 @@ class Address */ public function setStreetAddress2($streetAddress2) { - $this->streetAddress2 = $streetAddress2 === NULL ? '' : $streetAddress2; + $this->streetNumber = $streetAddress2 === NULL ? '' : $streetAddress2; return $this; } /** - * Get streetAddress2 + * Get streetAddress2 (legacy function) * * @return string */ From c5faa0b99d1c4d9440f19f54e2e136a409ee23c8 Mon Sep 17 00:00:00 2001 From: nobohan Date: Fri, 30 Apr 2021 15:15:47 +0200 Subject: [PATCH 19/42] update the address fixture by adding some points --- .../DataFixtures/ORM/LoadPeople.php | 122 ++++++++++-------- 1 file changed, 71 insertions(+), 51 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPeople.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPeople.php index fa20d3457..e8697ff28 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPeople.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPeople.php @@ -29,6 +29,7 @@ use Chill\PersonBundle\Entity\Person; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Chill\MainBundle\DataFixtures\ORM\LoadPostalCodes; use Chill\MainBundle\Entity\Address; +use Chill\MainBundle\Doctrine\Model\Point; /** * Load people into database @@ -37,17 +38,17 @@ use Chill\MainBundle\Entity\Address; * @author Marc Ducobu */ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, ContainerAwareInterface -{ - +{ + use \Symfony\Component\DependencyInjection\ContainerAwareTrait; - + protected $faker; - + public function __construct() { $this->faker = \Faker\Factory::create('fr_FR'); } - + public function prepare() { //prepare days, month, years @@ -56,57 +57,57 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con $this->years[] = $y; $y = $y +1; } while ($y >= 1990); - + $m = 1; do { $this->month[] = $m; $m = $m +1; } while ($m >= 12); - + $d = 1; do { $this->day[] = $d; $d = $d + 1; } while ($d <= 28); } - + public function getOrder() { return 10000; } - + public function load(ObjectManager $manager) { $this->loadRandPeople($manager); $this->loadExpectedPeople($manager); - + $manager->flush(); } - + public function loadExpectedPeople(ObjectManager $manager) { echo "loading expected people...\n"; - + foreach ($this->peoples as $person) { $this->addAPerson($this->fillWithDefault($person), $manager); } } - + public function loadRandPeople(ObjectManager $manager) { echo "loading rand people...\n"; - + $this->prepare(); - + $chooseLastNameOrTri = array('tri', 'tri', 'name', 'tri'); - + $i = 0; - + do { $i++; - + $sex = $this->genders[array_rand($this->genders)]; - + if ($chooseLastNameOrTri[array_rand($chooseLastNameOrTri)] === 'tri' ) { $length = rand(2, 3); $lastName = ''; @@ -117,13 +118,13 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con } else { $lastName = $this->lastNames[array_rand($this->lastNames)]; } - + if ($sex === Person::MALE_GENDER) { $firstName = $this->firstNamesMale[array_rand($this->firstNamesMale)]; } else { $firstName = $this->firstNamesFemale[array_rand($this->firstNamesFemale)]; } - + // add an address on 80% of the created people if (rand(0,100) < 80) { $address = $this->getRandomAddress(); @@ -137,7 +138,7 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con } else { $address = null; } - + $person = array( 'FirstName' => $firstName, 'LastName' => $lastName, @@ -147,15 +148,15 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con 'Address' => $address, 'maritalStatus' => $this->maritalStatusRef[array_rand($this->maritalStatusRef)] ); - + $this->addAPerson($this->fillWithDefault($person), $manager); - + } while ($i <= 100); } - + /** * fill a person array with default value - * + * * @param string[] $specific */ private function fillWithDefault(array $specific) @@ -171,10 +172,10 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con 'Address' => null ), $specific); } - + /** * create a new person from array data - * + * * @param array $person * @param ObjectManager $manager * @throws \Exception @@ -200,35 +201,51 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con $this->addAccompanyingPeriods($p, $value, $manager); break; } - + //try to add the data using the setSomething function, // if not possible, fallback to addSomething function if (method_exists($p, 'set'.$key)) { call_user_func(array($p, 'set'.$key), $value); } elseif (method_exists($p, 'add'.$key)) { // if we have a "addSomething", we may have multiple items to add - // so, we set the value in an array if it is not an array, and + // so, we set the value in an array if it is not an array, and // will call the function addSomething multiple times if (!is_array($value)) { $value = array($value); } - + foreach($value as $v) { if ($v !== NULL) { call_user_func(array($p, 'add'.$key), $v); } } - - } + + } } $manager->persist($p); echo "add person'".$p->__toString()."'\n"; } - + + /** - * Creata a random address - * + * Create a random point + * + * @return Point + */ + private function getRandomPoint() + { + $lonBrussels = 4.35243; + $latBrussels = 50.84676; + $lon = $lonBrussels + 0.01 * rand(-5, 5); + $lat = $latBrussels + 0.01 * rand(-5, 5); + return Point::fromLonLat($lon, $lat); + } + + + /** + * Create a random address + * * @return Address */ private function getRandomAddress() @@ -238,13 +255,16 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con ->setStreetAddress2( rand(0,9) > 5 ? $this->faker->streetAddress : '' ) + ->setPoint( + rand(0,9) > 5 ? $this->getRandomPoint() : NULL + ) ->setPostcode($this->getReference( LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)] )) ->setValidFrom($this->faker->dateTimeBetween('-5 years')) ; } - + private function getCountry($countryCode) { if ($countryCode === NULL) { @@ -257,30 +277,30 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con private $maritalStatusRef = ['ms_single', 'ms_married', 'ms_widow', 'ms_separat', 'ms_divorce', 'ms_legalco', 'ms_unknown']; - + private $firstNamesMale = array("Jean", "Mohamed", "Alfred", "Robert", "Justin", "Brian", "Compère", "Jean-de-Dieu", "Charles", "Pierre", "Luc", "Mathieu", "Alain", "Etienne", "Eric", "Corentin", "Gaston", "Spirou", "Fantasio", "Mahmadou", "Mohamidou", "Vursuv", "Youssef" ); - + private $firstNamesFemale = array("Svedana", "Sevlatina", "Irène", "Marcelle", "Corentine", "Alfonsine", "Caroline", "Solange", "Gostine", "Fatoumata", "Nicole", "Groseille", "Chana", "Oxana", "Ivana", "Julie", "Tina", "Adèle" ); - + private $lastNames = array("Diallo", "Bah", "Gaillot", "Martin"); - + private $lastNamesTrigrams = array("fas", "tré", "hu", 'blart', 'van', 'der', 'lin', 'den', 'ta', 'mi', 'net', 'gna', 'bol', 'sac', 'ré', 'jo', 'du', 'pont', 'cas', 'tor', 'rob', 'al', 'ma', 'gone', 'car',"fu", "ka", "lot", "no", "va", "du", "bu", "su", "jau", "tte", 'sir', "lo", 'to', "cho", "car", 'mo','zu', 'qi', 'mu'); - + private $genders = array(Person::MALE_GENDER, Person::FEMALE_GENDER); - + private $years = array(); - + private $month = array(); - + private $day = array(); - + private $peoples = array( array( 'LastName' => "Depardieu", @@ -362,21 +382,21 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con 'maritalStatus' => 'ms_legalco' ), ); - - + + private function addAccompanyingPeriods(Person $person, array $periods, ObjectManager $manager) { foreach ($periods as $period) { - + echo "adding new past Accompanying Period..\n"; - + /** @var AccompanyingPeriod $accompanyingPeriod */ $accompanyingPeriod = new AccompanyingPeriod(new \DateTime($period['from'])); $accompanyingPeriod ->setClosingDate(new \DateTime($period['to'])) ->setRemark($period['remark']) ; - + $person->addAccompanyingPeriod($accompanyingPeriod); } } From a1895ec65f7967d2b9560aa0f275041e2cfecf73 Mon Sep 17 00:00:00 2001 From: nobohan Date: Mon, 3 May 2021 11:18:31 +0200 Subject: [PATCH 20/42] fixture for addressReference - WIP --- .../Controller/AddressReferenceController.php | 49 ++++++++++ .../ORM/LoadAddressReferences.php | 96 +++++++++++++++++++ .../Entity/AddressReference.php | 12 ++- ...22101743.php => Version20210503085107.php} | 12 +-- 4 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Controller/AddressReferenceController.php create mode 100644 src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php rename src/Bundle/ChillMainBundle/migrations/{Version20210422101743.php => Version20210503085107.php} (75%) diff --git a/src/Bundle/ChillMainBundle/Controller/AddressReferenceController.php b/src/Bundle/ChillMainBundle/Controller/AddressReferenceController.php new file mode 100644 index 000000000..f505de235 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/AddressReferenceController.php @@ -0,0 +1,49 @@ +dispatcher->dispatch( + AccompanyingPeriodPrivacyEvent::ACCOMPANYING_PERIOD_PRIVACY_EVENT, + new AccompanyingPeriodPrivacyEvent($addressReference, [ + 'action' => 'showApi' + ]) + ); + + switch ($_format) { + case 'json': + return $this->json($addressReference); + default: + throw new BadRequestException('Unsupported format'); + } + + } +} diff --git a/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php b/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php new file mode 100644 index 000000000..a546210ce --- /dev/null +++ b/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php @@ -0,0 +1,96 @@ +faker = \Faker\Factory::create('fr_FR'); + } + + /** + * + * @var ContainerInterface + */ + private $container; + + public function setContainer(ContainerInterface $container = null) + { + $this->container = $container; + } + + public function getOrder() { + return 30; + } + + + /** + * Create a random point + * + * @return Point + */ + private function getRandomPoint() + { + $lonBrussels = 4.35243; + $latBrussels = 50.84676; + $lon = $lonBrussels + 0.01 * rand(-5, 5); + $lat = $latBrussels + 0.01 * rand(-5, 5); + return Point::fromLonLat($lon, $lat); + } + + /** + * Create a random reference address + * + * @return AddressReference + */ + private function getRandomAddressReference() + { + $ar= new AddressReference(); + + $ar->setRefId($this->faker->phoneNumber); + $ar->setStreet($this->faker->streetAddress); + $ar->setStreetNumber(rand(0,199)); + $ar ->setPoint($this->getRandomPoint()); + $ar->setPostcode($this->getReference( + LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)] + )); + + $ar->setMunicipalityCode($ar->getPostcode()); + dump($ar); + + return $ar + ; + } + + public function load(ObjectManager $manager) { + + echo "loading some reference address... \n"; + + for ($i=0; $i<10; $i++) { + $ar = $this->getRandomAddressReference(); + $manager->persist($ar); + } + + $manager->flush(); + } + + +} diff --git a/src/Bundle/ChillMainBundle/Entity/AddressReference.php b/src/Bundle/ChillMainBundle/Entity/AddressReference.php index 8f1678425..d7a0e0156 100644 --- a/src/Bundle/ChillMainBundle/Entity/AddressReference.php +++ b/src/Bundle/ChillMainBundle/Entity/AddressReference.php @@ -4,6 +4,7 @@ namespace Chill\MainBundle\Entity; use Chill\MainBundle\Entity\AddressReferenceRepository; use Doctrine\ORM\Mapping as ORM; +use Chill\MainBundle\Doctrine\Model\Point; /** * @ORM\Entity(repositoryClass=AddressReferenceRepository::class) @@ -52,10 +53,15 @@ class AddressReference private $source; /** - * @ORM\Column(type="string", length=255) + * A geospatial field storing the coordinates of the Address + * + * @var Point + * + * @ORM\Column(type="point") */ private $point; + public function getId(): ?int { return $this->id; @@ -133,12 +139,12 @@ class AddressReference return $this; } - public function getPoint(): ?string + public function getPoint(): ?Point { return $this->point; } - public function setPoint(string $point): self + public function setPoint(?Point $point): self { $this->point = $point; diff --git a/src/Bundle/ChillMainBundle/migrations/Version20210422101743.php b/src/Bundle/ChillMainBundle/migrations/Version20210503085107.php similarity index 75% rename from src/Bundle/ChillMainBundle/migrations/Version20210422101743.php rename to src/Bundle/ChillMainBundle/migrations/Version20210503085107.php index 2d4341155..f693777a0 100644 --- a/src/Bundle/ChillMainBundle/migrations/Version20210422101743.php +++ b/src/Bundle/ChillMainBundle/migrations/Version20210503085107.php @@ -8,24 +8,24 @@ use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; /** - * Add AddressReference + * Add a AddressReference table for storing authoritative address data */ -final class Version20210422101743 extends AbstractMigration +final class Version20210503085107 extends AbstractMigration { - public function getDescription() : string + public function getDescription(): string { return 'Add a AddressReference table for storing authoritative address data'; } - public function up(Schema $schema) : void + public function up(Schema $schema): void { $this->addSql('CREATE SEQUENCE chill_main_address_reference_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE TABLE chill_main_address_reference (id INT NOT NULL, postcode_id INT DEFAULT NULL, refId VARCHAR(255) NOT NULL, street VARCHAR(255) DEFAULT NULL, streetNumber VARCHAR(255) DEFAULT NULL, municipalityCode VARCHAR(255) DEFAULT NULL, source VARCHAR(255) DEFAULT NULL, point VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE chill_main_address_reference (id INT NOT NULL, postcode_id INT DEFAULT NULL, refId VARCHAR(255) NOT NULL, street VARCHAR(255) DEFAULT NULL, streetNumber VARCHAR(255) DEFAULT NULL, municipalityCode VARCHAR(255) DEFAULT NULL, source VARCHAR(255) DEFAULT NULL, point geometry(POINT,4326) NOT NULL, PRIMARY KEY(id))'); $this->addSql('CREATE INDEX IDX_CA6C1BD7EECBFDF1 ON chill_main_address_reference (postcode_id)'); $this->addSql('ALTER TABLE chill_main_address_reference ADD CONSTRAINT FK_CA6C1BD7EECBFDF1 FOREIGN KEY (postcode_id) REFERENCES chill_main_postal_code (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); } - public function down(Schema $schema) : void + public function down(Schema $schema): void { $this->addSql('DROP SEQUENCE chill_main_address_reference_id_seq CASCADE'); $this->addSql('DROP TABLE chill_main_address_reference'); From 17c01d9b463e6d2ae6310ab945547b2f000f9842 Mon Sep 17 00:00:00 2001 From: nobohan Date: Mon, 3 May 2021 12:18:21 +0200 Subject: [PATCH 21/42] fix fixture for address reference --- .../ORM/LoadAddressReferences.php | 5 ++-- .../Entity/AddressReference.php | 24 ++++++++++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php b/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php index a546210ce..63de2eb21 100644 --- a/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php +++ b/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php @@ -38,7 +38,7 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt } public function getOrder() { - return 30; + return 51; } @@ -73,8 +73,7 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)] )); - $ar->setMunicipalityCode($ar->getPostcode()); - dump($ar); + $ar->setMunicipalityCode($ar->getPostcode()->getCode()); return $ar ; diff --git a/src/Bundle/ChillMainBundle/Entity/AddressReference.php b/src/Bundle/ChillMainBundle/Entity/AddressReference.php index d7a0e0156..944cab81b 100644 --- a/src/Bundle/ChillMainBundle/Entity/AddressReference.php +++ b/src/Bundle/ChillMainBundle/Entity/AddressReference.php @@ -103,18 +103,30 @@ class AddressReference return $this; } - public function getPostcode(): ?string - { - return $this->postcode; - } - - public function setPostcode(?string $postcode): self + /** + * Set postcode + * + * @param PostalCode $postcode + * + * @return Address + */ + public function setPostcode(PostalCode $postcode = null) { $this->postcode = $postcode; return $this; } + /** + * Get postcode + * + * @return PostalCode + */ + public function getPostcode() + { + return $this->postcode; + } + public function getMunicipalityCode(): ?string { return $this->municipalityCode; From 1587c762f89215d139cdf93ef74003b9e9d5be68 Mon Sep 17 00:00:00 2001 From: nobohan Date: Mon, 3 May 2021 14:34:23 +0200 Subject: [PATCH 22/42] better fixture for AddressReference --- .../DataFixtures/ORM/LoadAddressReferences.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php b/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php index 63de2eb21..81bd1d909 100644 --- a/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php +++ b/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php @@ -65,8 +65,8 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt { $ar= new AddressReference(); - $ar->setRefId($this->faker->phoneNumber); - $ar->setStreet($this->faker->streetAddress); + $ar->setRefId($this->faker->numerify('ref-id-######')); + $ar->setStreet($this->faker->streetName); $ar->setStreetNumber(rand(0,199)); $ar ->setPoint($this->getRandomPoint()); $ar->setPostcode($this->getReference( From ce859697b50798ab5d5e296b9cedf125a36d8009 Mon Sep 17 00:00:00 2001 From: nobohan Date: Mon, 3 May 2021 17:16:45 +0200 Subject: [PATCH 23/42] Use json endpoint for showing address with Vuejs Components (WIP) --- ...ceController.php => AddressController.php} | 42 +++++++---- .../Resources/public/js/Address/App.vue | 43 ++++++++++++ .../Address/components/AccompanyingCourse.vue | 26 +++++++ .../js/Address/components/PersonItem.vue | 25 +++++++ .../Address/components/PersonsAssociated.vue | 69 +++++++++++++++++++ .../js/Address/components/Requestor.vue | 16 +++++ .../Resources/public/js/Address/index.js | 8 +++ .../Resources/public/js/Address/store/.keep | 0 .../Resources/views/Address/list.html.twig | 30 ++++++-- .../ChillPersonBundle/chill.webpack.config.js | 4 +- 10 files changed, 241 insertions(+), 22 deletions(-) rename src/Bundle/ChillMainBundle/Controller/{AddressReferenceController.php => AddressController.php} (52%) create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/js/Address/App.vue create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/AccompanyingCourse.vue create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/PersonItem.vue create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/PersonsAssociated.vue create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/Requestor.vue create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/js/Address/index.js create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/js/Address/store/.keep diff --git a/src/Bundle/ChillMainBundle/Controller/AddressReferenceController.php b/src/Bundle/ChillMainBundle/Controller/AddressController.php similarity index 52% rename from src/Bundle/ChillMainBundle/Controller/AddressReferenceController.php rename to src/Bundle/ChillMainBundle/Controller/AddressController.php index f505de235..1aeb39062 100644 --- a/src/Bundle/ChillMainBundle/Controller/AddressReferenceController.php +++ b/src/Bundle/ChillMainBundle/Controller/AddressController.php @@ -7,17 +7,39 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\Routing\Annotation\Route; +use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Entity\AddressReference; - /** - * Class AccompanyingCourseController + * Class AddressController * - * @package Chill\PersonBundle\Controller + * @package Chill\MainBundle\Controller */ -class AddressReferenceController extends AbstractController +class AddressController extends AbstractController { + + /** + * Get API Data for showing endpoint + * + * @Route( + * "/{_locale}/main/api/1.0/address/{address_id}/show.{_format}", + * name="chill_main_address_api_show" + * ) + * @ParamConverter("address", options={"id": "address_id"}) + */ + public function showAddress(Address $address, $_format): Response + { + // TODO check ACL ? + switch ($_format) { + case 'json': + return $this->json($address); + default: + throw new BadRequestException('Unsupported format'); + } + } + + /** * Get API Data for showing endpoint * @@ -27,17 +49,9 @@ class AddressReferenceController extends AbstractController * ) * @ParamConverter("addressReference", options={"id": "address_reference_id"}) */ - public function showAPI(AddressReference $addressReference, $_format): Response + public function showAddressReference(AddressReference $addressReference, $_format): Response { - // TODO check ACL on AccompanyingPeriod - - $this->dispatcher->dispatch( - AccompanyingPeriodPrivacyEvent::ACCOMPANYING_PERIOD_PRIVACY_EVENT, - new AccompanyingPeriodPrivacyEvent($addressReference, [ - 'action' => 'showApi' - ]) - ); - + // TODO check ACL ? switch ($_format) { case 'json': return $this->json($addressReference); diff --git a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/App.vue b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/App.vue new file mode 100644 index 000000000..924747c4e --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/App.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/AccompanyingCourse.vue b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/AccompanyingCourse.vue new file mode 100644 index 000000000..d08798c00 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/AccompanyingCourse.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/PersonItem.vue b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/PersonItem.vue new file mode 100644 index 000000000..f75f0779b --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/PersonItem.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/PersonsAssociated.vue b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/PersonsAssociated.vue new file mode 100644 index 000000000..f4ccf6964 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/PersonsAssociated.vue @@ -0,0 +1,69 @@ + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/Requestor.vue b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/Requestor.vue new file mode 100644 index 000000000..75a95e241 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/Requestor.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/index.js b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/index.js new file mode 100644 index 000000000..b524398f7 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/index.js @@ -0,0 +1,8 @@ +import App from './App.vue'; +import { createApp } from 'vue'; + +const app = createApp({ + template: `` +}) +.component('app', App) +.mount('#address'); diff --git a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/store/.keep b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/store/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Address/list.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Address/list.html.twig index 9e91e486c..1e39cf705 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Address/list.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Address/list.html.twig @@ -23,9 +23,9 @@ {% block title %}{{ 'Addresses\'history for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}{% endblock %} {% block personcontent %} - +

{{ 'Addresses\'history for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}

- + @@ -48,11 +48,11 @@ {% for address in person.addresses %} - + - + + + +
+ + {{ encore_entry_script_tags('address') }} + + + {% endfor %} {% endif %}
{{ 'Since %date%'|trans( { '%date%' : address.validFrom|format_date('long') } ) }} {{ address_macros._render(address, { 'with_valid_from' : false, 'has_no_address': true } ) }}
  • @@ -61,11 +61,26 @@
- + + + + + +
- -{% endblock personcontent %} \ No newline at end of file + +{% endblock personcontent %} diff --git a/src/Bundle/ChillPersonBundle/chill.webpack.config.js b/src/Bundle/ChillPersonBundle/chill.webpack.config.js index 9d7a33d02..8e2c60ef2 100644 --- a/src/Bundle/ChillPersonBundle/chill.webpack.config.js +++ b/src/Bundle/ChillPersonBundle/chill.webpack.config.js @@ -7,6 +7,8 @@ module.exports = function(encore, entries) encore.addAliases({ ChillPersonAssets: __dirname + '/Resources/public' }); - + encore.addEntry('accompanying_course', __dirname + '/Resources/public/js/AccompanyingCourse/index.js'); + encore.addEntry('address', __dirname + '/Resources/public/js/Address/index.js'); + }; From 86c177bbbbe8f1a58a066ad04fcc949f23e0532d Mon Sep 17 00:00:00 2001 From: nobohan Date: Tue, 4 May 2021 17:26:47 +0200 Subject: [PATCH 24/42] remove vuejs address component --- .../Resources/public/js/Address/App.vue | 43 ------------ .../Address/components/AccompanyingCourse.vue | 26 ------- .../js/Address/components/PersonItem.vue | 25 ------- .../Address/components/PersonsAssociated.vue | 69 ------------------- .../js/Address/components/Requestor.vue | 16 ----- .../Resources/public/js/Address/index.js | 8 --- .../Resources/public/js/Address/store/.keep | 0 .../Resources/views/Address/list.html.twig | 9 --- .../ChillPersonBundle/chill.webpack.config.js | 2 - 9 files changed, 198 deletions(-) delete mode 100644 src/Bundle/ChillPersonBundle/Resources/public/js/Address/App.vue delete mode 100644 src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/AccompanyingCourse.vue delete mode 100644 src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/PersonItem.vue delete mode 100644 src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/PersonsAssociated.vue delete mode 100644 src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/Requestor.vue delete mode 100644 src/Bundle/ChillPersonBundle/Resources/public/js/Address/index.js delete mode 100644 src/Bundle/ChillPersonBundle/Resources/public/js/Address/store/.keep diff --git a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/App.vue b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/App.vue deleted file mode 100644 index 924747c4e..000000000 --- a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/App.vue +++ /dev/null @@ -1,43 +0,0 @@ - - - - - diff --git a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/AccompanyingCourse.vue b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/AccompanyingCourse.vue deleted file mode 100644 index d08798c00..000000000 --- a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/AccompanyingCourse.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - diff --git a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/PersonItem.vue b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/PersonItem.vue deleted file mode 100644 index f75f0779b..000000000 --- a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/PersonItem.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - diff --git a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/PersonsAssociated.vue b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/PersonsAssociated.vue deleted file mode 100644 index f4ccf6964..000000000 --- a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/PersonsAssociated.vue +++ /dev/null @@ -1,69 +0,0 @@ - - - diff --git a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/Requestor.vue b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/Requestor.vue deleted file mode 100644 index 75a95e241..000000000 --- a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/components/Requestor.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/index.js b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/index.js deleted file mode 100644 index b524398f7..000000000 --- a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import App from './App.vue'; -import { createApp } from 'vue'; - -const app = createApp({ - template: `` -}) -.component('app', App) -.mount('#address'); diff --git a/src/Bundle/ChillPersonBundle/Resources/public/js/Address/store/.keep b/src/Bundle/ChillPersonBundle/Resources/public/js/Address/store/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Address/list.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Address/list.html.twig index 1e39cf705..8a2aeee42 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Address/list.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Address/list.html.twig @@ -62,15 +62,6 @@ - -
- - {{ encore_entry_script_tags('address') }} - - - {% endfor %} {% endif %} diff --git a/src/Bundle/ChillPersonBundle/chill.webpack.config.js b/src/Bundle/ChillPersonBundle/chill.webpack.config.js index 8e2c60ef2..7987ee124 100644 --- a/src/Bundle/ChillPersonBundle/chill.webpack.config.js +++ b/src/Bundle/ChillPersonBundle/chill.webpack.config.js @@ -9,6 +9,4 @@ module.exports = function(encore, entries) }); encore.addEntry('accompanying_course', __dirname + '/Resources/public/js/AccompanyingCourse/index.js'); - encore.addEntry('address', __dirname + '/Resources/public/js/Address/index.js'); - }; From f61af9d02a17b31dda4926245666c7a5f4fbf94e Mon Sep 17 00:00:00 2001 From: nobohan Date: Wed, 5 May 2021 11:44:55 +0200 Subject: [PATCH 25/42] add household and householdmembers entities + migration --- .../Entity/Household/Household.php | 24 +++ .../Entity/Household/HouseholdMembers.php | 153 ++++++++++++++++++ .../Household/HouseholdMembersRepository.php | 50 ++++++ .../Household/HouseholdRepository.php | 50 ++++++ .../migrations/Version20210505093408.php | 41 +++++ 5 files changed, 318 insertions(+) create mode 100644 src/Bundle/ChillPersonBundle/Entity/Household/Household.php create mode 100644 src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMembers.php create mode 100644 src/Bundle/ChillPersonBundle/Repository/Household/HouseholdMembersRepository.php create mode 100644 src/Bundle/ChillPersonBundle/Repository/Household/HouseholdRepository.php create mode 100644 src/Bundle/ChillPersonBundle/migrations/Version20210505093408.php diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/Household.php b/src/Bundle/ChillPersonBundle/Entity/Household/Household.php new file mode 100644 index 000000000..304321ac8 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Entity/Household/Household.php @@ -0,0 +1,24 @@ +id; + } +} diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMembers.php b/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMembers.php new file mode 100644 index 000000000..80864ecd0 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMembers.php @@ -0,0 +1,153 @@ +id; + } + + public function getPosition(): ?string + { + return $this->position; + } + + public function setPosition(?string $position): self + { + $this->position = $position; + + return $this; + } + + public function getStartDate(): ?\DateTimeInterface + { + return $this->startDate; + } + + public function setStartDate(\DateTimeInterface $startDate): self + { + $this->startDate = $startDate; + + return $this; + } + + public function getEndDate(): ?\DateTimeInterface + { + return $this->endDate; + } + + public function setEndDate(\DateTimeInterface $endDate): self + { + $this->endDate = $endDate; + + return $this; + } + + public function getComment(): ?string + { + return $this->comment; + } + + public function setComment(?string $comment): self + { + $this->comment = $comment; + + return $this; + } + + public function getSharedHousehold(): ?bool + { + return $this->sharedHousehold; + } + + public function setSharedHousehold(bool $sharedHousehold): self + { + $this->sharedHousehold = $sharedHousehold; + + return $this; + } + + public function getPerson(): ?Person + { + return $this->person; + } + + public function setPerson(?Person $person): self + { + $this->person = $person; + + return $this; + } + + public function getHousehold(): ?Household + { + return $this->household; + } + + public function setHousehold(?Household $household): self + { + $this->household = $household; + + return $this; + } +} diff --git a/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdMembersRepository.php b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdMembersRepository.php new file mode 100644 index 000000000..feea6d44d --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdMembersRepository.php @@ -0,0 +1,50 @@ +createQueryBuilder('h') + ->andWhere('h.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('h.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?HouseholdMembers + { + return $this->createQueryBuilder('h') + ->andWhere('h.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdRepository.php b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdRepository.php new file mode 100644 index 000000000..38522806c --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdRepository.php @@ -0,0 +1,50 @@ +createQueryBuilder('h') + ->andWhere('h.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('h.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?Household + { + return $this->createQueryBuilder('h') + ->andWhere('h.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20210505093408.php b/src/Bundle/ChillPersonBundle/migrations/Version20210505093408.php new file mode 100644 index 000000000..cf5a93164 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20210505093408.php @@ -0,0 +1,41 @@ +addSql('CREATE SEQUENCE Household_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE HouseholdMembers_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE Household (id INT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE HouseholdMembers (id INT NOT NULL, person_id INT DEFAULT NULL, household_id INT DEFAULT NULL, position VARCHAR(255) DEFAULT NULL, startDate DATE NOT NULL, endDate DATE NOT NULL, comment VARCHAR(255) DEFAULT NULL, sharedHousehold BOOLEAN NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_4D1FB288217BBB47 ON HouseholdMembers (person_id)'); + $this->addSql('CREATE INDEX IDX_4D1FB288E79FF843 ON HouseholdMembers (household_id)'); + $this->addSql('ALTER TABLE HouseholdMembers ADD CONSTRAINT FK_4D1FB288217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE HouseholdMembers ADD CONSTRAINT FK_4D1FB288E79FF843 FOREIGN KEY (household_id) REFERENCES Household (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE HouseholdMembers DROP CONSTRAINT FK_4D1FB288E79FF843'); + $this->addSql('ALTER TABLE HouseholdMembers DROP CONSTRAINT FK_4D1FB288217BBB47'); + $this->addSql('DROP SEQUENCE Household_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE HouseholdMembers_id_seq CASCADE'); + $this->addSql('DROP TABLE Household'); + $this->addSql('DROP TABLE HouseholdMembers'); + } +} From 0a894b0db1cacc3b1b24e836cd04e7fc6674fa44 Mon Sep 17 00:00:00 2001 From: nobohan Date: Wed, 5 May 2021 18:24:58 +0200 Subject: [PATCH 26/42] add new fields on Address and Household --- src/Bundle/ChillMainBundle/Entity/Address.php | 24 +++++++++- .../Entity/Household/Household.php | 46 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillMainBundle/Entity/Address.php b/src/Bundle/ChillMainBundle/Entity/Address.php index 9e9022e31..36eda09c2 100644 --- a/src/Bundle/ChillMainBundle/Entity/Address.php +++ b/src/Bundle/ChillMainBundle/Entity/Address.php @@ -5,6 +5,7 @@ namespace Chill\MainBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Chill\MainBundle\Doctrine\Model\Point; +use Chill\ThirdPartyBundle\Entity\ThirdParty; /** * Address @@ -130,6 +131,16 @@ class Address */ private $point; + /** + * A ThirdParty reference for person's addresses that are linked to a third party + * + * @var ThirdParty|null + * + * @ORM\ManyToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty") + * @ORM\JoinColumn(nullable=true) + */ + private $linkedToThirdParty; + /** * A list of metadata, added by customizable fields * @@ -142,7 +153,6 @@ class Address $this->validFrom = new \DateTime(); } - /** * Get id * @@ -487,5 +497,17 @@ class Address return $this; } + public function getLinkedToThirdParty() + { + return $this->linkedToThirdParty; + } + + public function setLinkedToThirdParty($linkedToThirdParty): self + { + $this->linkedToThirdParty = $linkedToThirdParty; + + return $this; + } + } diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/Household.php b/src/Bundle/ChillPersonBundle/Entity/Household/Household.php index 304321ac8..a12741dee 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Household/Household.php +++ b/src/Bundle/ChillPersonBundle/Entity/Household/Household.php @@ -4,6 +4,8 @@ namespace Chill\PersonBundle\Entity\Household; use Chill\PersonBundle\Repository\Household\HouseholdRepository; use Doctrine\ORM\Mapping as ORM; +use Doctrine\Common\Collections\Collection; +use Chill\MainBundle\Entity\Address; /** * @ORM\Entity(repositoryClass=HouseholdRepository::class) @@ -21,4 +23,48 @@ class Household { return $this->id; } + + /** + * Addresses + * @var Collection + * + * @ORM\ManyToMany( + * targetEntity="Chill\MainBundle\Entity\Address", + * cascade={"persist", "remove", "merge", "detach"}) + * @ORM\JoinTable(name="chill_person_household_to_addresses") + * @ORM\OrderBy({"validFrom" = "DESC"}) + */ + private $addresses; + + + /** + * @param Address $address + * @return $this + */ + public function addAddress(Address $address) + { + $this->addresses[] = $address; + + return $this; + } + + /** + * @param Address $address + */ + public function removeAddress(Address $address) + { + $this->addresses->removeElement($address); + } + + /** + * By default, the addresses are ordered by date, descending (the most + * recent first) + * + * @return \Chill\MainBundle\Entity\Address[] + */ + public function getAddresses() + { + return $this->addresses; + } + } From f02e33fda7a40aa63b3461c871c1b1b228073564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 5 May 2021 20:33:34 +0200 Subject: [PATCH 27/42] first impl for api --- .../Controller/AbstractCRUDController.php | 117 +++++++++++++++ .../CRUD/Controller/ApiController.php | 135 +++++++++++++++++ .../CRUD/Controller/CRUDController.php | 45 ++++-- .../CRUD/Routing/CRUDRoutesLoader.php | 138 +++++++++++++++--- .../ChillMainBundle/ChillMainBundle.php | 2 + .../ChillMainExtension.php | 94 ++++++++---- .../CRUDControllerCompilerPass.php | 103 +++++++++++++ .../DependencyInjection/Configuration.php | 69 ++++++++- .../ChillMainBundle/config/services/crud.yaml | 5 +- .../AccompanyingCourseApiController.php | 48 ++++++ .../ChillPersonExtension.php | 30 ++++ .../Entity/AccompanyingPeriod.php | 52 ++++--- .../Authorization/AccompanyingPeriodVoter.php | 73 +++++++++ .../ChillPersonBundle/config/services.yaml | 8 - .../config/services/security.yaml | 16 ++ .../translations/messages.fr.yml | 1 + 16 files changed, 849 insertions(+), 87 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php create mode 100644 src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php create mode 100644 src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/CRUDControllerCompilerPass.php create mode 100644 src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php create mode 100644 src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php create mode 100644 src/Bundle/ChillPersonBundle/config/services/security.yaml diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php new file mode 100644 index 000000000..363d65152 --- /dev/null +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php @@ -0,0 +1,117 @@ +getDoctrine() + ->getRepository($this->getEntityClass()) + ->find($id); + } + + /** + * + * @return string the complete fqdn of the class + */ + protected function getEntityClass(): string + { + return $this->crudConfig['class']; + } + + /** + * + */ + protected function onPostFetchEntity(string $action, Request $request, $entity, $_format): ?Response + { + return null; + } + + /** + * + */ + protected function onPostCheckACL(string $action, Request $request, $entity, $_format): ?Response + { + return null; + } + + + /** + * check the acl. Called by every action. + * + * By default, check the role given by `getRoleFor` for the value given in + * $entity. + * + * Throw an \Symfony\Component\Security\Core\Exception\AccessDeniedHttpException + * if not accessible. + * + * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedHttpException + */ + protected function checkACL(string $action, Request $request, $entity, $_format) + { + $this->denyAccessUnlessGranted($this->getRoleFor($action, $request, $entity, $_format), $entity); + } + + protected function getActionConfig(string $action) + { + return $this->crudConfig['actions'][$action]; + } + + + /** + * Set the crud configuration + * + * Used by the container to inject configuration for this crud. + */ + public function setCrudConfig(array $config): void + { + dump($config); + $this->crudConfig = $config; + } + + /** + * @return PaginatorFactory + */ + protected function getPaginatorFactory(): PaginatorFactory + { + return $this->container->get(PaginatorFactory::class); + } + + /** + * Defined the services necessary for this controller + * + * @return array + */ + public static function getSubscribedServices(): array + { + return \array_merge( + parent::getSubscribedServices(), + [ + PaginatorFactory::class => PaginatorFactory::class, + + ] + ); + } +} diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php new file mode 100644 index 000000000..c12c8e2b7 --- /dev/null +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php @@ -0,0 +1,135 @@ +getEntity($action, $id, $request, $_format); + + $postFetch = $this->onPostFetchEntity($action, $request, $entity, $_format); + + if ($postFetch instanceof Response) { + return $postFetch; + } + + if (NULL === $entity) { + throw $this->createNotFoundException(sprintf("The %s with id %s " + . "is not found", $this->getCrudName(), $id)); + } + + $response = $this->checkACL($action, $request, $entity, $_format); + if ($response instanceof Response) { + return $response; + } + + $response = $this->onPostCheckACL($action, $request, $entity, $_format); + if ($response instanceof Response) { + return $response; + } + + $response = $this->onBeforeSerialize($action, $request, $entity, $_format); + if ($response instanceof Response) { + return $response; + } + + if ($_format === 'json') { + $context = $this->getContextForSerialization($action, $request, $entity, $_format); + + return $this->json($entity, Response::HTTP_OK, [], $context); + } else { + throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This format is not implemented"); + } + } + + public function onBeforeSerialize(string $action, Request $request, $entity, string $_format): ?Response + { + return null; + } + + /** + * Base method for handling api action + * + * @return void + */ + public function entityApi(Request $request, $id, $_format): Response + { + switch ($request->getMethod()) { + case Request::METHOD_GET: + case REQUEST::METHOD_HEAD: + return $this->entityGet('_entity', $request, $id, $_format); + default: + throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This method is not implemented"); + } + } + + + protected function getContextForSerialization(string $action, Request $request, $entity, $_format): array + { + return []; + } + + /** + * get the role given from the config. + */ + protected function getRoleFor(string $action, Request $request, $entity, $_format): string + { + $actionConfig = $this->getActionConfig($action); + + if (NULL !== $actionConfig['roles'][$request->getMethod()]) { + return $actionConfig['roles'][$request->getMethod()]; + } + + if ($this->crudConfig['role']) { + return $this->crudConfig['role']; + } + + throw new \RuntimeException(sprintf("the config does not have any role for the ". + "method %s nor a global role for the whole action. Add those to your ". + "configuration or override the required method", $request->getMethod())); + + } + + protected function getSerializer(): SerializerInterface + { + return $this->get(SerializerInterface::class); + } + + /** + * Defined the services necessary for this controller + * + * @return array + */ + public static function getSubscribedServices(): array + { + return \array_merge( + parent::getSubscribedServices(), + [ + SerializerInterface::class => SerializerInterface::class, + ] + ); + } + +} diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/CRUDController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/CRUDController.php index 8a8d32932..8338af8c6 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/CRUDController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/CRUDController.php @@ -34,6 +34,7 @@ use Chill\MainBundle\CRUD\Form\CRUDDeleteEntityForm; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorInterface; use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Symfony\Component\Serializer\SerializerInterface; /** * Class CRUDController @@ -484,7 +485,7 @@ class CRUDController extends AbstractController * @param mixed $id * @return Response */ - protected function viewAction(string $action, Request $request, $id) + protected function viewAction(string $action, Request $request, $id, $_format = 'html'): Response { $entity = $this->getEntity($action, $id, $request); @@ -496,7 +497,7 @@ class CRUDController extends AbstractController if (NULL === $entity) { throw $this->createNotFoundException(sprintf("The %s with id %s " - . "is not found"), $this->getCrudName(), $id); + . "is not found", $this->getCrudName(), $id)); } $response = $this->checkACL($action, $entity); @@ -508,17 +509,36 @@ class CRUDController extends AbstractController if ($response instanceof Response) { return $response; } - - $defaultTemplateParameters = [ - 'entity' => $entity, - 'crud_name' => $this->getCrudName() - ]; - - return $this->render( - $this->getTemplateFor($action, $entity, $request), - $this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters) + + if ($_format === 'html') { + $defaultTemplateParameters = [ + 'entity' => $entity, + 'crud_name' => $this->getCrudName() + ]; + + return $this->render( + $this->getTemplateFor($action, $entity, $request), + $this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters) ); + } elseif ($_format === 'json') { + $context = $this->getContextForSerialization($action, $request, $entity, $_format); + + return $this->json($entity, Response::HTTP_OK, [], $context); + } else { + throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This format is not implemented"); + } + } + + /** + * Get the context for the serialization + */ + public function getContextForSerialization(string $action, Request $request, $entity, string $_format): array + { + return []; + } + + /** * The edit action. @@ -799,7 +819,7 @@ class CRUDController extends AbstractController */ protected function getRoleFor($action) { - if (NULL !== ($this->getActionConfig($action)['role'])) { + if (\array_key_exists('role', $this->getActionConfig($action))) { return $this->getActionConfig($action)['role']; } @@ -1181,6 +1201,7 @@ class CRUDController extends AbstractController AuthorizationHelper::class => AuthorizationHelper::class, EventDispatcherInterface::class => EventDispatcherInterface::class, Resolver::class => Resolver::class, + SerializerInterface::class => SerializerInterface::class, ] ); } diff --git a/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php b/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php index 5a0eb405e..69b9fca7e 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php +++ b/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php @@ -23,6 +23,9 @@ namespace Chill\MainBundle\CRUD\Routing; use Symfony\Component\Config\Loader\Loader; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\HttpFoundation\Request; +use Chill\MainBundle\CRUD\Controller\ApiController; +use Chill\MainBundle\CRUD\Controller\CRUDController; /** * Class CRUDRoutesLoader @@ -32,24 +35,34 @@ use Symfony\Component\Routing\RouteCollection; */ class CRUDRoutesLoader extends Loader { - /** - * @var array - */ - protected $config = []; + protected array $crudConfig = []; + + protected array $apiCrudConfig = []; /** * @var bool */ private $isLoaded = false; + + private const ALL_SINGLE_METHODS = [ + Request::METHOD_GET, + Request::METHOD_POST, + Request::METHOD_PUT, + Request::METHOD_DELETE + ]; + + private const ALL_INDEX_METHODS = [ Request::METHOD_GET, Request::METHOD_HEAD ]; /** * CRUDRoutesLoader constructor. * - * @param $config + * @param $crudConfig the config from cruds + * @param $apicrudConfig the config from api_crud */ - public function __construct($config) + public function __construct(array $crudConfig, array $apiConfig) { - $this->config = $config; + $this->crudConfig = $crudConfig; + $this->apiConfig = $apiConfig; } /** @@ -63,51 +76,132 @@ class CRUDRoutesLoader extends Loader } /** - * @return RouteCollection + * Load routes for CRUD and CRUD Api */ - public function load($resource, $type = null) + public function load($resource, $type = null): RouteCollection { - if (true === $this->isLoaded) { throw new \RuntimeException('Do not add the "CRUD" loader twice'); } - + $collection = new RouteCollection(); - foreach ($this->config as $config) { - $collection->addCollection($this->loadConfig($config)); + foreach ($this->crudConfig as $crudConfig) { + $collection->addCollection($this->loadCrudConfig($crudConfig)); } - + foreach ($this->apiConfig as $crudConfig) { + $collection->addCollection($this->loadApiSingle($crudConfig)); + //$collection->addCollection($this->loadApiMulti($crudConfig)); + } + return $collection; } /** - * @param $config + * Load routes for CRUD (without api) + * + * @param $crudConfig * @return RouteCollection */ - protected function loadConfig($config): RouteCollection + protected function loadCrudConfig($crudConfig): RouteCollection { $collection = new RouteCollection(); - foreach ($config['actions'] as $name => $action) { + $controller = $crudConfig['controller'] === CrudController::class ? + 'cscrud_'.$crudConfig['name'].'_controller' : $crudConfig['controller']; + + foreach ($crudConfig['actions'] as $name => $action) { + // defaults (controller name) $defaults = [ - '_controller' => 'cscrud_'.$config['name'].'_controller'.':'.($action['controller_action'] ?? $name) + '_controller' => $controller.':'.($action['controller_action'] ?? $name) ]; if ($name === 'index') { - $path = "{_locale}".$config['base_path']; + $path = "{_locale}".$crudConfig['base_path']; $route = new Route($path, $defaults); } elseif ($name === 'new') { - $path = "{_locale}".$config['base_path'].'/'.$name; + $path = "{_locale}".$crudConfig['base_path'].'/'.$name; $route = new Route($path, $defaults); } else { - $path = "{_locale}".$config['base_path'].($action['path'] ?? '/{id}/'.$name); + $path = "{_locale}".$crudConfig['base_path'].($action['path'] ?? '/{id}/'.$name); $requirements = $action['requirements'] ?? [ '{id}' => '\d+' ]; $route = new Route($path, $defaults, $requirements); } - $collection->add('chill_crud_'.$config['name'].'_'.$name, $route); + $collection->add('chill_crud_'.$crudConfig['name'].'_'.$name, $route); + } + + return $collection; + } + + /** + * Load routes for api single + * + * @param $crudConfig + * @return RouteCollection + */ + protected function loadApiSingle(array $crudConfig): RouteCollection + { + $collection = new RouteCollection(); + $controller = $crudConfig['controller'] === ApiController::class ? + 'cscrud_'.$crudConfig['name'].'_controller' : $crudConfig['controller']; + + foreach ($crudConfig['actions'] as $name => $action) { + // filter only on single actions + $singleCollection = $action['single-collection'] ?? $name === '_entity' ? 'single' : NULL; + if ('collection' === $singleCollection) { + continue; + } + + $defaults = [ + '_controller' => $controller.':'.($action['controller_action'] ?? '_entity' === $name ? 'entityApi' : $name.'Api') + ]; + + // path are rewritten + // if name === 'default', we rewrite it to nothing :-) + $localName = '_entity' === $name ? '' : $name; + $localPath = $action['path'] ?? '/{id}'.$localName.'.{_format}'; + $path = $crudConfig['base_path'].$localPath; + + $requirements = $action['requirements'] ?? [ '{id}' => '\d+' ]; + + $methods = \array_keys(\array_filter($action['methods'], function($value, $key) { return $value; }, + ARRAY_FILTER_USE_BOTH)); + + $route = new Route($path, $defaults, $requirements); + $route->setMethods($methods); + + $collection->add('chill_api_single_'.$crudConfig['name'].'_'.$name, $route); + } + + return $collection; + } + + /** + * Load routes for api multi + * + * @param $crudConfig + * @return RouteCollection + */ + protected function loadApiMultiConfig(array $crudConfig): RouteCollection + { + $collection = new RouteCollection(); + foreach ($crudConfig['actions_multi'] as $name => $action) { + // we compute the data from configuration to a local form + $defaults = [ + '_controller' => 'cscrud_'.$crudConfig['name'].'_controller'.':'.($action['controller_action'] ?? $name.'Api') + ]; + // path are rewritten + // if name === 'index', we rewrite it to nothing :-) + $localName = 'index' === $name ? '' : $name; + $localPath = $action['path'] ?? '.{_format}'; + $path = $crudConfig['base_path'].$localPath.$name; + $requirements = $action['requirements'] ?? [ '{id}' => '\d+' ]; + $methods = $name === 'default' ? self::ALL_MULTI_METHODS: []; + $route = new Route($path, $defaults, $requirements); + + $collection->add('chill_api_multi'.$crudConfig['name'].'_'.$name, $route); } return $collection; diff --git a/src/Bundle/ChillMainBundle/ChillMainBundle.php b/src/Bundle/ChillMainBundle/ChillMainBundle.php index abfcdd6fd..fba4db2cf 100644 --- a/src/Bundle/ChillMainBundle/ChillMainBundle.php +++ b/src/Bundle/ChillMainBundle/ChillMainBundle.php @@ -14,6 +14,7 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompile use Chill\MainBundle\DependencyInjection\CompilerPass\MenuCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\ACLFlagsCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\GroupingCenterCompilerPass; +use Chill\MainBundle\DependencyInjection\CompilerPass\CRUDControllerCompilerPass; use Chill\MainBundle\Templating\Entity\CompilerPass as RenderEntityCompilerPass; @@ -33,5 +34,6 @@ class ChillMainBundle extends Bundle $container->addCompilerPass(new ACLFlagsCompilerPass()); $container->addCompilerPass(new GroupingCenterCompilerPass()); $container->addCompilerPass(new RenderEntityCompilerPass()); + $container->addCompilerPass(new CRUDControllerCompilerPass()); } } diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 234e1203d..da3f27d8b 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -133,7 +133,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface, $loader->load('services/search.yaml'); $loader->load('services/serializer.yaml'); - $this->configureCruds($container, $config['cruds'], $loader); + $this->configureCruds($container, $config['cruds'], $config['apis'], $loader); } /** @@ -214,47 +214,91 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface, * @param array $config the config under 'cruds' key * @return null */ - protected function configureCruds(ContainerBuilder $container, $config, Loader\YamlFileLoader $loader) + protected function configureCruds(ContainerBuilder $container, $crudConfig, $apiConfig, Loader\YamlFileLoader $loader) { - if (count($config) === 0) { + if ((count($crudConfig) + count($apiConfig)) === 0) { return; } +// dump(array_keys($container->getDefinitions())); + $loader->load('services/crud.yaml'); - $container->setParameter('chill_main_crud_route_loader_config', $config); - + $container->setParameter('chill_main_crud_route_loader_config', $crudConfig); + $container->setParameter('chill_main_api_route_loader_config', $apiConfig); + return; +/* $definition = new Definition(); $definition ->setClass(\Chill\MainBundle\CRUD\Routing\CRUDRoutesLoader::class) ->addArgument('%chill_main_crud_route_loader_config%') + ->addArgument('%chill_main_api_route_loader_config%') ; $container->setDefinition('chill_main_crud_route_loader', $definition); + */ $alreadyExistingNames = []; - foreach ($config as $crudEntry) { - $controller = $crudEntry['controller']; - $controllerServiceName = 'cscrud_'.$crudEntry['name'].'_controller'; - $name = $crudEntry['name']; + foreach ($crudConfig as $crudEntry) { + $this->configureCrudController($container, $crudEntry, 'crud'); + } - // check for existing crud names - if (\in_array($name, $alreadyExistingNames)) { - throw new LogicException(sprintf("the name %s is defined twice in CRUD", $name)); - } - - if (!$container->has($controllerServiceName)) { - $controllerDefinition = new Definition($controller); - $controllerDefinition->addTag('controller.service_arguments'); - $controllerDefinition->setAutoconfigured(true); - $controllerDefinition->setClass($crudEntry['controller']); - $container->setDefinition($controllerServiceName, $controllerDefinition); - } - - $container->setParameter('chill_main_crud_config_'.$name, $crudEntry); - $container->getDefinition($controllerServiceName) - ->addMethodCall('setCrudConfig', ['%chill_main_crud_config_'.$name.'%']); + foreach ($apiConfig as $crudEntry) { + $this->configureCrudController($container, $crudEntry, 'crud'); } } + + /** + * Add a controller for each definition, and add a methodCall to inject crud configuration to controller + */ + private function configureCrudController(ContainerBuilder $container, array $crudEntry, string $apiOrCrud): void + { + $controller = $crudEntry['controller']; + $controllerServiceName = 'cscrud_'.$crudEntry['name'].'_controller'; + $name = $crudEntry['name']; + + // check for existing crud names + /*if (\in_array($name, $alreadyExistingNames)) { + throw new LogicException(sprintf("the name %s is defined twice in CRUD", $name)); + }*/ + + if (!$container->has($controllerServiceName)) { + $controllerDefinition = new Definition($controller); + $controllerDefinition->addTag('controller.service_arguments'); + $controllerDefinition->setAutoconfigured(true); + $controllerDefinition->setClass($crudEntry['controller']); + $container->setDefinition($controllerServiceName, $controllerDefinition); + } + + $container->setParameter('chill_main_'.$apiOrCrud.'_config_'.$name, $crudEntry); + $container->getDefinition($controllerServiceName) + ->addMethodCall('setCrudConfig', ['%chill_main_'.$apiOrCrud.'_config_'.$name.'%']); +/* + dump($controllerClass); + + if ($container->hasDefinition($controllerClass)) { + dump('container has controller class'); + $controllerServiceName = $controllerClass; + $controller = $container->getDefinition($controllerServiceName); + $param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name']; + $container->setParameter($param, $crudEntry); + $controller->addMethodCall('setCrudConfig', ['%'.$param.'%']); + dump(__LINE__, $controller); + $controller->setDefinition($controllerServiceName, $controller); + } else { + $controllerServiceName = 'cs'.$apiOrCrud.'_'.$crudEntry['name'].'_controller'; + $controller = new Definition($controllerClass); + $controller->addTag('controller.service_arguments'); + $controller->setAutoconfigured(true); + $controller->setClass($controllerClass); + $param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name']; + $container->setParameter($param, $crudEntry); + $controller->addMethodCall('setCrudConfig', ['%'.$param.'%']); + dump(__LINE__, $controller); + $container->setDefinition($controllerServiceName, $controller); + }*/ + + } + } diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/CRUDControllerCompilerPass.php b/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/CRUDControllerCompilerPass.php new file mode 100644 index 000000000..77ef023dd --- /dev/null +++ b/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/CRUDControllerCompilerPass.php @@ -0,0 +1,103 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\MainBundle\DependencyInjection\CompilerPass; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Reference; +use Chill\MainBundle\Routing\MenuComposer; +use Symfony\Component\DependencyInjection\Definition; + +/** + * + * + */ +class CRUDControllerCompilerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + $crudConfig = $container->getParameter('chill_main_crud_route_loader_config'); + $apiConfig = $container->getParameter('chill_main_api_route_loader_config'); + + + foreach ($crudConfig as $crudEntry) { + $this->configureCrudController($container, $crudEntry, 'crud'); + } + + foreach ($apiConfig as $crudEntry) { + $this->configureCrudController($container, $crudEntry, 'crud'); + } + } + + /** + * Add a controller for each definition, and add a methodCall to inject crud configuration to controller + */ + private function configureCrudController(ContainerBuilder $container, array $crudEntry, string $apiOrCrud): void + { + $controller = $crudEntry['controller']; + $controllerServiceName = 'cscrud_'.$crudEntry['name'].'_controller'; + $name = $crudEntry['name']; + + // check for existing crud names + /*if (\in_array($name, $alreadyExistingNames)) { + throw new LogicException(sprintf("the name %s is defined twice in CRUD", $name)); + } + + if (!$container->has($controllerServiceName)) { + $controllerDefinition = new Definition($controller); + $controllerDefinition->addTag('controller.service_arguments'); + $controllerDefinition->setAutoconfigured(true); + $controllerDefinition->setClass($crudEntry['controller']); + $container->setDefinition($controllerServiceName, $controllerDefinition); + } + + $container->setParameter('chill_main_'.$apiOrCrud.'_config_'.$name, $crudEntry); + $container->getDefinition($controllerServiceName) + ->addMethodCall('setCrudConfig', ['%chill_main_'.$apiOrCrud.'_config_'.$name.'%']); +/* + dump($controllerClass); + */ + + $controllerClass = $crudEntry['controller']; + dump('in_array', $controllerClass, \in_array($controllerClass, \array_keys($container->getDefinitions()))); + + if ($container->hasDefinition($controllerClass)) { + dump('container has controller class'); + $controllerServiceName = $controllerClass; + $controller = $container->getDefinition($controllerServiceName); + $param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name']; + $container->setParameter($param, $crudEntry); + $controller->addMethodCall('setCrudConfig', ['%'.$param.'%']); + dump(__LINE__, $controller); + $controller->setDefinition($controllerServiceName, $controller); + } else { + $controllerServiceName = 'cs'.$apiOrCrud.'_'.$crudEntry['name'].'_controller'; + $controller = new Definition($controllerClass); + $controller->addTag('controller.service_arguments'); + $controller->setAutoconfigured(true); + $controller->setClass($controllerClass); + $param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name']; + $container->setParameter($param, $crudEntry); + $controller->addMethodCall('setCrudConfig', ['%'.$param.'%']); + dump(__LINE__, $controller); + $container->setDefinition($controllerServiceName, $controller); + } + + } + +} diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php index d5024bf5d..a7b15a069 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php @@ -8,6 +8,7 @@ use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Chill\MainBundle\DependencyInjection\Widget\AddWidgetConfigurationTrait; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpFoundation\Request; /** @@ -140,7 +141,7 @@ class Configuration implements ConfigurationInterface ->scalarNode('controller_action') ->defaultNull() ->info('the method name to call in the route. Will be set to the action name if left empty.') - ->example("'action'") + ->example("action") ->end() ->scalarNode('path') ->defaultNull() @@ -168,6 +169,72 @@ class Configuration implements ConfigurationInterface ->end() ->end() + + + ->arrayNode('apis') + ->defaultValue([]) + ->arrayPrototype() + ->children() + ->scalarNode('class')->cannotBeEmpty()->isRequired()->end() + ->scalarNode('controller') + ->cannotBeEmpty() + ->defaultValue(\Chill\MainBundle\CRUD\Controller\ApiController::class) + ->end() + ->scalarNode('name')->cannotBeEmpty()->isRequired()->end() + ->scalarNode('base_path')->cannotBeEmpty()->isRequired()->end() + ->scalarNode('base_role')->defaultNull()->end() + ->arrayNode('actions') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->scalarNode('controller_action') + ->defaultNull() + ->info('the method name to call in the route. Will be set to the concatenation of action name + \'Api\' if left empty.') + ->example("showApi") + ->end() + ->scalarNode('path') + ->defaultNull() + ->info('the path that will be **appended** after the base path. Do not forget to add ' + . 'arguments for the method. By default, will set to the action name, including an `{id}` ' + . 'parameter. A suffix of action name will be appended, except if the action name is "entity".') + ->example('/{id}/my-action') + ->end() + ->arrayNode('requirements') + ->ignoreExtraKeys(false) + ->info('the requirements for the route. Will be set to `[ \'id\' => \'\d+\' ]` if left empty.') + ->end() + ->enumNode('single-collection') + ->values(['single', 'collection']) + ->info('indicates if the returned object is a single element or a collection') + ->end() + ->arrayNode('methods') + ->addDefaultsIfNotSet() + ->info('the allowed methods') + ->children() + ->booleanNode(Request::METHOD_GET)->defaultTrue()->end() + ->booleanNode(Request::METHOD_HEAD)->defaultTrue()->end() + ->booleanNode(Request::METHOD_POST)->defaultFalse()->end() + ->booleanNode(Request::METHOD_DELETE)->defaultFalse()->end() + ->booleanNode(Request::METHOD_PUT)->defaultFalse()->end() + ->end() + ->end() + ->arrayNode('roles') + ->info("The role require for each http method") + ->children() + ->scalarNode(Request::METHOD_GET)->defaultNull()->end() + ->scalarNode(Request::METHOD_HEAD)->defaultNull()->end() + ->scalarNode(Request::METHOD_POST)->defaultNull()->end() + ->scalarNode(Request::METHOD_DELETE)->defaultNull()->end() + ->scalarNode(Request::METHOD_PUT)->defaultNull()->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + + ->end() ->end() // end of root/children ->end() // end of root ; diff --git a/src/Bundle/ChillMainBundle/config/services/crud.yaml b/src/Bundle/ChillMainBundle/config/services/crud.yaml index 8723d6eb1..c5c05f344 100644 --- a/src/Bundle/ChillMainBundle/config/services/crud.yaml +++ b/src/Bundle/ChillMainBundle/config/services/crud.yaml @@ -1,7 +1,8 @@ services: Chill\MainBundle\CRUD\Routing\CRUDRoutesLoader: arguments: - $config: '%chill_main_crud_route_loader_config%' + $crudConfig: '%chill_main_crud_route_loader_config%' + $apiConfig: '%chill_main_api_route_loader_config%' tags: [ routing.loader ] Chill\MainBundle\CRUD\Resolver\Resolver: @@ -13,4 +14,4 @@ services: arguments: $resolver: '@Chill\MainBundle\CRUD\Resolver\Resolver' tags: - - { name: twig.extension } \ No newline at end of file + - { name: twig.extension } diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php new file mode 100644 index 000000000..a5f068c30 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php @@ -0,0 +1,48 @@ +getEntity($accompanyingPeriodId); + $person = $this->serializer->deserialize($request->getContent(), Person::class, $_format, []); + + if (NULL === $person) { + throw new BadRequestException('person id not found'); + } + + // TODO add acl + switch ($request->getMethod()) { + case Request::METHOD_POST: + $participation = $accompanyingCours->addPerson($person); + break; + case Request::METHOD_DELETE: + $participation = $accompanyingCours->removePerson($person); + break; + default: + throw new BadRequestException("This method is not supported"); + } + + $errors = $this->validator->validate($accompanyingCourse); + + if ($errors->count() > 0) { + // only format accepted + return $this->json($errors); + } + + $this->getDoctrine()->getManager()->flush(); + + return $this->json($participation); + } +} diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 9eaa5d17f..085fedc79 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -28,6 +28,7 @@ use Chill\MainBundle\DependencyInjection\MissingBundleException; use Chill\PersonBundle\Security\Authorization\PersonVoter; use Chill\MainBundle\Security\Authorization\ChillExportVoter; use Chill\PersonBundle\Doctrine\DQL\AddressPart; +use Symfony\Component\HttpFoundation\Request; /** * Class ChillPersonExtension @@ -76,6 +77,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac $loader->load('services/templating.yaml'); $loader->load('services/alt_names.yaml'); $loader->load('services/serializer.yaml'); + $loader->load('services/security.yaml'); // load service advanced search only if configure if ($config['search']['search_by_phone'] != 'never') { @@ -307,6 +309,34 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac 'template' => '@ChillPerson/MaritalStatus/edit.html.twig', ] ] + ], + ], + 'apis' => [ + [ + 'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod::class, + 'name' => 'accompanying_course', + 'base_path' => '/api/1.0/accompanying_course', + 'controller' => \Chill\PersonBundle\Controller\AccompanyingCourseApiController::class, + 'actions' => [ + '_entity' => [ + 'roles' => [ + Request::METHOD_GET => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE + ] + ], + '/participation' => [ + 'methods' => [ + Request::METHOD_POST => true, + Request::METHOD_DELETE => true, + Request::METHOD_GET => false, + Request::METHOD_HEAD => false, + ], + 'roles' => [ + Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE, + Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE + ] + ] + + ] ] ] ]); diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index 18ee535b8..46028f028 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -344,50 +344,68 @@ class AccompanyingPeriod } /** - * This private function scan Participations Collection, - * searching for a given Person + * Get the participation containing a person */ - private function participationsContainsPerson(Person $person): ?AccompanyingPeriodParticipation + public function getParticipationsContainsPerson(Person $person): Collection { - foreach ($this->participations as $participation) { - /** @var AccompanyingPeriodParticipation $participation */ - if ($person === $participation->getPerson()) { - return $participation; - }} - - return null; + return $this->getParticipations()->filter( + function(AccompanyingPeriodParticipation $participation) use ($person) { + if ($person === $participation->getPerson()) { + return $participation; + } + }); } /** - * This public function is the same but return only true or false + * Get the opened participation containing a person + * + * "Open" means that the closed date is NULL + */ + public function getOpenParticipationsContainsPerson(Person $person): ?AccompanyingPeriodParticipation + { + $collection = $this->getParticipationsContainsPerson()->filter( + function(AccompanyingPeriodParticipation $participation) use ($person) { + if (NULL === $participation->getClosingDate()) { + return $participation; + } + }); + + return $collection->count() > 0 ? $collection->first() : NULL; + } + + /** + * Return true if the accompanying period contains a person. + * + * **Note**: this participation can be opened or not. */ public function containsPerson(Person $person): bool { - return ($this->participationsContainsPerson($person) === null) ? false : true; + return $this->participationsContainsPerson($person)->count() > 0; } /** * Add Person */ - public function addPerson(Person $person = null): self + public function addPerson(Person $person = null): AccompanyingPeriodParticipation { $participation = new AccompanyingPeriodParticipation($this, $person); $this->participations[] = $participation; - return $this; + return $participation; } /** * Remove Person */ - public function removePerson(Person $person): void + public function removePerson(Person $person): ?AccompanyingPeriodParticipation { - $participation = $this->participationsContainsPerson($person); + $participation = $this->getOpenParticipationContainsPerson($person); if (! null === $participation) { $participation->setEndDate(new \DateTimeImmutable('now')); - $this->participations->removeElement($participation); } + + return $participation; } diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php new file mode 100644 index 000000000..ca92c6d7c --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php @@ -0,0 +1,73 @@ +helper = $helper; + } + + protected function supports($attribute, $subject) + { + return $subject instanceof AccompanyingPeriod; + } + + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + { + if (!$token->getUser() instanceof User) { + return false; + } + + // TODO take scopes into account + foreach ($subject->getPersons() as $person) { + // give access as soon as on center is reachable + if ($this->helper->userHasAccess($token->getUser(), $person->getCenter(), $attribute)) { + return true; + } + + return false; + } + } + + private function getAttributes() + { + return [ + self::SEE + ]; + } + + public function getRoles() + { + return $this->getAttributes(); + } + + public function getRolesWithoutScope() + { + return []; + } + + public function getRolesWithHierarchy() + { + return [ 'Person' => $this->getRoles() ]; + } + +} diff --git a/src/Bundle/ChillPersonBundle/config/services.yaml b/src/Bundle/ChillPersonBundle/config/services.yaml index bccb69b0d..f986a4ccb 100644 --- a/src/Bundle/ChillPersonBundle/config/services.yaml +++ b/src/Bundle/ChillPersonBundle/config/services.yaml @@ -27,14 +27,6 @@ services: public: true tags: - { name: chill.timeline, context: 'person' } - - chill.person.security.authorization.person: - class: Chill\PersonBundle\Security\Authorization\PersonVoter - arguments: - - "@chill.main.security.authorization.helper" - tags: - - { name: security.voter } - - { name: chill.role } chill.person.birthdate_validation: class: Chill\PersonBundle\Validator\Constraints\BirthdateValidator diff --git a/src/Bundle/ChillPersonBundle/config/services/security.yaml b/src/Bundle/ChillPersonBundle/config/services/security.yaml new file mode 100644 index 000000000..21590fcda --- /dev/null +++ b/src/Bundle/ChillPersonBundle/config/services/security.yaml @@ -0,0 +1,16 @@ +services: + chill.person.security.authorization.person: + class: Chill\PersonBundle\Security\Authorization\PersonVoter + arguments: + - "@chill.main.security.authorization.helper" + tags: + - { name: security.voter } + - { name: chill.role } + + Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter: + arguments: + - "@chill.main.security.authorization.helper" + tags: + - { name: security.voter } + - { name: chill.role } + diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 1fe6e3956..ab3a5bfe6 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -190,6 +190,7 @@ CHILL_PERSON_CREATE: Ajouter des personnes CHILL_PERSON_STATS: Statistiques sur les personnes CHILL_PERSON_LISTS: Liste des personnes CHILL_PERSON_DUPLICATE: Gérer les doublons de personnes +CHILL_PERSON_ACCOMPANYING_PERIOD_SEE: Voir les périodes d'accompagnement #period Period closed!: Période clôturée! From 07e06927831b85e5b2fdb6b1f3b36e14cf30ac71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 May 2021 00:14:36 +0200 Subject: [PATCH 28/42] bootstrap api and apply on accompanying period --- .../CRUDControllerCompilerPass.php | 76 +++++++++++++ .../Controller/AbstractCRUDController.php | 19 +++- .../CRUD/Controller/ApiController.php | 19 +--- .../CRUD/Routing/CRUDRoutesLoader.php | 8 +- .../ChillMainBundle/ChillMainBundle.php | 2 +- .../ChillMainExtension.php | 91 ++-------------- .../CRUDControllerCompilerPass.php | 103 ------------------ .../AccompanyingCourseApiController.php | 47 ++++++-- .../DataFixtures/ORM/LoadPersonACL.php | 8 ++ .../ChillPersonExtension.php | 4 +- .../Entity/AccompanyingPeriod.php | 12 +- .../AccompanyingPeriodNormalizer.php | 2 +- .../Normalizer/PersonNormalizer.php | 2 +- ...> AccompanyingCourseApiControllerTest.php} | 58 ++++++++-- .../Tests/Entity/AccompanyingPeriodTest.php | 35 +++--- .../config/services/controller.yaml | 6 + 16 files changed, 235 insertions(+), 257 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/CRUD/CompilerPass/CRUDControllerCompilerPass.php delete mode 100644 src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/CRUDControllerCompilerPass.php rename src/Bundle/ChillPersonBundle/Tests/Controller/{AccompanyingCourseControllerTest.php => AccompanyingCourseApiControllerTest.php} (71%) diff --git a/src/Bundle/ChillMainBundle/CRUD/CompilerPass/CRUDControllerCompilerPass.php b/src/Bundle/ChillMainBundle/CRUD/CompilerPass/CRUDControllerCompilerPass.php new file mode 100644 index 000000000..43424d4f9 --- /dev/null +++ b/src/Bundle/ChillMainBundle/CRUD/CompilerPass/CRUDControllerCompilerPass.php @@ -0,0 +1,76 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\MainBundle\CRUD\CompilerPass; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Reference; +use Chill\MainBundle\Routing\MenuComposer; +use Symfony\Component\DependencyInjection\Definition; + +/** + * + * + */ +class CRUDControllerCompilerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + $crudConfig = $container->getParameter('chill_main_crud_route_loader_config'); + $apiConfig = $container->getParameter('chill_main_api_route_loader_config'); + + foreach ($crudConfig as $crudEntry) { + $this->configureCrudController($container, $crudEntry, 'crud'); + } + + foreach ($apiConfig as $crudEntry) { + $this->configureCrudController($container, $crudEntry, 'api'); + } + } + + /** + * Add a controller for each definition, and add a methodCall to inject crud configuration to controller + */ + private function configureCrudController(ContainerBuilder $container, array $crudEntry, string $apiOrCrud): void + { + $controllerClass = $crudEntry['controller']; + + $controllerServiceName = 'cs'.$apiOrCrud.'_'.$crudEntry['name'].'_controller'; + + if ($container->hasDefinition($controllerClass)) { + $controller = $container->getDefinition($controllerClass); + $container->removeDefinition($controllerClass); + $alreadyDefined = true; + } else { + $controller = new Definition($controllerClass); + $alreadyDefined = false; + } + + $controller->addTag('controller.service_arguments'); + if (FALSE === $alreadyDefined) { + $controller->setAutoconfigured(true); + } + + $param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name']; + $container->setParameter($param, $crudEntry); + $controller->addMethodCall('setCrudConfig', ['%'.$param.'%']); + + $container->setDefinition($controllerServiceName, $controller); + } + +} diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php index 363d65152..31141a857 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php @@ -33,6 +33,7 @@ class AbstractCRUDController extends AbstractController } /** + * Get the complete FQDN of the class * * @return string the complete fqdn of the class */ @@ -42,7 +43,7 @@ class AbstractCRUDController extends AbstractController } /** - * + * called on post fetch entity */ protected function onPostFetchEntity(string $action, Request $request, $entity, $_format): ?Response { @@ -50,14 +51,13 @@ class AbstractCRUDController extends AbstractController } /** - * + * Called on post check ACL */ protected function onPostCheckACL(string $action, Request $request, $entity, $_format): ?Response { return null; } - /** * check the acl. Called by every action. * @@ -74,12 +74,20 @@ class AbstractCRUDController extends AbstractController $this->denyAccessUnlessGranted($this->getRoleFor($action, $request, $entity, $_format), $entity); } + /** + * + * @return string the crud name + */ + protected function getCrudName(): string + { + return $this->crudConfig['name']; + } + protected function getActionConfig(string $action) { return $this->crudConfig['actions'][$action]; } - - + /** * Set the crud configuration * @@ -87,7 +95,6 @@ class AbstractCRUDController extends AbstractController */ public function setCrudConfig(array $config): void { - dump($config); $this->crudConfig = $config; } diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php index c12c8e2b7..7256720bb 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php @@ -5,6 +5,7 @@ namespace Chill\MainBundle\CRUD\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Serializer\SerializerInterface; class ApiController extends AbstractCRUDController { @@ -114,22 +115,6 @@ class ApiController extends AbstractCRUDController protected function getSerializer(): SerializerInterface { - return $this->get(SerializerInterface::class); + return $this->get('serializer'); } - - /** - * Defined the services necessary for this controller - * - * @return array - */ - public static function getSubscribedServices(): array - { - return \array_merge( - parent::getSubscribedServices(), - [ - SerializerInterface::class => SerializerInterface::class, - ] - ); - } - } diff --git a/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php b/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php index 69b9fca7e..b09bbc55b 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php +++ b/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php @@ -106,8 +106,7 @@ class CRUDRoutesLoader extends Loader protected function loadCrudConfig($crudConfig): RouteCollection { $collection = new RouteCollection(); - $controller = $crudConfig['controller'] === CrudController::class ? - 'cscrud_'.$crudConfig['name'].'_controller' : $crudConfig['controller']; + $controller ='cscrud_'.$crudConfig['name'].'_controller'; foreach ($crudConfig['actions'] as $name => $action) { // defaults (controller name) @@ -144,8 +143,7 @@ class CRUDRoutesLoader extends Loader protected function loadApiSingle(array $crudConfig): RouteCollection { $collection = new RouteCollection(); - $controller = $crudConfig['controller'] === ApiController::class ? - 'cscrud_'.$crudConfig['name'].'_controller' : $crudConfig['controller']; + $controller ='csapi_'.$crudConfig['name'].'_controller'; foreach ($crudConfig['actions'] as $name => $action) { // filter only on single actions @@ -160,7 +158,7 @@ class CRUDRoutesLoader extends Loader // path are rewritten // if name === 'default', we rewrite it to nothing :-) - $localName = '_entity' === $name ? '' : $name; + $localName = '_entity' === $name ? '' : '/'.$name; $localPath = $action['path'] ?? '/{id}'.$localName.'.{_format}'; $path = $crudConfig['base_path'].$localPath; diff --git a/src/Bundle/ChillMainBundle/ChillMainBundle.php b/src/Bundle/ChillMainBundle/ChillMainBundle.php index fba4db2cf..51ef344f2 100644 --- a/src/Bundle/ChillMainBundle/ChillMainBundle.php +++ b/src/Bundle/ChillMainBundle/ChillMainBundle.php @@ -14,7 +14,7 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompile use Chill\MainBundle\DependencyInjection\CompilerPass\MenuCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\ACLFlagsCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\GroupingCenterCompilerPass; -use Chill\MainBundle\DependencyInjection\CompilerPass\CRUDControllerCompilerPass; +use Chill\MainBundle\CRUD\CompilerPass\CRUDControllerCompilerPass; use Chill\MainBundle\Templating\Entity\CompilerPass as RenderEntityCompilerPass; diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index da3f27d8b..00b164303 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -133,7 +133,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface, $loader->load('services/search.yaml'); $loader->load('services/serializer.yaml'); - $this->configureCruds($container, $config['cruds'], $config['apis'], $loader); + $this->configureCruds($container, $config['cruds'], $config['apis'], $loader); } /** @@ -210,95 +210,24 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface, } /** - * @param ContainerBuilder $container - * @param array $config the config under 'cruds' key - * @return null + * Load parameter for configuration and set parameters for api */ - protected function configureCruds(ContainerBuilder $container, $crudConfig, $apiConfig, Loader\YamlFileLoader $loader) + protected function configureCruds( + ContainerBuilder $container, + array $crudConfig, + array $apiConfig, + Loader\YamlFileLoader $loader + ): void { - if ((count($crudConfig) + count($apiConfig)) === 0) { + if (count($crudConfig) === 0) { return; } -// dump(array_keys($container->getDefinitions())); - $loader->load('services/crud.yaml'); $container->setParameter('chill_main_crud_route_loader_config', $crudConfig); $container->setParameter('chill_main_api_route_loader_config', $apiConfig); - return; -/* - $definition = new Definition(); - $definition - ->setClass(\Chill\MainBundle\CRUD\Routing\CRUDRoutesLoader::class) - ->addArgument('%chill_main_crud_route_loader_config%') - ->addArgument('%chill_main_api_route_loader_config%') - ; - $container->setDefinition('chill_main_crud_route_loader', $definition); - */ - - $alreadyExistingNames = []; - - foreach ($crudConfig as $crudEntry) { - $this->configureCrudController($container, $crudEntry, 'crud'); - } - - foreach ($apiConfig as $crudEntry) { - $this->configureCrudController($container, $crudEntry, 'crud'); - } + // Note: the controller are loaded inside compiler pass } - - /** - * Add a controller for each definition, and add a methodCall to inject crud configuration to controller - */ - private function configureCrudController(ContainerBuilder $container, array $crudEntry, string $apiOrCrud): void - { - $controller = $crudEntry['controller']; - $controllerServiceName = 'cscrud_'.$crudEntry['name'].'_controller'; - $name = $crudEntry['name']; - - // check for existing crud names - /*if (\in_array($name, $alreadyExistingNames)) { - throw new LogicException(sprintf("the name %s is defined twice in CRUD", $name)); - }*/ - - if (!$container->has($controllerServiceName)) { - $controllerDefinition = new Definition($controller); - $controllerDefinition->addTag('controller.service_arguments'); - $controllerDefinition->setAutoconfigured(true); - $controllerDefinition->setClass($crudEntry['controller']); - $container->setDefinition($controllerServiceName, $controllerDefinition); - } - - $container->setParameter('chill_main_'.$apiOrCrud.'_config_'.$name, $crudEntry); - $container->getDefinition($controllerServiceName) - ->addMethodCall('setCrudConfig', ['%chill_main_'.$apiOrCrud.'_config_'.$name.'%']); -/* - dump($controllerClass); - - if ($container->hasDefinition($controllerClass)) { - dump('container has controller class'); - $controllerServiceName = $controllerClass; - $controller = $container->getDefinition($controllerServiceName); - $param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name']; - $container->setParameter($param, $crudEntry); - $controller->addMethodCall('setCrudConfig', ['%'.$param.'%']); - dump(__LINE__, $controller); - $controller->setDefinition($controllerServiceName, $controller); - } else { - $controllerServiceName = 'cs'.$apiOrCrud.'_'.$crudEntry['name'].'_controller'; - $controller = new Definition($controllerClass); - $controller->addTag('controller.service_arguments'); - $controller->setAutoconfigured(true); - $controller->setClass($controllerClass); - $param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name']; - $container->setParameter($param, $crudEntry); - $controller->addMethodCall('setCrudConfig', ['%'.$param.'%']); - dump(__LINE__, $controller); - $container->setDefinition($controllerServiceName, $controller); - }*/ - - } - } diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/CRUDControllerCompilerPass.php b/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/CRUDControllerCompilerPass.php deleted file mode 100644 index 77ef023dd..000000000 --- a/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/CRUDControllerCompilerPass.php +++ /dev/null @@ -1,103 +0,0 @@ - - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -namespace Chill\MainBundle\DependencyInjection\CompilerPass; - -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\Reference; -use Chill\MainBundle\Routing\MenuComposer; -use Symfony\Component\DependencyInjection\Definition; - -/** - * - * - */ -class CRUDControllerCompilerPass implements CompilerPassInterface -{ - public function process(ContainerBuilder $container) - { - $crudConfig = $container->getParameter('chill_main_crud_route_loader_config'); - $apiConfig = $container->getParameter('chill_main_api_route_loader_config'); - - - foreach ($crudConfig as $crudEntry) { - $this->configureCrudController($container, $crudEntry, 'crud'); - } - - foreach ($apiConfig as $crudEntry) { - $this->configureCrudController($container, $crudEntry, 'crud'); - } - } - - /** - * Add a controller for each definition, and add a methodCall to inject crud configuration to controller - */ - private function configureCrudController(ContainerBuilder $container, array $crudEntry, string $apiOrCrud): void - { - $controller = $crudEntry['controller']; - $controllerServiceName = 'cscrud_'.$crudEntry['name'].'_controller'; - $name = $crudEntry['name']; - - // check for existing crud names - /*if (\in_array($name, $alreadyExistingNames)) { - throw new LogicException(sprintf("the name %s is defined twice in CRUD", $name)); - } - - if (!$container->has($controllerServiceName)) { - $controllerDefinition = new Definition($controller); - $controllerDefinition->addTag('controller.service_arguments'); - $controllerDefinition->setAutoconfigured(true); - $controllerDefinition->setClass($crudEntry['controller']); - $container->setDefinition($controllerServiceName, $controllerDefinition); - } - - $container->setParameter('chill_main_'.$apiOrCrud.'_config_'.$name, $crudEntry); - $container->getDefinition($controllerServiceName) - ->addMethodCall('setCrudConfig', ['%chill_main_'.$apiOrCrud.'_config_'.$name.'%']); -/* - dump($controllerClass); - */ - - $controllerClass = $crudEntry['controller']; - dump('in_array', $controllerClass, \in_array($controllerClass, \array_keys($container->getDefinitions()))); - - if ($container->hasDefinition($controllerClass)) { - dump('container has controller class'); - $controllerServiceName = $controllerClass; - $controller = $container->getDefinition($controllerServiceName); - $param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name']; - $container->setParameter($param, $crudEntry); - $controller->addMethodCall('setCrudConfig', ['%'.$param.'%']); - dump(__LINE__, $controller); - $controller->setDefinition($controllerServiceName, $controller); - } else { - $controllerServiceName = 'cs'.$apiOrCrud.'_'.$crudEntry['name'].'_controller'; - $controller = new Definition($controllerClass); - $controller->addTag('controller.service_arguments'); - $controller->setAutoconfigured(true); - $controller->setClass($controllerClass); - $param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name']; - $container->setParameter($param, $crudEntry); - $controller->addMethodCall('setCrudConfig', ['%'.$param.'%']); - dump(__LINE__, $controller); - $container->setDefinition($controllerServiceName, $controller); - } - - } - -} diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php index a5f068c30..2716d1c1a 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php @@ -8,33 +8,51 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Symfony\Component\HttpFoundation\Exception\BadRequestException; - +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; +use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent; +use Chill\PersonBundle\Entity\Person; class AccompanyingCourseApiController extends ApiController { - public function participationApi($accompanyingPeriodId, Request $request) + protected EventDispatcherInterface $eventDispatcher; + + protected ValidatorInterface $validator; + + public function __construct(EventDispatcherInterface $eventDispatcher, $validator) + { + $this->eventDispatcher = $eventDispatcher; + $this->validator = $validator; + } + + public function participationApi($id, Request $request, $_format) { /** @var AccompanyingPeriod $accompanyingPeriod */ - $accompanyingPeriod = $this->getEntity($accompanyingPeriodId); - $person = $this->serializer->deserialize($request->getContent(), Person::class, $_format, []); + $accompanyingPeriod = $this->getEntity('participation', $id, $request); + $person = $this->getSerializer() + ->deserialize($request->getContent(), Person::class, $_format, []); if (NULL === $person) { throw new BadRequestException('person id not found'); } // TODO add acl + // + $this->onPostCheckACL('participation', $request, $accompanyingPeriod, $_format); + switch ($request->getMethod()) { case Request::METHOD_POST: - $participation = $accompanyingCours->addPerson($person); + $participation = $accompanyingPeriod->addPerson($person); break; case Request::METHOD_DELETE: - $participation = $accompanyingCours->removePerson($person); + $participation = $accompanyingPeriod->removePerson($person); + $participation->setEndDate(new \DateTimeImmutable('now')); break; default: throw new BadRequestException("This method is not supported"); } - $errors = $this->validator->validate($accompanyingCourse); + $errors = $this->validator->validate($accompanyingPeriod); if ($errors->count() > 0) { // only format accepted @@ -44,5 +62,18 @@ class AccompanyingCourseApiController extends ApiController $this->getDoctrine()->getManager()->flush(); return $this->json($participation); - } + } + + protected function onPostCheckACL(string $action, Request $request, $entity, $_format): ?Response + { + $this->eventDispatcher->dispatch( + AccompanyingPeriodPrivacyEvent::ACCOMPANYING_PERIOD_PRIVACY_EVENT, + new AccompanyingPeriodPrivacyEvent($entity, [ + 'action' => $action, + 'request' => $request->getMethod() + ]) + ); + + return null; + } } diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPersonACL.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPersonACL.php index a3f42ced9..c8593d1fd 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPersonACL.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPersonACL.php @@ -25,6 +25,7 @@ use Doctrine\Persistence\ObjectManager; use Chill\MainBundle\DataFixtures\ORM\LoadPermissionsGroup; use Chill\MainBundle\Entity\RoleScope; use Chill\PersonBundle\Security\Authorization\PersonVoter; +use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; /** * Add a role CHILL_PERSON_UPDATE & CHILL_PERSON_CREATE for all groups except administrative, @@ -44,6 +45,7 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface { foreach (LoadPermissionsGroup::$refs as $permissionsGroupRef) { $permissionsGroup = $this->getReference($permissionsGroupRef); + $scopeSocial = $this->getReference('scope_social'); //create permission group switch ($permissionsGroup->getName()) { @@ -51,6 +53,12 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface case 'direction': printf("Adding CHILL_PERSON_UPDATE & CHILL_PERSON_CREATE to %s permission group \n", $permissionsGroup->getName()); + $permissionsGroup->addRoleScope( + (new RoleScope()) + ->setRole(AccompanyingPeriodVoter::SEE) + ->setScope($scopeSocial) + ); + $roleScopeUpdate = (new RoleScope()) ->setRole('CHILL_PERSON_UPDATE') ->setScope(null); diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 085fedc79..909df750f 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -315,7 +315,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac [ 'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod::class, 'name' => 'accompanying_course', - 'base_path' => '/api/1.0/accompanying_course', + 'base_path' => '/api/1.0/person/accompanying-course', 'controller' => \Chill\PersonBundle\Controller\AccompanyingCourseApiController::class, 'actions' => [ '_entity' => [ @@ -323,7 +323,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac Request::METHOD_GET => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE ] ], - '/participation' => [ + 'participation' => [ 'methods' => [ Request::METHOD_POST => true, Request::METHOD_DELETE => true, diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index 46028f028..8b60888bd 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -118,7 +118,7 @@ class AccompanyingPeriod * * @ORM\OneToMany(targetEntity=AccompanyingPeriodParticipation::class, * mappedBy="accompanyingPeriod", - * cascade={"persist", "remove", "merge", "detach"}) + * cascade={"persist", "refresh", "remove", "merge", "detach"}) */ private $participations; @@ -348,7 +348,7 @@ class AccompanyingPeriod */ public function getParticipationsContainsPerson(Person $person): Collection { - return $this->getParticipations()->filter( + return $this->getParticipations($person)->filter( function(AccompanyingPeriodParticipation $participation) use ($person) { if ($person === $participation->getPerson()) { return $participation; @@ -361,11 +361,11 @@ class AccompanyingPeriod * * "Open" means that the closed date is NULL */ - public function getOpenParticipationsContainsPerson(Person $person): ?AccompanyingPeriodParticipation + public function getOpenParticipationContainsPerson(Person $person): ?AccompanyingPeriodParticipation { - $collection = $this->getParticipationsContainsPerson()->filter( + $collection = $this->getParticipationsContainsPerson($person)->filter( function(AccompanyingPeriodParticipation $participation) use ($person) { - if (NULL === $participation->getClosingDate()) { + if (NULL === $participation->getEndDate()) { return $participation; } }); @@ -380,7 +380,7 @@ class AccompanyingPeriod */ public function containsPerson(Person $person): bool { - return $this->participationsContainsPerson($person)->count() > 0; + return $this->getParticipationsContainsPerson($person)->count() > 0; } /** diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/AccompanyingPeriodNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/AccompanyingPeriodNormalizer.php index 55a59700d..7cf2330cb 100644 --- a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/AccompanyingPeriodNormalizer.php +++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/AccompanyingPeriodNormalizer.php @@ -38,7 +38,7 @@ class AccompanyingPeriodNormalizer implements NormalizerInterface, NormalizerAwa 'remark' => $period->getRemark(), 'participations' => $this->normalizer->normalize($period->getParticipations(), $format), 'closingMotive' => $this->normalizer->normalize($period->getClosingMotive(), $format), - 'user' => $period->getUser() ? $this->normalize($period->getUser(), $format) : null, + 'user' => $period->getUser() ? $this->normalizer->normalize($period->getUser(), $format) : null, 'step' => $period->getStep(), 'origin' => $this->normalizer->normalize($period->getOrigin(), $format), 'intensity' => $period->getIntensity(), diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php index 90a816ebc..d88d27ddc 100644 --- a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php +++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php @@ -55,7 +55,7 @@ class PersonNormalizer implements 'id' => $person->getId(), 'firstName' => $person->getFirstName(), 'lastName' => $person->getLastName(), - 'birthdate' => $person->getBirthdate(), + 'birthdate' => $this->normalizer->normalize($person->getBirthdate()), 'center' => $this->normalizer->normalize($person->getCenter()) ]; } diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php similarity index 71% rename from src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseControllerTest.php rename to src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php index c45cb93d9..61b5307a4 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseControllerTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php @@ -36,7 +36,7 @@ use Symfony\Component\HttpFoundation\Request; /** * Test api for AccompanyingCourseControllerTest */ -class AccompanyingCourseControllerTest extends WebTestCase +class AccompanyingCourseApiControllerTest extends WebTestCase { protected static EntityManagerInterface $em; @@ -65,7 +65,7 @@ class AccompanyingCourseControllerTest extends WebTestCase */ public function testAccompanyingCourseShow(int $personId, AccompanyingPeriod $period) { - $this->client->request(Request::METHOD_GET, sprintf('/fr/person/api/1.0/accompanying-course/%d/show.json', $period->getId())); + $c = $this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $period->getId())); $response = $this->client->getResponse(); $this->assertEquals(200, $response->getStatusCode(), "Test that the response of rest api has a status code ok (200)"); @@ -77,6 +77,14 @@ class AccompanyingCourseControllerTest extends WebTestCase $this->assertGreaterThan(0, $data->participations); } + public function testShow404() + { + $this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', 99999)); + $response = $this->client->getResponse(); + + $this->assertEquals(404, $response->getStatusCode(), "Test that the response of rest api has a status code 'not found' (404)"); + } + /** * * @dataProvider dataGenerateRandomAccompanyingCourse @@ -85,26 +93,55 @@ class AccompanyingCourseControllerTest extends WebTestCase { $this->client->request( Request::METHOD_POST, - sprintf('/fr/person/api/1.0/accompanying-course/%d/participation.json', $period->getId()), + sprintf('/api/1.0/person/accompanying-course/%d/participation.json', $period->getId()), [], // parameters [], // files [], // server parameters \json_encode([ 'id' => $personId ]) ); $response = $this->client->getResponse(); + $data = \json_decode($response->getContent(), true); $this->assertEquals(200, $response->getStatusCode(), "Test that the response of rest api has a status code ok (200)"); - $this->client->request(Request::METHOD_GET, sprintf('/fr/person/api/1.0/accompanying-course/%d/show.json', $period->getId())); + $this->assertArrayHasKey('id', $data); + $this->assertArrayHasKey('startDate', $data); + $this->assertNotNull($data['startDate']); + + // check by deownloading the accompanying cours + + $this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $period->getId())); $response = $this->client->getResponse(); $data = \json_decode($response->getContent()); + // check that the person id is contained $participationsPersonsIds = \array_map( function($participation) { return $participation->person->id; }, $data->participations); $this->assertContains($personId, $participationsPersonsIds); + // check removing the participation + $this->client->request( + Request::METHOD_DELETE, + sprintf('/api/1.0/person/accompanying-course/%d/participation.json', $period->getId()), + [], // parameters + [], // files + [], // server parameters + \json_encode([ 'id' => $personId ]) + ); + $response = $this->client->getResponse(); + $data = \json_decode($response->getContent(), true); + + $this->assertEquals(200, $response->getStatusCode(), "Test that the response of rest api has a status code ok (200)"); + $this->assertArrayHasKey('id', $data); + $this->assertArrayHasKey('startDate', $data); + $this->assertNotNull($data['startDate']); + $this->assertArrayHasKey('endDate', $data); + $this->assertNotNull($data['endDate']); + + + // set to variable for tear down $this->personId = $personId; $this->period = $period; } @@ -112,6 +149,7 @@ class AccompanyingCourseControllerTest extends WebTestCase protected function tearDown() { // remove participation created during test 'testAccompanyingCourseAddParticipation' + // and if the test could not remove it $testAddParticipationName = 'testAccompanyingCourseAddParticipation'; @@ -126,8 +164,10 @@ class AccompanyingCourseControllerTest extends WebTestCase ->findOneBy(['person' => $this->personId, 'accompanyingPeriod' => $this->period]) ; - $em->remove($participation); - $em->flush(); + if (NULL !== $participation) { + $em->remove($participation); + $em->flush(); + } } public function dataGenerateRandomAccompanyingCourse() @@ -139,9 +179,9 @@ class AccompanyingCourseControllerTest extends WebTestCase // * one for getting the person, which will in turn provide his accompanying period; // * one for getting the personId to populate to the data manager // - // Ensure to keep always $maxGenerated to the double of $maxResults - $maxGenerated = 1; - $maxResults = 15 * 8; + // Ensure to keep always $maxGenerated to the double of $maxResults. x8 is a good compromize :) + $maxGenerated = 3; + $maxResults = $maxGenerated * 8; static::bootKernel(); $em = static::$container->get(EntityManagerInterface::class); diff --git a/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php b/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php index a31055a0d..a1d13892f 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php @@ -27,7 +27,6 @@ use Chill\PersonBundle\Entity\Person; class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase { - public function testClosingIsAfterOpeningConsistency() { $datetime1 = new \DateTime('now'); @@ -77,22 +76,24 @@ class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase $this->assertFalse($period->isOpen()); } - public function testCanBeReOpened() + public function testPersonPeriod() { - $person = new Person(\DateTime::createFromFormat('Y-m-d', '2010-01-01')); - $person->close($person->getAccompanyingPeriods()[0] - ->setClosingDate(\DateTime::createFromFormat('Y-m-d', '2010-12-31'))); - - $firstAccompanygingPeriod = $person->getAccompanyingPeriodsOrdered()[0]; - - $this->assertTrue($firstAccompanygingPeriod->canBeReOpened()); - - $lastAccompanyingPeriod = (new AccompanyingPeriod(\DateTime::createFromFormat('Y-m-d', '2011-01-01'))) - ->setClosingDate(\DateTime::createFromFormat('Y-m-d', '2011-12-31')) - ; - $person->addAccompanyingPeriod($lastAccompanyingPeriod); - - $this->assertFalse($firstAccompanygingPeriod->canBeReOpened()); - } + $person = new Person(); + $period = new AccompanyingPeriod(new \DateTime()); + $period->addPerson($person); + + $this->assertEquals(1, $period->getParticipations()->count()); + $this->assertTrue($period->containsPerson($person)); + + $participation = $period->getOpenParticipationContainsPerson($person); + $participations = $period->getParticipationsContainsPerson($person); + $this->assertNotNull($participation); + $this->assertEquals(1, $participations->count()); + + $period->removePerson($person); + + $participation = $period->getOpenParticipationContainsPerson($person); + $this->assertNull($participation); + } } diff --git a/src/Bundle/ChillPersonBundle/config/services/controller.yaml b/src/Bundle/ChillPersonBundle/config/services/controller.yaml index 893b1cfd3..c2329e73a 100644 --- a/src/Bundle/ChillPersonBundle/config/services/controller.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/controller.yaml @@ -45,3 +45,9 @@ services: $dispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface' $validator: '@Symfony\Component\Validator\Validator\ValidatorInterface' tags: ['controller.service_arguments'] + + Chill\PersonBundle\Controller\AccompanyingCourseApiController: + arguments: + $eventDispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface' + $validator: '@Symfony\Component\Validator\Validator\ValidatorInterface' + tags: ['controller.service_arguments'] From 2b8bbe019da261b10aaae6c5d7215347cf5cf432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 May 2021 12:08:45 +0200 Subject: [PATCH 29/42] first impl for index action --- .../CRUDControllerCompilerPass.php | 1 + .../Controller/AbstractCRUDController.php | 139 +++++++++++++++--- .../CRUD/Controller/ApiController.php | 112 +++++++++++++- .../CRUD/Routing/CRUDRoutesLoader.php | 93 ++++++++---- .../DependencyInjection/Configuration.php | 16 +- .../Serializer/Model/Collection.php | 28 ++++ .../Normalizer/CollectionNormalizer.php | 39 +++++ .../config/services/pagination.yaml | 1 + .../config/services/serializer.yaml | 4 + .../Entity/AccompanyingPeriod/Origin.php | 2 +- 10 files changed, 375 insertions(+), 60 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Serializer/Model/Collection.php create mode 100644 src/Bundle/ChillMainBundle/Serializer/Normalizer/CollectionNormalizer.php diff --git a/src/Bundle/ChillMainBundle/CRUD/CompilerPass/CRUDControllerCompilerPass.php b/src/Bundle/ChillMainBundle/CRUD/CompilerPass/CRUDControllerCompilerPass.php index 43424d4f9..d8a46837c 100644 --- a/src/Bundle/ChillMainBundle/CRUD/CompilerPass/CRUDControllerCompilerPass.php +++ b/src/Bundle/ChillMainBundle/CRUD/CompilerPass/CRUDControllerCompilerPass.php @@ -64,6 +64,7 @@ class CRUDControllerCompilerPass implements CompilerPassInterface $controller->addTag('controller.service_arguments'); if (FALSE === $alreadyDefined) { $controller->setAutoconfigured(true); + $controller->setPublic(true); } $param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name']; diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php index 31141a857..5cc055f26 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php @@ -7,6 +7,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Chill\MainBundle\Pagination\PaginatorFactory; +use Chill\MainBundle\Pagination\PaginatorInterface; class AbstractCRUDController extends AbstractController { @@ -32,6 +33,122 @@ class AbstractCRUDController extends AbstractController ->find($id); } + /** + * Count the number of entities + * + * By default, count all entities. You can customize the query by + * using the method `customizeQuery`. + * + * @param string $action + * @param Request $request + * @return int + */ + protected function countEntities(string $action, Request $request, $_format): int + { + return $this->buildQueryEntities($action, $request) + ->select('COUNT(e)') + ->getQuery() + ->getSingleScalarResult() + ; + } + + /** + * Query the entity. + * + * By default, get all entities. You can customize the query by using the + * method `customizeQuery`. + * + * The method `orderEntity` is called internally to order entities. + * + * It returns, by default, a query builder. + * + */ + protected function queryEntities(string $action, Request $request, string $_format, PaginatorInterface $paginator) + { + $query = $this->buildQueryEntities($action, $request) + ->setFirstResult($paginator->getCurrentPage()->getFirstItemNumber()) + ->setMaxResults($paginator->getItemsPerPage()); + + // allow to order queries and return the new query + return $this->orderQuery($action, $query, $request, $paginator, $_format); + } + + /** + * Add ordering fields in the query build by self::queryEntities + * + */ + protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator, $_format) + { + return $query; + } + + /** + * Build the base query for listing all entities. + * + * This method is used internally by `countEntities` `queryEntities` + * + * This base query does not contains any `WHERE` or `SELECT` clauses. You + * can add some by using the method `customizeQuery`. + * + * The alias for the entity is "e". + * + * @param string $action + * @param Request $request + * @return QueryBuilder + */ + protected function buildQueryEntities(string $action, Request $request) + { + $qb = $this->getDoctrine()->getManager() + ->createQueryBuilder() + ->select('e') + ->from($this->getEntityClass(), 'e') + ; + + $this->customizeQuery($action, $request, $qb); + + return $qb; + } + + protected function customizeQuery(string $action, Request $request, $query): void {} + + /** + * Get the result of the query + */ + protected function getQueryResult(string $action, Request $request, string $_format, int $totalItems, PaginatorInterface $paginator, $query) + { + return $query->getQuery()->getResult(); + } + + protected function onPreIndex(string $action, Request $request, string $_format): ?Response + { + return null; + } + + /** + * method used by indexAction + */ + protected function onPreIndexBuildQuery(string $action, Request $request, string $_format, int $totalItems, PaginatorInterface $paginator): ?Response + { + return null; + } + + /** + * method used by indexAction + */ + protected function onPostIndexBuildQuery(string $action, Request $request, string $_format, int $totalItems, PaginatorInterface $paginator, $query): ?Response + { + return null; + } + + /** + * method used by indexAction + */ + protected function onPostIndexFetchQuery(string $action, Request $request, string $_format, int $totalItems, PaginatorInterface $paginator, $entities): ?Response + { + return null; + } + + /** * Get the complete FQDN of the class * @@ -53,7 +170,7 @@ class AbstractCRUDController extends AbstractController /** * Called on post check ACL */ - protected function onPostCheckACL(string $action, Request $request, $entity, $_format): ?Response + protected function onPostCheckACL(string $action, Request $request, string $_format, $entity): ?Response { return null; } @@ -69,7 +186,7 @@ class AbstractCRUDController extends AbstractController * * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedHttpException */ - protected function checkACL(string $action, Request $request, $entity, $_format) + protected function checkACL(string $action, Request $request, string $_format, $entity = null) { $this->denyAccessUnlessGranted($this->getRoleFor($action, $request, $entity, $_format), $entity); } @@ -103,22 +220,6 @@ class AbstractCRUDController extends AbstractController */ protected function getPaginatorFactory(): PaginatorFactory { - return $this->container->get(PaginatorFactory::class); - } - - /** - * Defined the services necessary for this controller - * - * @return array - */ - public static function getSubscribedServices(): array - { - return \array_merge( - parent::getSubscribedServices(), - [ - PaginatorFactory::class => PaginatorFactory::class, - - ] - ); + return $this->container->get('chill_main.paginator_factory'); } } diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php index 7256720bb..eb1de49d3 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php @@ -6,6 +6,8 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Serializer\SerializerInterface; +use Chill\MainBundle\Serializer\Model\Collection; +use Chill\MainBundle\Pagination\PaginatorInterface; class ApiController extends AbstractCRUDController { @@ -41,7 +43,7 @@ class ApiController extends AbstractCRUDController . "is not found", $this->getCrudName(), $id)); } - $response = $this->checkACL($action, $request, $entity, $_format); + $response = $this->checkACL($action, $request, $_format, $entity); if ($response instanceof Response) { return $response; } @@ -86,6 +88,110 @@ class ApiController extends AbstractCRUDController } } + /** + * Base action for indexing entities + */ + public function indexApi(Request $request, string $_format) + { + switch ($request->getMethod()) { + case Request::METHOD_GET: + case REQUEST::METHOD_HEAD: + return $this->indexApiAction('_index', $request, $_format); + default: + throw $this->createNotFoundException("This method is not supported"); + } + } + + /** + * Build an index page. + * + * Some steps may be overriden during this process of rendering. + * + * This method: + * + * 1. Launch `onPreIndex` + * x. check acl. If it does return a response instance, return it + * x. launch `onPostCheckACL`. If it does return a response instance, return it + * 1. count the items, using `countEntities` + * 2. build a paginator element from the the number of entities ; + * 3. Launch `onPreIndexQuery`. If it does return a response instance, return it + * 3. build a query, using `queryEntities` + * x. fetch the results, using `getQueryResult` + * x. Launch `onPostIndexFetchQuery`. If it does return a response instance, return it + * 4. create default parameters: + * + * The default parameters are: + * + * * entities: the list en entities ; + * * crud_name: the name of the crud ; + * * paginator: a paginator element ; + * 5. Launch rendering, the parameter is fetch using `getTemplateFor` + * The parameters may be personnalized using `generateTemplateParameter`. + * + * @param string $action + * @param Request $request + * @return type + */ + protected function indexApiAction($action, Request $request, $_format) + { + $this->onPreIndex($action, $request, $_format); + + $response = $this->checkACL($action, $request, $_format); + if ($response instanceof Response) { + return $response; + } + + if (!isset($entity)) { + $entity = ''; + } + + $response = $this->onPostCheckACL($action, $request, $_format, $entity); + if ($response instanceof Response) { + return $response; + } + + $totalItems = $this->countEntities($action, $request, $_format); + $paginator = $this->getPaginatorFactory()->create($totalItems); + + $response = $this->onPreIndexBuildQuery($action, $request, $_format, $totalItems, + $paginator); + + if ($response instanceof Response) { + return $response; + } + + $query = $this->queryEntities($action, $request, $_format, $paginator); + + $response = $this->onPostIndexBuildQuery($action, $request, $_format, $totalItems, + $paginator, $query); + + if ($response instanceof Response) { + return $response; + } + + $entities = $this->getQueryResult($action, $request, $_format, $totalItems, $paginator, $query); + + $response = $this->onPostIndexFetchQuery($action, $request, $_format, $totalItems, + $paginator, $entities); + + if ($response instanceof Response) { + return $response; + } + + return $this->serializeCollectionItems($action, $request, $_format, $paginator, $entities); + } + + /** + * Serialize collections + * + */ + protected function serializeCollectionItems(string $action, Request $request, string $_format, PaginatorInterface $paginator, $entities): Response + { + $model = new Collection($entities, $paginator); + + return $this->json($model); + } + protected function getContextForSerialization(string $action, Request $request, $entity, $_format): array { @@ -103,8 +209,8 @@ class ApiController extends AbstractCRUDController return $actionConfig['roles'][$request->getMethod()]; } - if ($this->crudConfig['role']) { - return $this->crudConfig['role']; + if ($this->crudConfig['base_role']) { + return $this->crudConfig['base_role']; } throw new \RuntimeException(sprintf("the config does not have any role for the ". diff --git a/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php b/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php index b09bbc55b..32068e518 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php +++ b/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php @@ -90,8 +90,7 @@ class CRUDRoutesLoader extends Loader $collection->addCollection($this->loadCrudConfig($crudConfig)); } foreach ($this->apiConfig as $crudConfig) { - $collection->addCollection($this->loadApiSingle($crudConfig)); - //$collection->addCollection($this->loadApiMulti($crudConfig)); + $collection->addCollection($this->loadApi($crudConfig)); } return $collection; @@ -140,7 +139,7 @@ class CRUDRoutesLoader extends Loader * @param $crudConfig * @return RouteCollection */ - protected function loadApiSingle(array $crudConfig): RouteCollection + protected function loadApi(array $crudConfig): RouteCollection { $collection = new RouteCollection(); $controller ='csapi_'.$crudConfig['name'].'_controller'; @@ -149,6 +148,65 @@ class CRUDRoutesLoader extends Loader // filter only on single actions $singleCollection = $action['single-collection'] ?? $name === '_entity' ? 'single' : NULL; if ('collection' === $singleCollection) { +// continue; + } + + // compute default action + switch ($name) { + case '_entity': + $controllerAction = 'entityApi'; + break; + case '_index': + $controllerAction = 'indexApi'; + break; + default: + $controllerAction = $name.'Api'; + break; + } + + $defaults = [ + '_controller' => $controller.':'.($action['controller_action'] ?? $controllerAction) + ]; + + // path are rewritten + // if name === 'default', we rewrite it to nothing :-) + $localName = \in_array($name, [ '_entity', '_index' ]) ? '' : '/'.$name; + if ('collection' === $action['single-collection'] || '_index' === $name) { + $localPath = $action['path'] ?? $localName.'.{_format}'; + } else { + $localPath = $action['path'] ?? '/{id}'.$localName.'.{_format}'; + } + $path = $crudConfig['base_path'].$localPath; + + $requirements = $action['requirements'] ?? [ '{id}' => '\d+' ]; + + $methods = \array_keys(\array_filter($action['methods'], function($value, $key) { return $value; }, + ARRAY_FILTER_USE_BOTH)); + + $route = new Route($path, $defaults, $requirements); + $route->setMethods($methods); + + $collection->add('chill_api_single_'.$crudConfig['name'].'_'.$name, $route); + } + + return $collection; + } + + /** + * Load routes for api multi + * + * @param $crudConfig + * @return RouteCollection + */ + protected function loadApiMultiConfig(array $crudConfig): RouteCollection + { + $collection = new RouteCollection(); + $controller ='csapi_'.$crudConfig['name'].'_controller'; + + foreach ($crudConfig['actions'] as $name => $action) { + // filter only on single actions + $singleCollection = $action['single-collection'] ?? $name === '_index' ? 'collection' : NULL; + if ('single' === $singleCollection) { continue; } @@ -175,33 +233,4 @@ class CRUDRoutesLoader extends Loader return $collection; } - - /** - * Load routes for api multi - * - * @param $crudConfig - * @return RouteCollection - */ - protected function loadApiMultiConfig(array $crudConfig): RouteCollection - { - $collection = new RouteCollection(); - foreach ($crudConfig['actions_multi'] as $name => $action) { - // we compute the data from configuration to a local form - $defaults = [ - '_controller' => 'cscrud_'.$crudConfig['name'].'_controller'.':'.($action['controller_action'] ?? $name.'Api') - ]; - // path are rewritten - // if name === 'index', we rewrite it to nothing :-) - $localName = 'index' === $name ? '' : $name; - $localPath = $action['path'] ?? '.{_format}'; - $path = $crudConfig['base_path'].$localPath.$name; - $requirements = $action['requirements'] ?? [ '{id}' => '\d+' ]; - $methods = $name === 'default' ? self::ALL_MULTI_METHODS: []; - $route = new Route($path, $defaults, $requirements); - - $collection->add('chill_api_multi'.$crudConfig['name'].'_'.$name, $route); - } - - return $collection; - } } diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php index a7b15a069..c7e4c00ef 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php @@ -189,14 +189,16 @@ class Configuration implements ConfigurationInterface ->children() ->scalarNode('controller_action') ->defaultNull() - ->info('the method name to call in the route. Will be set to the concatenation of action name + \'Api\' if left empty.') + ->info('the method name to call in the controller. Will be set to the concatenation '. + 'of action name + \'Api\' if left empty.') ->example("showApi") ->end() ->scalarNode('path') ->defaultNull() - ->info('the path that will be **appended** after the base path. Do not forget to add ' - . 'arguments for the method. By default, will set to the action name, including an `{id}` ' - . 'parameter. A suffix of action name will be appended, except if the action name is "entity".') + ->info('the path that will be **appended** after the base path. Do not forget to add ' . + 'arguments for the method. By default, will set to the action name, including an `{id}` '. + 'parameter. A suffix of action name will be appended, except if the action name '. + 'is "_entity".') ->example('/{id}/my-action') ->end() ->arrayNode('requirements') @@ -205,7 +207,10 @@ class Configuration implements ConfigurationInterface ->end() ->enumNode('single-collection') ->values(['single', 'collection']) - ->info('indicates if the returned object is a single element or a collection') + ->defaultValue('single') + ->info('indicates if the returned object is a single element or a collection. '. + 'If the action name is `_index`, this value will always be considered as '. + '`collection`') ->end() ->arrayNode('methods') ->addDefaultsIfNotSet() @@ -219,6 +224,7 @@ class Configuration implements ConfigurationInterface ->end() ->end() ->arrayNode('roles') + ->addDefaultsIfNotSet() ->info("The role require for each http method") ->children() ->scalarNode(Request::METHOD_GET)->defaultNull()->end() diff --git a/src/Bundle/ChillMainBundle/Serializer/Model/Collection.php b/src/Bundle/ChillMainBundle/Serializer/Model/Collection.php new file mode 100644 index 000000000..9983d3595 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Serializer/Model/Collection.php @@ -0,0 +1,28 @@ +items = $items; + $this->paginator = $paginator; + } + + public function getPaginator(): PaginatorInterface + { + return $this->paginator; + } + + public function getItems() + { + return $this->items; + } +} diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/CollectionNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/CollectionNormalizer.php new file mode 100644 index 000000000..b21bf6326 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/CollectionNormalizer.php @@ -0,0 +1,39 @@ +getPaginator(); + + $data['count'] = $paginator->getTotalItems(); + $data['first'] = $paginator->getCurrentPageFirstItemNumber(); + $data['items_per_page'] = $paginator->getItemsPerPage(); + $data['next'] = $paginator->hasNextPage() ? + $paginator->getNextPage()->generateUrl() : null; + $data['previous'] = $paginator->hasPreviousPage() ? + $paginator->getPreviousPage()->generateUrl() : null; + + // normalize results + $data['results'] = $this->normalizer->normalize($collection->getItems(), + $format, $context); + + return $data; + } +} diff --git a/src/Bundle/ChillMainBundle/config/services/pagination.yaml b/src/Bundle/ChillMainBundle/config/services/pagination.yaml index c7c8a89a9..f6282a39f 100644 --- a/src/Bundle/ChillMainBundle/config/services/pagination.yaml +++ b/src/Bundle/ChillMainBundle/config/services/pagination.yaml @@ -6,6 +6,7 @@ services: - "@request_stack" - "@router" - "%chill_main.pagination.item_per_page%" + Chill\MainBundle\Pagination\PaginatorFactory: '@chill_main.paginator_factory' chill_main.paginator.twig_extensions: diff --git a/src/Bundle/ChillMainBundle/config/services/serializer.yaml b/src/Bundle/ChillMainBundle/config/services/serializer.yaml index 763576a5c..fb5f57b7e 100644 --- a/src/Bundle/ChillMainBundle/config/services/serializer.yaml +++ b/src/Bundle/ChillMainBundle/config/services/serializer.yaml @@ -11,3 +11,7 @@ services: Chill\MainBundle\Serializer\Normalizer\UserNormalizer: tags: - { name: 'serializer.normalizer', priority: 64 } + + Chill\MainBundle\Serializer\Normalizer\CollectionNormalizer: + tags: + - { name: 'serializer.normalizer', priority: 64 } diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Origin.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Origin.php index 42c75efca..9227a8d58 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Origin.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Origin.php @@ -53,7 +53,7 @@ class Origin return $this->id; } - public function getLabel(): ?string + public function getLabel() { return $this->label; } From 90fe484d8122748eeae2c38c9ae111826c426563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 May 2021 12:18:42 +0200 Subject: [PATCH 30/42] fix api for rendering a single item --- .../CRUD/Controller/ApiController.php | 30 ++++++++----------- .../Entity/AccompanyingPeriod/Origin.php | 2 +- .../config/services/repository.yaml | 5 ++++ 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php index eb1de49d3..7643db0e9 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php @@ -48,18 +48,18 @@ class ApiController extends AbstractCRUDController return $response; } - $response = $this->onPostCheckACL($action, $request, $entity, $_format); + $response = $this->onPostCheckACL($action, $request, $_format, $entity); if ($response instanceof Response) { return $response; } - $response = $this->onBeforeSerialize($action, $request, $entity, $_format); + $response = $this->onBeforeSerialize($action, $request, $_format, $entity); if ($response instanceof Response) { return $response; } if ($_format === 'json') { - $context = $this->getContextForSerialization($action, $request, $entity, $_format); + $context = $this->getContextForSerialization($action, $request, $_format, $entity); return $this->json($entity, Response::HTTP_OK, [], $context); } else { @@ -67,7 +67,7 @@ class ApiController extends AbstractCRUDController } } - public function onBeforeSerialize(string $action, Request $request, $entity, string $_format): ?Response + public function onBeforeSerialize(string $action, Request $request, $_format, $entity): ?Response { return null; } @@ -118,15 +118,7 @@ class ApiController extends AbstractCRUDController * 3. build a query, using `queryEntities` * x. fetch the results, using `getQueryResult` * x. Launch `onPostIndexFetchQuery`. If it does return a response instance, return it - * 4. create default parameters: - * - * The default parameters are: - * - * * entities: the list en entities ; - * * crud_name: the name of the crud ; - * * paginator: a paginator element ; - * 5. Launch rendering, the parameter is fetch using `getTemplateFor` - * The parameters may be personnalized using `generateTemplateParameter`. + * 4. Serialize the entities in a Collection, using `SerializeCollection` * * @param string $action * @param Request $request @@ -178,22 +170,24 @@ class ApiController extends AbstractCRUDController return $response; } - return $this->serializeCollectionItems($action, $request, $_format, $paginator, $entities); + return $this->serializeCollection($action, $request, $_format, $paginator, $entities); } /** * Serialize collections * */ - protected function serializeCollectionItems(string $action, Request $request, string $_format, PaginatorInterface $paginator, $entities): Response + protected function serializeCollection(string $action, Request $request, string $_format, PaginatorInterface $paginator, $entities): Response { - $model = new Collection($entities, $paginator); + $model = new Collection($entities, $paginator); - return $this->json($model); + $context = $this->getContextForSerialization($action, $request, $_format, $entities); + + return $this->json($model, Response::HTTP_OK, [], $context); } - protected function getContextForSerialization(string $action, Request $request, $entity, $_format): array + protected function getContextForSerialization(string $action, Request $request, string $_format, $entity): array { return []; } diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Origin.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Origin.php index 9227a8d58..55857de4c 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Origin.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Origin.php @@ -22,7 +22,7 @@ namespace Chill\PersonBundle\Entity\AccompanyingPeriod; -use Chill\PersonBundle\Entity\AccompanyingPeriod\OriginRepository; +use Chill\PersonBundle\Repository\AccompanyingPeriod\OriginRepository; use Doctrine\ORM\Mapping as ORM; /** diff --git a/src/Bundle/ChillPersonBundle/config/services/repository.yaml b/src/Bundle/ChillPersonBundle/config/services/repository.yaml index e899ba9e1..b99402bcf 100644 --- a/src/Bundle/ChillPersonBundle/config/services/repository.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/repository.yaml @@ -23,3 +23,8 @@ services: arguments: - '@Doctrine\Persistence\ManagerRegistry' tags: [ doctrine.repository_service ] + + Chill\PersonBundle\Repository\AccompanyingPeriod\OriginRepository: + arguments: + - '@Doctrine\Persistence\ManagerRegistry' + tags: [ doctrine.repository_service ] From 4770758aeeb5cc204129a068dc81a3785143415b Mon Sep 17 00:00:00 2001 From: nobohan Date: Thu, 6 May 2021 12:36:58 +0200 Subject: [PATCH 31/42] add migrations for addresse on household + add linkedtothirdparty on address --- .../migrations/Version20210505153727.php | 33 +++++++++++++++++++ .../migrations/Version20210505154316.php | 33 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20210505153727.php create mode 100644 src/Bundle/ChillPersonBundle/migrations/Version20210505154316.php diff --git a/src/Bundle/ChillMainBundle/migrations/Version20210505153727.php b/src/Bundle/ChillMainBundle/migrations/Version20210505153727.php new file mode 100644 index 000000000..c1fff5b9e --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20210505153727.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE chill_main_address ADD linkedToThirdParty_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_address ADD CONSTRAINT FK_165051F6114B8DD9 FOREIGN KEY (linkedToThirdParty_id) REFERENCES chill_3party.third_party (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_165051F6114B8DD9 ON chill_main_address (linkedToThirdParty_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_main_address DROP CONSTRAINT FK_165051F6114B8DD9'); + $this->addSql('DROP INDEX IDX_165051F6114B8DD9'); + $this->addSql('ALTER TABLE chill_main_address DROP linkedToThirdParty_id'); + } +} diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20210505154316.php b/src/Bundle/ChillPersonBundle/migrations/Version20210505154316.php new file mode 100644 index 000000000..bb3a56337 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20210505154316.php @@ -0,0 +1,33 @@ +addSql('CREATE TABLE chill_person_household_to_addresses (household_id INT NOT NULL, address_id INT NOT NULL, PRIMARY KEY(household_id, address_id))'); + $this->addSql('CREATE INDEX IDX_7109483E79FF843 ON chill_person_household_to_addresses (household_id)'); + $this->addSql('CREATE INDEX IDX_7109483F5B7AF75 ON chill_person_household_to_addresses (address_id)'); + $this->addSql('ALTER TABLE chill_person_household_to_addresses ADD CONSTRAINT FK_7109483E79FF843 FOREIGN KEY (household_id) REFERENCES Household (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_person_household_to_addresses ADD CONSTRAINT FK_7109483F5B7AF75 FOREIGN KEY (address_id) REFERENCES chill_main_address (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP TABLE chill_person_household_to_addresses'); + } +} From f56dc650213904cab6c204d7b4607143d93e313e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 May 2021 12:55:12 +0200 Subject: [PATCH 32/42] fix argument order --- .../Controller/AccompanyingCourseApiController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php index 2716d1c1a..bbf2f399a 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php @@ -38,7 +38,7 @@ class AccompanyingCourseApiController extends ApiController // TODO add acl // - $this->onPostCheckACL('participation', $request, $accompanyingPeriod, $_format); + $this->onPostCheckACL('participation', $request, $_format, $accompanyingPeriod); switch ($request->getMethod()) { case Request::METHOD_POST: @@ -64,7 +64,7 @@ class AccompanyingCourseApiController extends ApiController return $this->json($participation); } - protected function onPostCheckACL(string $action, Request $request, $entity, $_format): ?Response + protected function onPostCheckACL(string $action, Request $request, string $_format, $entity): ?Response { $this->eventDispatcher->dispatch( AccompanyingPeriodPrivacyEvent::ACCOMPANYING_PERIOD_PRIVACY_EVENT, From e7985ea52f3e4993b109b9fb7c62d1b26afde78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 May 2021 12:55:40 +0200 Subject: [PATCH 33/42] customize controller for origin --- .../Controller/OpeningApiController.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/Bundle/ChillPersonBundle/Controller/OpeningApiController.php diff --git a/src/Bundle/ChillPersonBundle/Controller/OpeningApiController.php b/src/Bundle/ChillPersonBundle/Controller/OpeningApiController.php new file mode 100644 index 000000000..3b44162c4 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Controller/OpeningApiController.php @@ -0,0 +1,17 @@ +where($qb->expr()->gt('e.noActiveAfter', ':now')) + ->orWhere($qb->expr()->isNull('e.noActiveAfter')); + $qb->setParameter('now', new \DateTime('now')); + } +} From f8805980522f5e46230c70a46a00d8bfcdef401a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 May 2021 12:56:19 +0200 Subject: [PATCH 34/42] documentation for api --- docs/source/development/api.rst | 353 ++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 docs/source/development/api.rst diff --git a/docs/source/development/api.rst b/docs/source/development/api.rst new file mode 100644 index 000000000..f895be089 --- /dev/null +++ b/docs/source/development/api.rst @@ -0,0 +1,353 @@ +.. Copyright (C) 2014 Champs Libres Cooperative SCRLFS + Permission is granted to copy, distribute and/or modify this document + under the terms of the GNU Free Documentation License, Version 1.3 + or any later version published by the Free Software Foundation; + with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. + A copy of the license is included in the section entitled "GNU + Free Documentation License". + +.. _api + +API +### + +Chill provides a basic framework to build REST api. + +Configure a route +================= + +Follow those steps to build a REST api: + +1. Create your model; +2. Configure the API; + +You can also: + +* hook into the controller to customize some steps; +* add more route and steps + +.. read-also:: + + * `How to use annotation to configure serialization `_ + * `How to create your custom normalizer `_ + +Auto-loading the routes +*********************** + +Ensure that those lines are present in your file `app/config/routing.yml`: + + +.. code-block:: yaml + + chill_cruds: + resource: 'chill_main_crud_route_loader:load' + type: service + + +Create your model +***************** + +Create your model on the usual way: + +.. code-block:: php + + namespace Chill\PersonBundle\Entity\AccompanyingPeriod; + + use Chill\PersonBundle\Entity\AccompanyingPeriod\OriginRepository; + use Doctrine\ORM\Mapping as ORM; + + /** + * @ORM\Entity(repositoryClass=OriginRepository::class) + * @ORM\Table(name="chill_person_accompanying_period_origin") + */ + class Origin + { + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + private $id; + + /** + * @ORM\Column(type="json") + */ + private $label; + + /** + * @ORM\Column(type="date_immutable", nullable=true) + */ + private $noActiveAfter; + + // .. getters and setters + + } + + +Configure api +************* + +Configure the api using Yaml (see the full configuration below): + +.. code-block:: yaml + + # config/packages/chill_main.yaml + chill_main: + apis: + accompanying_period_origin: + base_path: '/api/1.0/person/accompanying-period/origin' + class: 'Chill\PersonBundle\Entity\AccompanyingPeriod\Origin' + name: accompanying_period_origin + base_role: 'ROLE_USER' + actions: + _index: + methods: + GET: true + HEAD: true + _entity: + methods: + GET: true + HEAD: true + +The :code:`_index` and :code:`_entity` action +============================================= + +The :code:`_index` and :code:`_entity` action are default actions: + +* they will call a specific method in the default controller; +* they will generate defined routes: + +Index: + Name: :code:`chill_api_single_accompanying_period_origin__index` + + Path: :code:`/api/1.0/person/accompanying-period/origin.{_format}` + +Entity: + Name: :code:`chill_api_single_accompanying_period_origin__index` + + Path: :code:`/api/1.0/person/accompanying-period/origin.{_format}` + +Role +==== + +By default, the key `base_role` is used to check ACL. Take care of creating the :code:`Voter` required to take that into account. + +For index action, the role will be called with :code:`NULL` as :code:`$subject`. The retrieved entity will be the subject for single queries. + +You can also define a role for each method: + +.. code-block:: yaml + + # config/packages/chill_main.yaml + chill_main: + apis: + accompanying_period_origin: + base_path: '/api/1.0/person/bla/bla' + class: 'Chill\PersonBundle\Entity\Blah' + name: bla + actions: + _entity: + methods: + GET: true + HEAD: true + roles: + GET: MY_ROLE_SEE + HEAD: MY ROLE_SEE + +Customize the controller +======================== + +You can customize the controller by hooking into the default actions. Take care of extending :code:`Chill\MainBundle\CRUD\Controller\ApiController`. + + +.. code-block:: php + + + namespace Chill\PersonBundle\Controller; + + use Chill\MainBundle\CRUD\Controller\ApiController; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + class OpeningApiController extends ApiController + { + protected function customizeQuery(string $action, Request $request, $qb): void + { + $qb->where($qb->expr()->gt('e.noActiveAfter', ':now')) + ->orWhere($qb->expr()->isNull('e.noActiveAfter')); + $qb->setParameter('now', new \DateTime('now')); + } + } + +And set your controller in configuration: + +.. code-block:: yaml + + chill_main: + apis: + accompanying_period_origin: + base_path: '/api/1.0/person/accompanying-period/origin' + class: 'Chill\PersonBundle\Entity\AccompanyingPeriod\Origin' + name: accompanying_period_origin + # add a controller + controller: 'Chill\PersonBundle\Controller\OpeningApiController' + base_role: 'ROLE_USER' + actions: + _index: + methods: + GET: true + HEAD: true + _entity: + methods: + GET: true + HEAD: true + +Create your own actions +======================= + +You can add your own actions: + +.. code-block:: yaml + + chill_main: + apis: + - + class: Chill\PersonBundle\Entity\AccompanyingPeriod + name: accompanying_course + base_path: /api/1.0/person/accompanying-course + controller: Chill\PersonBundle\Controller\AccompanyingCourseApiController + actions: + # add a custom participation: + participation: + methods: + POST: true + DELETE: true + GET: false + HEAD: false + PUT: false + roles: + POST: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE + DELETE: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE + GET: null + HEAD: null + PUT: null + single-collection: single + +The key :code:`single-collection` with value :code:`single` will add a :code:`/{id}/ + "action name"` (in this example, :code:`/{id}/participation`) into the path, after the base path. If the value is :code:`collection`, no id will be set, but the action name will be append to the path. + +Then, create the corresponding action into your controller: + +.. code-block:: php + + namespace Chill\PersonBundle\Controller; + + use Chill\MainBundle\CRUD\Controller\ApiController; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Chill\PersonBundle\Entity\AccompanyingPeriod; + use Symfony\Component\HttpFoundation\Exception\BadRequestException; + use Symfony\Component\EventDispatcher\EventDispatcherInterface; + use Symfony\Component\Validator\Validator\ValidatorInterface; + use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent; + use Chill\PersonBundle\Entity\Person; + + class AccompanyingCourseApiController extends ApiController + { + protected EventDispatcherInterface $eventDispatcher; + + protected ValidatorInterface $validator; + + public function __construct(EventDispatcherInterface $eventDispatcher, $validator) + { + $this->eventDispatcher = $eventDispatcher; + $this->validator = $validator; + } + + public function participationApi($id, Request $request, $_format) + { + /** @var AccompanyingPeriod $accompanyingPeriod */ + $accompanyingPeriod = $this->getEntity('participation', $id, $request); + $person = $this->getSerializer() + ->deserialize($request->getContent(), Person::class, $_format, []); + + if (NULL === $person) { + throw new BadRequestException('person id not found'); + } + + $this->onPostCheckACL('participation', $request, $accompanyingPeriod, $_format); + + switch ($request->getMethod()) { + case Request::METHOD_POST: + $participation = $accompanyingPeriod->addPerson($person); + break; + case Request::METHOD_DELETE: + $participation = $accompanyingPeriod->removePerson($person); + break; + default: + throw new BadRequestException("This method is not supported"); + } + + $errors = $this->validator->validate($accompanyingPeriod); + + if ($errors->count() > 0) { + // only format accepted + return $this->json($errors); + } + + $this->getDoctrine()->getManager()->flush(); + + return $this->json($participation); + } + } + +.. api_full_config: + +Full configuration example +========================== + +.. code-block:: yaml + + apis: + - + class: Chill\PersonBundle\Entity\AccompanyingPeriod + name: accompanying_course + base_path: /api/1.0/person/accompanying-course + controller: Chill\PersonBundle\Controller\AccompanyingCourseApiController + actions: + _entity: + roles: + GET: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE + HEAD: null + POST: null + DELETE: null + PUT: null + controller_action: null + path: null + single-collection: single + methods: + GET: true + HEAD: true + POST: false + DELETE: false + PUT: false + participation: + methods: + POST: true + DELETE: true + GET: false + HEAD: false + PUT: false + roles: + POST: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE + DELETE: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE + GET: null + HEAD: null + PUT: null + controller_action: null + # the requirements for the route. Will be set to `[ 'id' => '\d+' ]` if left empty. + requirements: [] + path: null + single-collection: single + base_role: null + + From ee77c8540a4ac12b3cacd6d7299511f5343af3ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 May 2021 13:00:59 +0200 Subject: [PATCH 35/42] fix typos --- docs/source/development/api.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/development/api.rst b/docs/source/development/api.rst index f895be089..012178cbe 100644 --- a/docs/source/development/api.rst +++ b/docs/source/development/api.rst @@ -123,9 +123,9 @@ Index: Path: :code:`/api/1.0/person/accompanying-period/origin.{_format}` Entity: - Name: :code:`chill_api_single_accompanying_period_origin__index` + Name: :code:`chill_api_single_accompanying_period_origin__entity` - Path: :code:`/api/1.0/person/accompanying-period/origin.{_format}` + Path: :code:`/api/1.0/person/accompanying-period/origin/{id}.{_format}` Role ==== @@ -134,7 +134,7 @@ By default, the key `base_role` is used to check ACL. Take care of creating the For index action, the role will be called with :code:`NULL` as :code:`$subject`. The retrieved entity will be the subject for single queries. -You can also define a role for each method: +You can also define a role for each method. In this case, this role is used for the given method, and, if any, the base role is taken into account. .. code-block:: yaml From c693002ddb1b37961761253f489bda17d74eeed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 May 2021 13:11:59 +0200 Subject: [PATCH 36/42] documentation for api - prepend config --- docs/source/development/api.rst | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/docs/source/development/api.rst b/docs/source/development/api.rst index 012178cbe..6905075d9 100644 --- a/docs/source/development/api.rst +++ b/docs/source/development/api.rst @@ -109,6 +109,65 @@ Configure the api using Yaml (see the full configuration below): GET: true HEAD: true +.. note:: + + If you are working on a shared bundle (aka "The chill bundles"), you should define your configuration inside the class :code:`ChillXXXXBundleExtension`, using the "prependConfig" feature: + + .. code-block:: php + + namespace Chill\PersonBundle\DependencyInjection; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; + use Symfony\Component\HttpFoundation\Request; + + /** + * Class ChillPersonExtension + * Loads and manages your bundle configuration + * + * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html} + * @package Chill\PersonBundle\DependencyInjection + */ + class ChillPersonExtension extends Extension implements PrependExtensionInterface + { + public function prepend(ContainerBuilder $container) + { + $this->prependCruds($container); + } + + /** + * @param ContainerBuilder $container + */ + protected function prependCruds(ContainerBuilder $container) + { + $container->prependExtensionConfig('chill_main', [ + 'apis' => [ + [ + 'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod\Origin::class, + 'name' => 'accompanying_period_origin', + 'base_path' => '/api/1.0/person/accompanying-period/origin', + 'controller' => \Chill\PersonBundle\Controller\OpeningApiController::class, + 'base_role' => 'ROLE_USER', + 'actions' => [ + '_index' => [ + 'methods' => [ + Request::METHOD_GET => true, + Request::METHOD_HEAD => true + ], + ], + '_entity' => [ + 'methods' => [ + Request::METHOD_GET => true, + Request::METHOD_HEAD => true + ] + ], + ] + ] + ] + ]); + } + } + The :code:`_index` and :code:`_entity` action ============================================= From f2a04cebe67dbe999b9d578d238d8057e9472031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 May 2021 13:12:18 +0200 Subject: [PATCH 37/42] configure origin route into chillMainExtension --- .../ChillPersonExtension.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 909df750f..84b8bb397 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -337,6 +337,27 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac ] ] + ], + [ + 'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod\Origin::class, + 'name' => 'accompanying_period_origin', + 'base_path' => '/api/1.0/person/accompanying-period/origin', + 'controller' => \Chill\PersonBundle\Controller\OpeningApiController::class, + 'base_role' => 'ROLE_USER', + 'actions' => [ + '_index' => [ + 'methods' => [ + Request::METHOD_GET => true, + Request::METHOD_HEAD => true + ], + ], + '_entity' => [ + 'methods' => [ + Request::METHOD_GET => true, + Request::METHOD_HEAD => true + ] + ], + ] ] ] ]); From ed3f46ce7f60b98a23baf100ec8c5eeaa50578d9 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Thu, 6 May 2021 12:52:29 +0200 Subject: [PATCH 38/42] Fix return type. --- src/Bundle/ChillPersonBundle/Entity/Person.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php index 464be5f31..94e0a0ee5 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Person.php +++ b/src/Bundle/ChillPersonBundle/Entity/Person.php @@ -1011,7 +1011,7 @@ class Person implements HasCenterInterface * By default, the addresses are ordered by date, descending (the most * recent first) */ - public function getAddresses(): ArrayCollection + public function getAddresses(): Collection { return $this->addresses; } From 8c98f2cf6e323ac8cf45941db898e85119405d03 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Fri, 7 May 2021 09:04:29 +0200 Subject: [PATCH 39/42] Update .gitignore file. --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 34dec8d5d..c497e28d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ .composer/* composer.phar composer.lock - docs/build/ +.php_cs.cache ###> symfony/framework-bundle ### /.env.local @@ -19,3 +19,4 @@ docs/build/ /phpunit.xml .phpunit.result.cache ###< phpunit/phpunit ### + From 1b8462b40d40a2d6833d3bd3e937cfd3e255f818 Mon Sep 17 00:00:00 2001 From: nobohan Date: Fri, 7 May 2021 10:27:11 +0200 Subject: [PATCH 40/42] update validto field in chill_main_address for legacy --- .../migrations/Version20210505153727.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/Bundle/ChillMainBundle/migrations/Version20210505153727.php b/src/Bundle/ChillMainBundle/migrations/Version20210505153727.php index c1fff5b9e..42161ba84 100644 --- a/src/Bundle/ChillMainBundle/migrations/Version20210505153727.php +++ b/src/Bundle/ChillMainBundle/migrations/Version20210505153727.php @@ -22,6 +22,26 @@ final class Version20210505153727 extends AbstractMigration $this->addSql('ALTER TABLE chill_main_address ADD linkedToThirdParty_id INT DEFAULT NULL'); $this->addSql('ALTER TABLE chill_main_address ADD CONSTRAINT FK_165051F6114B8DD9 FOREIGN KEY (linkedToThirdParty_id) REFERENCES chill_3party.third_party (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); $this->addSql('CREATE INDEX IDX_165051F6114B8DD9 ON chill_main_address (linkedToThirdParty_id)'); + $this->addSql(' + CREATE TABLE chill_main_address_legacy AS + TABLE chill_main_address; + '); + $this->addSql(' + WITH hydrated_addresses AS ( + SELECT *, rank() OVER (PARTITION BY pa_a.person_id ORDER BY validfrom) + FROM chill_main_address AS aa JOIN chill_person_persons_to_addresses AS pa_a ON aa.id = pa_a.address_id + ) + UPDATE chill_main_address AS b + SET validto = ( + SELECT validfrom - INTERVAL \'1 DAY\' + FROM hydrated_addresses + WHERE hydrated_addresses.id = ( + SELECT a1.id + FROM hydrated_addresses AS a1 JOIN hydrated_addresses AS a2 ON a2.person_id = a1.person_id AND a2.rank = (a1.rank-1) + WHERE a2.id = b.id + ) + ); + '); } public function down(Schema $schema): void @@ -29,5 +49,10 @@ final class Version20210505153727 extends AbstractMigration $this->addSql('ALTER TABLE chill_main_address DROP CONSTRAINT FK_165051F6114B8DD9'); $this->addSql('DROP INDEX IDX_165051F6114B8DD9'); $this->addSql('ALTER TABLE chill_main_address DROP linkedToThirdParty_id'); + $this->addSql('DROP TABLE IF EXISTS chill_main_address_legacy'); + $this->addSql(' + UPDATE chill_main_address + SET validto = null; + '); } } From 2c9edf374151e28b58f14c9ecdfde005faf779de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 7 May 2021 12:13:24 +0200 Subject: [PATCH 41/42] Set Collection normalization to fit into specs from https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/136 --- .../Serializer/Normalizer/CollectionNormalizer.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/CollectionNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/CollectionNormalizer.php index b21bf6326..1ba95d924 100644 --- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/CollectionNormalizer.php +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/CollectionNormalizer.php @@ -23,12 +23,14 @@ class CollectionNormalizer implements NormalizerInterface, NormalizerAwareInterf $paginator = $collection->getPaginator(); $data['count'] = $paginator->getTotalItems(); - $data['first'] = $paginator->getCurrentPageFirstItemNumber(); - $data['items_per_page'] = $paginator->getItemsPerPage(); - $data['next'] = $paginator->hasNextPage() ? + $pagination['first'] = $paginator->getCurrentPageFirstItemNumber(); + $pagination['items_per_page'] = $paginator->getItemsPerPage(); + $pagination['next'] = $paginator->hasNextPage() ? $paginator->getNextPage()->generateUrl() : null; - $data['previous'] = $paginator->hasPreviousPage() ? + $pagination['previous'] = $paginator->hasPreviousPage() ? $paginator->getPreviousPage()->generateUrl() : null; + $pagination['more'] = $paginator->hasNextPage(); + $data['pagination'] = $pagination; // normalize results $data['results'] = $this->normalizer->normalize($collection->getItems(), From 8002725c87e31b63ce575c5b2cd994c1e1787f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 7 May 2021 12:42:49 +0200 Subject: [PATCH 42/42] fix doc for api --- docs/source/development/api.rst | 49 +++++++++++++++++++++++--- docs/source/development/index.rst | 1 + docs/source/development/pagination.rst | 4 ++- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/docs/source/development/api.rst b/docs/source/development/api.rst index 6905075d9..86eb6ff65 100644 --- a/docs/source/development/api.rst +++ b/docs/source/development/api.rst @@ -6,7 +6,7 @@ A copy of the license is included in the section entitled "GNU Free Documentation License". -.. _api +.. _api: API ### @@ -26,7 +26,9 @@ You can also: * hook into the controller to customize some steps; * add more route and steps -.. read-also:: +.. note:: + + Useful links: * `How to use annotation to configure serialization `_ * `How to create your custom normalizer `_ @@ -87,7 +89,7 @@ Create your model on the usual way: Configure api ************* -Configure the api using Yaml (see the full configuration below): +Configure the api using Yaml (see the full configuration: :ref:`api_full_configuration`): .. code-block:: yaml @@ -359,7 +361,46 @@ Then, create the corresponding action into your controller: } } -.. api_full_config: +Serialization for collection +============================ + +A specific model has been defined for returning collection: + +.. code-block:: json + + { + "count": 49, + "results": [ + ], + "pagination": { + "more": true, + "next": "/api/1.0/search.json&q=xxxx......&page=2", + "previous": null, + "first": 0, + "items_per_page": 1 + } + } + + +This can be achieved quickly by assembling results into a :code:`Chill\MainBundle\Serializer\Model\Collection`. The pagination information is given by using :code:`Paginator` (see :ref:`Pagination `). + +.. code-block:: php + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Chill\MainBundle\Pagination\PaginatorInterface; + + class MyController extends AbstractController + { + + protected function serializeCollection(PaginatorInterface $paginator, $entities): Response + { + $model = new Collection($entities, $paginator); + + return $this->json($model, Response::HTTP_OK, [], $context); + } + } + +.. _api_full_configuration: Full configuration example ========================== diff --git a/docs/source/development/index.rst b/docs/source/development/index.rst index af2e3f948..f35bc12db 100644 --- a/docs/source/development/index.rst +++ b/docs/source/development/index.rst @@ -16,6 +16,7 @@ As Chill rely on the `symfony `_ framework, reading the fram Instructions to create a new bundle CRUD (Create - Update - Delete) for one entity + Helpers for building a REST API Routing Menus Forms diff --git a/docs/source/development/pagination.rst b/docs/source/development/pagination.rst index 44c06b308..c0cec9639 100644 --- a/docs/source/development/pagination.rst +++ b/docs/source/development/pagination.rst @@ -7,6 +7,8 @@ Free Documentation License". +.. _pagination-ref: + Pagination ########## @@ -15,7 +17,7 @@ The Bundle :code:`Chill\MainBundle` provides a **Pagination** api which allow yo A simple example **************** -In the controller, get the :class:`Chill\Main\Pagination\PaginatorFactory` from the `Container` and use this :code:`PaginatorFactory` to create a :code:`Paginator` instance. +In the controller, get the :code:`Chill\Main\Pagination\PaginatorFactory` from the `Container` and use this :code:`PaginatorFactory` to create a :code:`Paginator` instance. .. literalinclude:: pagination/example.php