Преглед на файлове

add FederationStructure api resource and provider

Olivier Massot преди 3 години
родител
ревизия
544f67b02e

+ 321 - 76
src/ApiResources/Public/FederationStructure.php

@@ -1,59 +1,85 @@
 <?php
+declare(strict_types=1);
+
 namespace App\ApiResources\Public;
 
 use ApiPlatform\Core\Annotation\ApiProperty;
 use ApiPlatform\Core\Annotation\ApiResource;
 use App\ApiResources\ApiResourcesInterface;
+use Symfony\Component\Serializer\Annotation\Groups;
 
 /**
  * Structure telle qu'elle est représentée sur l'iframe de recherche des structures d'une fédération
  */
 #[ApiResource(
-    collectionOperations:[
+    collectionOperations: [
         'get' => [
             'method' => 'GET',
-            'path' => '/api/public/federation_structures/get?parent-id=${parentId}',
+            'path' => '/public/federation_structures' // + '?parent={\d+}'
         ]
     ],
     itemOperations: [
         'get' => [
             'method' => 'GET',
-            'path' => '/api/public/federation_structures/get?organization-id=${organizationId}',
+            'path' => '/public/federation_structures/{id}',
+            'requirements' => ['id' => '\d+']
         ]
     ]
 )]
 class FederationStructure implements ApiResourcesInterface
 {
-// n1.parent_id as n1Id, net1.name as n1Name, n2.parent_id as n2Id, n3.parent_id as n3Id, n4.parent_id as n4Id, n5.parent_id as n5Id,
-
     #[ApiProperty(identifier: true)]
     private int $id;
 
-    private string $name;
+    private ?string $name;
+
+    private ?int $logoId;
 
-    private int $logoId;
+    private ?string $description;
 
-    private string $principalType;
+    private ?int $imageId;
 
-    private string $website;
+    private ?string $principalType;
+
+    private ?string $website;
 
     private string $addresses;
 
-    private string $practices;
+    private ?string $telphone;
+
+    private ?string $mobilPhone;
+
+    private ?string $email;
+
+    private ?string $facebook;
+
+    private ?string $twitter;
+
+    private ?string $instagram;
+
+    private ?string $youtube;
+
+    private ?array $articles;
+
+    private ?string $practices;
 
-    private int $parentId;
+    private ?float $latitude;
 
-    private int $n1Id;
+    private ?float $longitude;
 
-    private int $n2Id;
+    private ?int $n1Id;
 
-    private int $n3Id;
+    private ?string $n1Name;
 
-    private int $n4Id;
+    private ?int $n2Id;
 
-    private int $n5Id;
+    private ?int $n3Id;
 
-    private string $parents;
+    private ?int $n4Id;
+
+    private ?int $n5Id;
+
+    private ?string $parents;
 
     /**
      * @return int
@@ -70,7 +96,6 @@ class FederationStructure implements ApiResourcesInterface
     public function setId(int $id): self
     {
         $this->id = $id;
-
         return $this;
     }
 
@@ -84,6 +109,7 @@ class FederationStructure implements ApiResourcesInterface
 
     /**
      * @param string $name
+     * @return FederationStructure
      */
     public function setName(string $name): self
     {
@@ -93,218 +119,437 @@ class FederationStructure implements ApiResourcesInterface
     }
 
     /**
-     * @return int
+     * @return int|null
      */
-    public function getLogoId(): int
+    public function getLogoId(): ?int
     {
         return $this->logoId;
     }
 
     /**
-     * @param int $logoId
+     * @param int|null $logoId
+     * @return FederationStructure
      */
-    public function setLogoId(int $logoId): self
+    public function setLogoId(?int $logoId): self
     {
         $this->logoId = $logoId;
+        return $this;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getDescription(): ?string
+    {
+        return $this->description;
+    }
+
+    /**
+     * @param string|null $description
+     * @return FederationStructure
+     */
+    public function setDescription(?string $description): self
+    {
+        $this->description = $description;
 
         return $this;
     }
 
     /**
-     * @return string
+     * @return int|null
+     */
+    public function getImageId(): ?int
+    {
+        return $this->imageId;
+    }
+
+    /**
+     * @param int|null $imageId
+     * @return FederationStructure
+     */
+    public function setImageId(?int $imageId): self
+    {
+        $this->imageId = $imageId;
+
+        return $this;
+    }
+
+    /**
+     * @return string|null
      */
-    public function getPrincipalType(): string
+    public function getPrincipalType(): ?string
     {
         return $this->principalType;
     }
 
     /**
-     * @param string $principalType
+     * @param string|null $principalType
+     * @return FederationStructure
      */
-    public function setPrincipalType(string $principalType): self
+    public function setPrincipalType(?string $principalType): self
     {
         $this->principalType = $principalType;
-
         return $this;
     }
 
     /**
-     * @return string
+     * @return string|null
      */
-    public function getWebsite(): string
+    public function getWebsite(): ?string
     {
         return $this->website;
     }
 
     /**
-     * @param string $website
+     * @param string|null $website
+     * @return FederationStructure
      */
-    public function setWebsite(string $website): self
+    public function setWebsite(?string $website): self
     {
         $this->website = $website;
-
         return $this;
     }
 
     /**
-     * @return string
+     * @return string|null
      */
-    public function getAddresses(): string
+    public function getAddresses(): ?string
     {
         return $this->addresses;
     }
 
     /**
-     * @param string $addresses
+     * @param string|null $addresses
+     * @return FederationStructure
      */
-    public function setAddresses(string $addresses): self
+    public function setAddresses(?string $addresses): self
     {
         $this->addresses = $addresses;
+        return $this;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getTelphone(): ?string
+    {
+        return $this->telphone;
+    }
+
+    /**
+     * @param string|null $telphone
+     */
+    public function setTelphone(?string $telphone): self
+    {
+        $this->telphone = $telphone;
 
         return $this;
     }
 
     /**
-     * @return string
+     * @return string|null
+     */
+    public function getMobilPhone(): ?string
+    {
+        return $this->mobilPhone;
+    }
+
+    /**
+     * @param string|null $mobilPhone
+     */
+    public function setMobilPhone(?string $mobilPhone): self
+    {
+        $this->mobilPhone = $mobilPhone;
+
+        return $this;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getEmail(): ?string
+    {
+        return $this->email;
+    }
+
+    /**
+     * @param string|null $email
+     */
+    public function setEmail(?string $email): self
+    {
+        $this->email = $email;
+
+        return $this;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getFacebook(): ?string
+    {
+        return $this->facebook;
+    }
+
+    /**
+     * @param string|null $facebook
      */
-    public function getPractices(): string
+    public function setFacebook(?string $facebook): self
+    {
+        $this->facebook = $facebook;
+
+        return $this;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getTwitter(): ?string
+    {
+        return $this->twitter;
+    }
+
+    /**
+     * @param string|null $twitter
+     */
+    public function setTwitter(?string $twitter): self
+    {
+        $this->twitter = $twitter;
+
+        return $this;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getInstagram(): ?string
+    {
+        return $this->instagram;
+    }
+
+    /**
+     * @param string|null $instagram
+     */
+    public function setInstagram(?string $instagram): self
+    {
+        $this->instagram = $instagram;
+
+        return $this;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getYoutube(): ?string
+    {
+        return $this->youtube;
+    }
+
+    /**
+     * @param string|null $youtube
+     * @return FederationStructure
+     */
+    public function setYoutube(?string $youtube): self
+    {
+        $this->youtube = $youtube;
+
+        return $this;
+    }
+
+    /**
+     * @return array|null
+     */
+    public function getArticles(): ?array
+    {
+        return $this->articles;
+    }
+
+    /**
+     * @param array|null $articles
+     */
+    public function setArticles(?array $articles): self
+    {
+        $this->articles = $articles;
+
+        return $this;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getPractices(): ?string
     {
         return $this->practices;
     }
 
     /**
-     * @param string $practices
+     * @param string|null $practices
+     * @return FederationStructure
      */
-    public function setPractices(string $practices): self
+    public function setPractices(?string $practices): self
     {
         $this->practices = $practices;
-
         return $this;
     }
 
     /**
-     * @return int
+     * @return float|null
+     */
+    public function getLatitude(): ?float
+    {
+        return $this->latitude;
+    }
+
+    /**
+     * @param float|null $latitude
+     * @return FederationStructure
      */
-    public function getParentId(): int
+    public function setLatitude(?float $latitude): self
     {
-        return $this->parentId;
+        $this->latitude = $latitude;
+        return $this;
     }
 
     /**
-     * @param int $parentId
+     * @return float|null
      */
-    public function setParentId(int $parentId): self
+    public function getLongitude(): ?float
     {
-        $this->parentId = $parentId;
+        return $this->longitude;
+    }
 
+    /**
+     * @param float|null $longitude
+     * @return FederationStructure
+     */
+    public function setLongitude(?float $longitude): self
+    {
+        $this->longitude = $longitude;
         return $this;
     }
 
     /**
-     * @return int
+     * @return int|null
      */
-    public function getN1Id(): int
+    public function getN1Id(): ?int
     {
         return $this->n1Id;
     }
 
     /**
-     * @param int $n1Id
+     * @param int|null $n1Id
+     * @return FederationStructure
      */
-    public function setN1Id(int $n1Id): self
+    public function setN1Id(?int $n1Id): self
     {
         $this->n1Id = $n1Id;
+        return $this;
+    }
 
+    /**
+     * @return string|null
+     */
+    public function getN1Name(): ?string
+    {
+        return $this->n1Name;
+    }
+
+    /**
+     * @param string|null $n1Name
+     * @return FederationStructure
+     */
+    public function setN1Name(?string $n1Name): self
+    {
+        $this->n1Name = $n1Name;
         return $this;
     }
 
     /**
-     * @return int
+     * @return int|null
      */
-    public function getN2Id(): int
+    public function getN2Id(): ?int
     {
         return $this->n2Id;
     }
 
     /**
-     * @param int $n2Id
+     * @param int|null $n2Id
+     * @return FederationStructure
      */
-    public function setN2Id(int $n2Id): self
+    public function setN2Id(?int $n2Id): self
     {
         $this->n2Id = $n2Id;
-
         return $this;
     }
 
     /**
-     * @return int
+     * @return int|null
      */
-    public function getN3Id(): int
+    public function getN3Id(): ?int
     {
         return $this->n3Id;
     }
 
     /**
-     * @param int $n3Id
+     * @param int|null $n3Id
+     * @return FederationStructure
      */
-    public function setN3Id(int $n3Id): self
+    public function setN3Id(?int $n3Id): self
     {
         $this->n3Id = $n3Id;
-
         return $this;
     }
 
     /**
-     * @return int
+     * @return int|null
      */
-    public function getN4Id(): int
+    public function getN4Id(): ?int
     {
         return $this->n4Id;
     }
 
     /**
-     * @param int $n4Id
+     * @param int|null $n4Id
+     * @return FederationStructure
      */
-    public function setN4Id(int $n4Id): self
+    public function setN4Id(?int $n4Id): self
     {
         $this->n4Id = $n4Id;
-
         return $this;
     }
 
     /**
-     * @return int
+     * @return int|null
      */
-    public function getN5Id(): int
+    public function getN5Id(): ?int
     {
         return $this->n5Id;
     }
 
     /**
-     * @param int $n5Id
+     * @param int|null $n5Id
+     * @return FederationStructure
      */
-    public function setN5Id(int $n5Id): self
+    public function setN5Id(?int $n5Id): self
     {
         $this->n5Id = $n5Id;
-
         return $this;
     }
 
     /**
-     * @return string
+     * @return string|null
      */
-    public function getParents(): string
+    public function getParents(): ?string
     {
         return $this->parents;
     }
 
     /**
-     * @param string $parents
+     * @param string|null $parents
+     * @return FederationStructure
      */
-    public function setParents(string $parents): self
+    public function setParents(?string $parents): self
     {
         $this->parents = $parents;
-
         return $this;
     }
 }

+ 12 - 6
src/DataProvider/Public/FederationStructureDataProvider.php

@@ -6,15 +6,16 @@ use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
 use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
 use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
 use App\ApiResources\Public\FederationStructure;
-use App\Service\ApiResourceBuilder\Public\FederationStructureBuilder;
+use App\Repository\Organization\OrganizationRepository;
+use Doctrine\Common\Collections\ArrayCollection;
 use Symfony\Component\HttpFoundation\RequestStack;
 
 
 class FederationStructureDataProvider implements ItemDataProviderInterface, CollectionDataProviderInterface, RestrictedDataProviderInterface
 {
     public function __construct(
-        private FederationStructureBuilder $builder,
-        private RequestStack $requestStack
+        private RequestStack $requestStack,
+        private OrganizationRepository $organizationRepository
     ) {}
 
     public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
@@ -24,15 +25,20 @@ class FederationStructureDataProvider implements ItemDataProviderInterface, Coll
 
     public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?FederationStructure
     {
-        return $this->builder->getFederationStructureByOrganizationId($id);
+        return $this->organizationRepository->getFederationStructureByOrganizationId($id);
     }
 
-    public function getCollection(string $resourceClass, string $operationName = null): array
+    public function getCollection(string $resourceClass, string $operationName = null): ArrayCollection
     {
         $request = $this->requestStack->getCurrentRequest();
         if ($request === null) {
             throw new \RuntimeException('Undefined request');
         }
-        return $this->builder->getDataByFederationId($request->get('parent-id'));
+        $parentId = $request->query->get('parent');
+        if (empty($parentId) || !is_numeric($parentId)) {
+            throw new \RuntimeException('Bad or missing parent value');
+        }
+
+        return new ArrayCollection($this->organizationRepository->getChildrenStructuresByFederationId($parentId));
     }
 }

+ 137 - 2
src/Repository/Organization/OrganizationRepository.php

@@ -3,9 +3,10 @@ declare(strict_types=1);
 
 namespace App\Repository\Organization;
 
-use App\DQL\DateConditions;
+use App\ApiResources\Public\FederationStructure;
 use App\Entity\Organization\Organization;
 use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\Persistence\ManagerRegistry;
 
 /**
@@ -16,8 +17,142 @@ use Doctrine\Persistence\ManagerRegistry;
  */
 class OrganizationRepository extends ServiceEntityRepository
 {
-    public function __construct(ManagerRegistry $registry)
+    public function __construct(ManagerRegistry $registry, private EntityManagerInterface $em)
     {
         parent::__construct($registry, Organization::class);
     }
+
+    /**
+     * Route optimisée pour retourner les données de réseau d'une structure membre de fédération, sous forme d'Api resources
+     * de type FederationStructure.
+     *
+     * Cette route est utilisée par l'iframe de recherche des structures
+     * @see https://gitlab.2iopenservice.com/opentalent/frames
+     *
+     * @param int $organizationId
+     * @return FederationStructure
+     * @throws \Doctrine\DBAL\DBALException
+     * @throws \Doctrine\DBAL\Driver\Exception
+     * @throws \Doctrine\DBAL\Exception
+     */
+    public function getFederationStructureByOrganizationId(int $organizationId): FederationStructure
+    {
+
+        $sql = "SELECT o.id, o.name, o.logo_id as logoId, o.description, o.image_id as imageId, p.otherWebsite as website, a.latitude, a.longitude,
+                       GROUP_CONCAT(COLUMN_JSON(COLUMN_CREATE(
+                             'type', oa.type, 'latitude', a.latitude, 'longitude', a.longitude, 
+                             'streetAddress', TRIM(BOTH '\n' FROM CONCAT_WS('\n', a.addressOwner, a.streetAddress, a.streetAddressSecond, a.streetAddressThird)),
+                             'postalCode', a.postalCode, 'addressCity', a.addressCity, 'country', c.name))) as addresses,
+                       cp.telphone, cp.mobilPhone, cp.email, o.facebook, o.twitter, o.instagram, o.youtube,
+                       (SELECT CONCAT(GROUP_CONCAT(DISTINCT CONCAT(tp.name)))
+                        FROM organization_type_of_practices AS otp
+                        LEFT JOIN TypeOfPractice AS tp ON(tp.id = otp.typeofpractice_id)
+                        WHERE otp.organization_id = o.id) AS practices,
+                       oar.articles,
+                       n1.parent_id as n1Id, net1.name as n1Name
+                    FROM opentalent.Organization o
+                        INNER JOIN opentalent.Parameters p on o.parameters_id = p.id
+                        LEFT JOIN opentalent.OrganizationAddressPostal oa on oa.organization_id = o.id
+                        LEFT JOIN opentalent.AddressPostal a on oa.addressPostal_id = a.id
+                        LEFT JOIN opentalent.Country c ON c.id = a.addressCountry_id
+                        INNER JOIN (SELECT * FROM NetworkOrganization WHERE parent_id NOT IN (32366, 13) AND (endDate IS NULL OR endDate = '0000-00-00')) n1 on n1.organization_id = o.id
+                        INNER JOIN Organization net1 ON net1.id = n1.parent_id
+                        LEFT JOIN opentalent.organization_contactpoint ocp ON ocp.organization_id = o.id
+                        INNER JOIN (SELECT * FROM opentalent.ContactPoint WHERE `contactType`='PRINCIPAL') cp ON cp.id = ocp.contactPoint_id
+                        LEFT JOIN (
+                            SELECT oar_.organization_id, GROUP_CONCAT(COLUMN_JSON(COLUMN_CREATE('id', oar_.id, 'title', oar_.title, 'date', DATE_FORMAT(oar_.date, '%Y-%m-%dT%TZ'), 'link', oar_.link))) as articles
+                            FROM (SELECT * FROM OrganizationArticle WHERE link is not null and link != '' ORDER BY date DESC) as oar_
+                            group by organization_id
+                        ) oar ON oar.organization_id = o.id
+                    WHERE o.id = :organizationId;";
+
+        $stmt = $this->em->getConnection()->prepare($sql);
+        $data = $stmt->executeQuery(['organizationId' => $organizationId])->fetchAssociative();
+        return self::buildFederationStructure($data);
+    }
+
+    /**
+     * Route optimisée pour retourner l'ensemble des structures d'une fédération sous forme d'Api resources
+     * de type FederationStructure
+     *
+     * Cette route est utilisée par l'iframe de recherche des structures
+     * @see https://gitlab.2iopenservice.com/opentalent/frames
+     *
+     * @param int $parentId
+     * @return array
+     * @throws \Doctrine\DBAL\DBALException
+     * @throws \Doctrine\DBAL\Driver\Exception
+     * @throws \Doctrine\DBAL\Exception
+     */
+    public function getChildrenStructuresByFederationId(int $parentId): array
+    {
+        // NOTE: Cette route est utilisée pour l'affichage et la recherche des structures adhérentes à une fédération
+        // Pour éviter une requête récursive et conserver des performances correctes, on a mis en place ces JOIN chainés.
+        // Au moment du développement de cette route (juin 2021), aucune structure n'a plus de 4 fédération parentes,
+        // les 5 niveaux de JOIN devraient donc suffire.
+        $sql = "SELECT o.id, o.name, o.logo_id as logoId, o.principalType, p.otherWebsite as website,    
+                        GROUP_CONCAT(COLUMN_JSON(COLUMN_CREATE(
+                             'type', oa.type, 'latitude', a.latitude, 'longitude', a.longitude, 
+                             'streetAddress', TRIM(BOTH '\n' FROM CONCAT_WS('\n', a.streetAddress, a.streetAddressSecond, a.streetAddressThird)),
+                             'postalCode', a.postalCode, 'addressCity', a.addressCity, 'country', c.name))) as addresses,
+                        (SELECT CONCAT(GROUP_CONCAT(DISTINCT CONCAT(tp.name)))
+                        FROM organization_type_of_practices AS otp
+                        LEFT JOIN TypeOfPractice AS tp ON(tp.id = otp.typeofpractice_id)
+                        WHERE otp.organization_id = o.id) AS practices,
+                       n1.parent_id as n1Id, net1.name as n1Name, n2.parent_id as n2Id, n3.parent_id as n3Id, n4.parent_id as n4Id, n5.parent_id as n5Id,
+                       CONCAT_WS(',', n1.parent_id, n2.parent_id, n3.parent_id, n4.parent_id, n5.parent_id) as parents
+                    FROM opentalent.Organization o
+                        INNER JOIN opentalent.Parameters p on o.parameters_id = p.id
+                        LEFT JOIN opentalent.OrganizationAddressPostal oa on oa.organization_id = o.id
+                        LEFT JOIN opentalent.AddressPostal a on oa.addressPostal_id = a.id
+                        LEFT JOIN opentalent.Country c ON (c.id = a.addressCountry_id)
+                        INNER JOIN (SELECT DISTINCT organization_id, parent_id FROM NetworkOrganization WHERE parent_id NOT IN (32366, 13) AND (endDate IS NULL OR endDate = '0000-00-00')) n1 on n1.organization_id = o.id
+                        INNER JOIN Organization net1 ON net1.id = n1.parent_id
+                        LEFT JOIN (SELECT DISTINCT organization_id, parent_id FROM NetworkOrganization WHERE parent_id NOT IN (32366, 13) AND (endDate IS NULL OR endDate = '0000-00-00')) n2 on n2.organization_id = n1.parent_id
+                        LEFT JOIN (SELECT DISTINCT organization_id, parent_id FROM NetworkOrganization WHERE parent_id NOT IN (32366, 13) AND (endDate IS NULL OR endDate = '0000-00-00')) n3 on n3.organization_id = n2.parent_id
+                        LEFT JOIN (SELECT DISTINCT organization_id, parent_id FROM NetworkOrganization WHERE parent_id NOT IN (32366, 13) AND (endDate IS NULL OR endDate = '0000-00-00')) n4 on n4.organization_id = n3.parent_id
+                        LEFT JOIN (SELECT DISTINCT organization_id, parent_id FROM NetworkOrganization WHERE parent_id NOT IN (32366, 13) AND (endDate IS NULL OR endDate = '0000-00-00')) n5 on n5.organization_id = n4.parent_id
+                        WHERE :parentId IN (n1.parent_id, n2.parent_id, n3.parent_id, n4.parent_id, n5.parent_id)
+                        GROUP BY o.id
+        ;";
+
+        $stmt = $this->em->getConnection()->prepare($sql);
+        $rows = $stmt->executeQuery(['parentId' => $parentId])->fetchAllAssociative();
+
+        return array_map('self::buildFederationStructure', $rows);
+    }
+
+    /**
+     * Build a FederationStructure with the data provided by getFederationStructureByOrganizationId() and
+     * getChildrenStructuresByFederationId().
+     */
+    private static function buildFederationStructure(array $data) {
+        return (new FederationStructure())
+            ->setId((int)$data['id'])
+            ->setName($data['name'])
+            ->setLogoId(isset($data['logoId']) ? (int)$data['logoId'] : null)
+            ->setDescription($data['description'] ?? null)
+            ->setImageId(isset($data['imageId']) ? (int)$data['imageId'] : null)
+            ->setPrincipalType($data['principalType'] ?? null)
+            ->setWebsite($data['website'])
+            ->setAddresses($data['addresses'])
+            ->setTelphone($data['telphone'] ?? null)
+            ->setMobilPhone($data['mobilPhone'] ?? null)
+            ->setEmail($data['email'] ?? null)
+            ->setFacebook($data['facebook'] ?? null)
+            ->setTwitter($data['twitter'] ?? null)
+            ->setInstagram($data['instagram'] ?? null)
+            ->setYoutube($data['youtube'] ?? null)
+            ->setArticles($data['articles'] ?? null)
+            ->setPractices($data['practices'])
+            ->setLatitude($data['latitude'] ?? null)
+            ->setLongitude($data['longitude'] ?? null)
+            ->setN1Id(isset($data['n1Id']) ? (int)$data['n1Id'] : null)
+            ->setN1Name($data['n1Name'] ?? null)
+            ->setN2Id(isset($data['n2id']) ? (int)$data['n2id'] : null)
+            ->setN3Id(isset($data['n3Id']) ? (int)$data['n3Id'] : null)
+            ->setN4Id(isset($data['n4Id']) ? (int)$data['n4Id'] : null)
+            ->setN5Id(isset($data['n5Id']) ? (int)$data['n5Id'] : null)
+            ->setParents($data['parents'] ?? null);
+    }
 }