diff --git a/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/DocGenObjectNormalizer.php b/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/DocGenObjectNormalizer.php index 636821258..8361e1b59 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/DocGenObjectNormalizer.php +++ b/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/DocGenObjectNormalizer.php @@ -98,8 +98,9 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte if ($reflection->hasProperty($attribute->getName())) { if (!$reflection->getProperty($attribute->getName())->hasType()) { throw new \LogicException(sprintf( - 'Could not determine how the content is determined for the attribute %s. Add a type on this property', - $attribute->getName() + 'Could not determine how the content is determined for the attribute %s on class %s. Add a type on this property', + $attribute->getName(), + $reflection->getName() )); } @@ -107,8 +108,9 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte } elseif ($reflection->hasMethod($method = 'get' . ucfirst($attribute->getName()))) { if (!$reflection->getMethod($method)->hasReturnType()) { throw new \LogicException(sprintf( - 'Could not determine how the content is determined for the attribute %s. Add a return type on the method', - $attribute->getName() + 'Could not determine how the content is determined for the attribute %s on class %s. Add a return type on the method', + $attribute->getName(), + $reflection->getName() )); } @@ -116,8 +118,9 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte } elseif ($reflection->hasMethod($method = 'is' . ucfirst($attribute->getName()))) { if (!$reflection->getMethod($method)->hasReturnType()) { throw new \LogicException(sprintf( - 'Could not determine how the content is determined for the attribute %s. Add a return type on the method', - $attribute->getName() + 'Could not determine how the content is determined for the attribute %s on class %s. Add a return type on the method', + $attribute->getName(), + $reflection->getName() )); } @@ -125,8 +128,9 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte } elseif ($reflection->hasMethod($attribute->getName())) { if (!$reflection->getMethod($attribute->getName())->hasReturnType()) { throw new \LogicException(sprintf( - 'Could not determine how the content is determined for the attribute %s. Add a return type on the method', - $attribute->getName() + 'Could not determine how the content is determined for the attribute %s on class %s. Add a return type on the method', + $attribute->getName(), + $reflection->getName() )); } diff --git a/src/Bundle/ChillPersonBundle/Controller/RelationshipApiController.php b/src/Bundle/ChillPersonBundle/Controller/RelationshipApiController.php index 72e92ac92..57a8992ab 100644 --- a/src/Bundle/ChillPersonBundle/Controller/RelationshipApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/RelationshipApiController.php @@ -14,6 +14,7 @@ namespace Chill\PersonBundle\Controller; use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Repository\Relationships\RelationshipRepository; +use Chill\PersonBundle\Security\Authorization\PersonVoter; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -36,9 +37,10 @@ class RelationshipApiController extends ApiController */ public function getRelationshipsByPerson(Person $person) { - //TODO: add permissions? (voter?) + $this->denyAccessUnlessGranted(PersonVoter::SEE, $person); + $relationships = $this->repository->findByPerson($person); - return $this->json(array_values($relationships), Response::HTTP_OK, [], ['groups' => ['read']]); + return $this->json($relationships, Response::HTTP_OK, [], ['groups' => ['read']]); } } diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMember.php b/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMember.php index 644c03ac1..22c33f85f 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMember.php +++ b/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMember.php @@ -65,7 +65,7 @@ class HouseholdMember * @ORM\Column(type="integer") * @Serializer\Groups({"read", "docgen:read"}) */ - private $id; + private ?int $id = null; /** * @var Person diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/Position.php b/src/Bundle/ChillPersonBundle/Entity/Household/Position.php index b8f707126..45eb21524 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Household/Position.php +++ b/src/Bundle/ChillPersonBundle/Entity/Household/Position.php @@ -35,7 +35,7 @@ class Position * @ORM\Column(type="integer") * @Serializer\Groups({"read", "docgen:read"}) */ - private ?int $id; + private ?int $id = null; /** * @ORM\Column(type="json") diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php index f58cee747..fb34a779a 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Person.php +++ b/src/Bundle/ChillPersonBundle/Entity/Person.php @@ -588,8 +588,6 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI return $this; } - // a period opened and another one after it - /** * Function used for validation that check if the accompanying periods of * the person are not collapsing (i.e. have not shared days) or having diff --git a/src/Bundle/ChillPersonBundle/Entity/Relationships/Relationship.php b/src/Bundle/ChillPersonBundle/Entity/Relationships/Relationship.php index 45cd546b0..87f250e4e 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Relationships/Relationship.php +++ b/src/Bundle/ChillPersonBundle/Entity/Relationships/Relationship.php @@ -116,6 +116,28 @@ class Relationship implements TrackCreationInterface, TrackUpdateInterface return $this->id; } + /** + * Return the opposite person of the @link{counterpart} person. + * + * this is the from person if the given is associated to the To, + * or the To person otherwise. + * + * @param Person $counterpartthe counterpart + * @throw RuntimeException if the counterpart is neither in the from or to person + */ + public function getOpposite(Person $counterpart): Person + { + if ($this->fromPerson !== $counterpart && $this->toPerson !== $counterpart) { + throw new \RuntimeException("the counterpart is neither the from nor to person for this relationship"); + } + + if ($this->fromPerson === $counterpart) { + return $this->toPerson; + } + + return $this->fromPerson; + } + public function getRelation(): ?Relation { return $this->relation; diff --git a/src/Bundle/ChillPersonBundle/Repository/Relationships/RelationshipRepository.php b/src/Bundle/ChillPersonBundle/Repository/Relationships/RelationshipRepository.php index ab172e459..f82c061e9 100644 --- a/src/Bundle/ChillPersonBundle/Repository/Relationships/RelationshipRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/Relationships/RelationshipRepository.php @@ -11,18 +11,24 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository\Relationships; +use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Entity\Relationships\Relation; use Chill\PersonBundle\Entity\Relationships\Relationship; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; class RelationshipRepository implements ObjectRepository { private EntityRepository $repository; + private EntityManagerInterface $em; + public function __construct(EntityManagerInterface $em) { $this->repository = $em->getRepository(Relationship::class); + $this->em = $em; } public function find($id): ?Relationship @@ -40,19 +46,42 @@ class RelationshipRepository implements ObjectRepository return $this->repository->findBy($criteria, $orderBy, $limit, $offset); } - public function findByPerson($personId): array + /** + * @param Person $person + * @return array|Relationship[] + */ + public function findByPerson(Person $person): array { - // return all relationships of which person is part? or only where person is the fromPerson? - return $this->repository->createQueryBuilder('r') - ->select('r, t') // entity Relationship - ->join('r.relation', 't') - ->where('r.fromPerson = :val') - ->orWhere('r.toPerson = :val') - ->setParameter('val', $personId) + return $this->buildQueryByPerson($person) + ->select('r') ->getQuery() ->getResult(); } + public function countByPerson(Person $person): int + { + return $this->buildQueryByPerson($person) + ->select('COUNT(p)') + ->getQuery() + ->getSingleScalarResult(); + } + + private function buildQueryByPerson(Person $person): QueryBuilder + { + $qb = $this->em->createQueryBuilder(); + $qb + ->from(Relationship::class, 'r') + ->where( + $qb->expr()->orX( + $qb->expr()->eq('r.fromPerson', ':person'), + $qb->expr()->eq('r.toPerson', ':person') + ) + ) + ->setParameter('person', $person); + + return $qb; + } + public function findOneBy(array $criteria): ?Relationship { return $this->findOneBy($criteria); diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonDocGenNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonDocGenNormalizer.php index f103105a0..94fa8a652 100644 --- a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonDocGenNormalizer.php +++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonDocGenNormalizer.php @@ -16,6 +16,8 @@ use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\PersonAltName; +use Chill\PersonBundle\Repository\Relationships\RelationRepository; +use Chill\PersonBundle\Repository\Relationships\RelationshipRepository; use Chill\PersonBundle\Templating\Entity\PersonRender; use DateTimeInterface; use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; @@ -34,16 +36,20 @@ class PersonDocGenNormalizer implements private PersonRender $personRender; + private RelationshipRepository $relationshipRepository; + private TranslatableStringHelper $translatableStringHelper; private TranslatorInterface $translator; public function __construct( PersonRender $personRender, + RelationshipRepository $relationshipRepository, TranslatorInterface $translator, TranslatableStringHelper $translatableStringHelper ) { $this->personRender = $personRender; + $this->relationshipRepository = $relationshipRepository; $this->translator = $translator; $this->translatableStringHelper = $translatableStringHelper; } @@ -94,6 +100,18 @@ class PersonDocGenNormalizer implements ); } + if ($context['docgen:person:with-relations'] ?? false) { + $data['relations'] = $this->normalizer->normalize( + $this->relationshipRepository->findByPerson($person), + $format, + array_merge($context, [ + 'docgen:person:with-household' => false, + 'docgen:person:with-relation' => false, + 'docgen:relationship:counterpart' => $person + ]) + ); + } + return $data; } diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/RelationshipDocGenNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/RelationshipDocGenNormalizer.php new file mode 100644 index 000000000..948824b99 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/RelationshipDocGenNormalizer.php @@ -0,0 +1,87 @@ +translatableStringHelper = $translatableStringHelper; + } + + /** + * @param Relationship $relation + */ + public function normalize($relation, string $format = null, array $context = []) + { + $counterpart = $context['docgen:relationship:counterpart'] ?? null; + $contextPerson = array_merge($context, [ + 'docgen:person:with-relation' => false, + 'docgen:relationship:counterpart' => null, + 'docgen:expects' => Person::class, + ]); + + if (null !== $counterpart) { + $opposite = $relation->getOpposite($counterpart); + } else { + $opposite = null; + } + + if (null === $relation) { + return [ + "id" => "", + "fromPerson" => $nullPerson = $this->normalizer->normalize(null, $format, $contextPerson), + 'toPerson' => $nullPerson, + 'opposite' => $nullPerson, + 'text' => '', + 'relationId' => '', + ]; + } + + return [ + 'id' => $relation->getId(), + 'fromPerson' => $this->normalizer->normalize( + $relation->getFromPerson(), + $format, + $contextPerson + ), + 'toPerson' => $this->normalizer->normalize( + $relation->getToPerson(), + $format, + $contextPerson + ), + 'text' => $relation->getReverse() ? + $this->translatableStringHelper->localize($relation->getRelation()->getReverseTitle()) : + $this->translatableStringHelper->localize($relation->getRelation()->getTitle()), + 'opposite' => $this->normalizer->normalize($opposite, $format, $contextPerson), + 'relationId' => $relation->getRelation()->getId(), + ]; + } + + public function supportsNormalization($data, string $format = null, array $context = []) + { + if ('docgen' !== $format) { + return false; + } + + return $data instanceof Relationship || (null === $data + && ($context['docgen:expects'] ?? null) === Relationship::class); + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonDocGenNormalizerTest.php b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonDocGenNormalizerTest.php index 04ac15c85..892d16211 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonDocGenNormalizerTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonDocGenNormalizerTest.php @@ -11,12 +11,22 @@ declare(strict_types=1); namespace Serializer\Normalizer; +use Chill\MainBundle\Templating\TranslatableStringHelper; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Household\HouseholdMember; use Chill\PersonBundle\Entity\Household\Position; use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Entity\Relationships\Relation; +use Chill\PersonBundle\Entity\Relationships\Relationship; +use Chill\PersonBundle\Repository\Relationships\RelationshipRepository; +use Chill\PersonBundle\Serializer\Normalizer\PersonDocGenNormalizer; +use Chill\PersonBundle\Templating\Entity\PersonRender; +use Prophecy\Argument; +use Prophecy\Argument\Token\AnyValueToken; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Contracts\Translation\TranslatorInterface; use function array_merge; /** @@ -47,6 +57,38 @@ final class PersonDocGenNormalizerTest extends KernelTestCase private NormalizerInterface $normalizer; + private function buildPersonNormalizer( + ?PersonRender $personRender = null, + ?RelationshipRepository $relationshipRepository = null, + ?TranslatorInterface $translator = null, + ?TranslatableStringHelper $translatableStringHelper = null + ): PersonDocGenNormalizer { + $normalizer = new PersonDocGenNormalizer( + $personRender ?? self::$container->get(PersonRender::class), + $relationshipRepository ?? self::$container->get(RelationshipRepository::class), + $translator ??self::$container->get(TranslatorInterface::class), + $translatableStringHelper ?? self::$container->get(TranslatableStringHelperInterface::class) + ); + $normalizerManager = $this->prophesize(NormalizerInterface::class); + $normalizerManager->supportsNormalization(Argument::any(), 'docgen', Argument::any())->willReturn(true); + $normalizerManager->normalize(Argument::type(Person::class), 'docgen', Argument::any()) + ->will(function($args) use ($normalizer) { + return $normalizer->normalize($args[0], $args[1], $args[2]); + }); + $normalizerManager->normalize(Argument::any(), 'docgen', Argument::any())->will( + function ($args) { + if (is_iterable($args[0])) { + $r = []; + foreach ($args[0] as $i) { $r[] = ['fake' => true, 'hash' => spl_object_hash($i)];} + return $r; + } + return ['fake' => true, 'hash' => null !== $args[0] ? spl_object_hash($args[0]) : null]; + }); + $normalizer->setNormalizer($normalizerManager->reveal()); + + return $normalizer; + } + protected function setUp() { self::bootKernel(); @@ -113,7 +155,7 @@ final class PersonDocGenNormalizerTest extends KernelTestCase $householdMember = new HouseholdMember(); $householdMember ->setPosition((new Position())->setAllowHolder(true)->setLabel(['fr' => 'position2']) - ->setShareHousehold(false)) + ->setShareHousehold(true)) ->setHolder(false) ; $person->addHouseholdParticipation($householdMember); @@ -128,7 +170,43 @@ final class PersonDocGenNormalizerTest extends KernelTestCase $this->assertCount(2, $household->getMembers()); $this->assertIsArray($actual); + $this->assertArrayHasKey('household', $actual); + $this->assertCount(2, $actual['household']['currentMembers']); + $this->assertCount(2, $actual['household']['members']); + } - var_dump($actual); + public function testNormalizePersonWithRelationships() + { + $person = (new Person())->setFirstName('Renaud')->setLastName('megane'); + $father = (new Person())->setFirstName('Clément')->setLastName('megane'); + $mother = (new Person())->setFirstName('Mireille')->setLastName('Mathieu'); + $sister = (new Person())->setFirstName('Callie')->setLastName('megane'); + + $relations = [ + (new Relationship())->setFromPerson($person)->setToPerson($father) + ->setReverse(false)->setRelation((new Relation())->setTitle(['fr' => 'Père']) + ->setReverseTitle(['fr' => 'Fils'])), + (new Relationship())->setFromPerson($person)->setToPerson($mother) + ->setReverse(false)->setRelation((new Relation())->setTitle(['fr' => 'Mère']) + ->setReverseTitle(['fr' => 'Fils'])), + (new Relationship())->setFromPerson($person)->setToPerson($sister) + ->setReverse(true)->setRelation((new Relation())->setTitle(['fr' => 'Frère']) + ->setReverseTitle(['fr' => 'Soeur'])), + ]; + + $repository = $this->prophesize(RelationshipRepository::class); + $repository->findByPerson($person)->willReturn($relations); + + $normalizer = $this->buildPersonNormalizer(null, $repository->reveal(), null, null); + + $actual = $normalizer->normalize($person, 'docgen', [ + 'groups' => 'docgen:read', + 'docgen:expects' => Person::class, + 'docgen:person:with-relations' => true, + ]); + + $this->assertIsArray($actual); + $this->assertArrayHasKey('relations', $actual); + $this->assertCount(3, $actual['relations']); } } diff --git a/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/RelationshipDocGenNormalizerTest.php b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/RelationshipDocGenNormalizerTest.php new file mode 100644 index 000000000..5304eed7a --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/RelationshipDocGenNormalizerTest.php @@ -0,0 +1,134 @@ +prophesize(TranslatableStringHelperInterface::class); + $translatableStringHelper->localize(Argument::type('array'))->will( + function ($args) { return $args[0][array_keys($args[0])[0]]; } + ); + + $normalizer = new RelationshipDocGenNormalizer( + $translatableStringHelper->reveal() + ); + + $normalizerManager = $this->prophesize(NormalizerInterface::class); + $normalizerManager->supportsNormalization(Argument::any(), 'docgen', Argument::any())->willReturn(true); + $normalizerManager->normalize(Argument::type(Relationship::class), 'docgen', Argument::any()) + ->will(function($args) use ($normalizer) { + return $normalizer->normalize($args[0], $args[1], $args[2]); + }); + $normalizerManager->normalize(Argument::any(), 'docgen', Argument::any())->will( + function ($args) { + if (null === $args[0]) { + return null; + } elseif (is_iterable($args[0])) { + $r = []; + foreach ($args[0] as $i) { $r[] = ['fake' => true, 'hash' => spl_object_hash($i)];} + return $r; + } elseif (is_object($args[0])) { + return ['fake' => true, 'hash' => null !== $args[0] ? spl_object_hash($args[0]) : null]; + } + return $args[0]; + }); + $normalizer->setNormalizer($normalizerManager->reveal()); + + return $normalizer; + } + + public function testNormalizeRelationshipWithCounterPart() + { + $relationship = new Relationship(); + $relationship + ->setFromPerson($person1 = new Person()) + ->setToPerson($person2 = new Person()) + ->setRelation((new Relation())->setTitle(['fr' => 'title']) + ->setReverseTitle(['fr' => 'reverse title']) + ) + ->setReverse(false) + ; + + $normalizer = $this->buildNormalizer(); + + $this->assertTrue($normalizer->supportsNormalization($relationship, 'docgen', [])); + $this->assertFalse($normalizer->supportsNormalization($person1, 'docgen', [])); + + $actual = $normalizer->normalize($relationship, 'docgen', [ + 'docgen:expects' => Relationship::class, + 'docgen:relationship:counterpart' => $person1 + ]); + + $this->assertIsArray($actual); + $this->assertEqualsCanonicalizing( + ['fromPerson', 'toPerson', 'id', 'relationId', 'text', 'opposite'], + array_keys($actual), + 'check that the expected keys are present' + ); + $this->assertEquals(spl_object_hash($person2), $actual['opposite']['hash']); + } + + public function testNormalizeRelationshipWithoutCounterPart() + { + $relationship = new Relationship(); + $relationship + ->setFromPerson($person1 = new Person()) + ->setToPerson($person2 = new Person()) + ->setRelation((new Relation())->setTitle(['fr' => 'title']) + ->setReverseTitle(['fr' => 'reverse title']) + ) + ->setReverse(false) + ; + + $normalizer = $this->buildNormalizer(); + + $this->assertTrue($normalizer->supportsNormalization($relationship, 'docgen', [])); + $this->assertFalse($normalizer->supportsNormalization($person1, 'docgen', [])); + + $actual = $normalizer->normalize($relationship, 'docgen', [ + 'docgen:expects' => Relationship::class, + ]); + $this->assertIsArray($actual); + $this->assertEqualsCanonicalizing( + ['fromPerson', 'toPerson', 'id', 'relationId', 'text', 'opposite'], + array_keys($actual), + 'check that the expected keys are present' + ); + $this->assertEquals(null, $actual['opposite']); + } + + public function testNormalizeRelationshipNull() + { + $relationship = null; + + $normalizer = $this->buildNormalizer(); + + $this->assertTrue($normalizer->supportsNormalization($relationship, 'docgen', [ + 'docgen:expects' => Relationship::class + ])); + $this->assertFalse($normalizer->supportsNormalization($relationship, 'docgen', [ + 'docgen:expects' => Person::class + ])); + + $actual = $normalizer->normalize($relationship, 'docgen', [ + 'docgen:expects' => Relationship::class, + ]); + $this->assertIsArray($actual); + $this->assertEqualsCanonicalizing( + ['fromPerson', 'toPerson', 'id', 'relationId', 'text', 'opposite'], + array_keys($actual), + 'check that the expected keys are present' + ); + } +}