src/Controller/Api/WhileProject/CategoriesController.php line 75

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Api\WhileProject;
  3. use App\Entity\WhileProject\MissionsCategories;
  4. use Doctrine\ORM\EntityManagerInterface;
  5. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  6. use Symfony\Component\HttpFoundation\JsonResponse;
  7. use Symfony\Component\HttpFoundation\Request;
  8. use Symfony\Component\HttpFoundation\Response;
  9. /**
  10.  * Gestion des catégories de missions.
  11.  *
  12.  * Modèle : "1 row = 1 langue" (locale + code stable cross-langue).
  13.  * Hiérarchie via self-FK `parent`. L'arbre parent/enfants est reconstruit
  14.  * côté front (le backend renvoie une liste plate avec `parent_code`).
  15.  *
  16.  *  Public :
  17.  *  - GET    /missions/categories                    → liste publique (locale fr par défaut)
  18.  *  - GET    /missions/categories/by-slug/{slug}     → catégorie + enfants + breadcrumb
  19.  *
  20.  *  Admin :
  21.  *  - GET    /missions/categories/all                → liste complète (toutes locales, désactivées incl.)
  22.  *  - POST   /missions/categories                    → créer
  23.  *  - POST   /missions/categories/{id}               → mettre à jour
  24.  *  - DELETE /missions/categories/{id}               → supprimer
  25.  *  - GET    /missions/categories/export             → export JSON
  26.  *  - POST   /missions/categories/import             → import JSON (mode merge)
  27.  */
  28. class CategoriesController extends AbstractController
  29. {
  30.     private EntityManagerInterface $em;
  31.     public function __construct(EntityManagerInterface $em)
  32.     {
  33.         $this->em $em;
  34.     }
  35.     /**
  36.      * Liste publique : catégories actives, filtrées par locale (défaut: fr).
  37.      * GET /missions/categories?locale=fr
  38.      */
  39.     public function list(Request $request): JsonResponse
  40.     {
  41.         $locale strtolower(trim((string) $request->query->get('locale''fr')));
  42.         $rows $this->em->createQueryBuilder()
  43.             ->select('c')
  44.             ->from(MissionsCategories::class, 'c')
  45.             ->leftJoin('c.parent''p')->addSelect('p')
  46.             ->where('c.enabled = :ok')
  47.             ->andWhere('c.locale = :loc')
  48.             ->setParameter('ok'true)
  49.             ->setParameter('loc'$locale)
  50.             ->orderBy('c.position''ASC')
  51.             ->addOrderBy('c.name''ASC')
  52.             ->getQuery()->getResult();
  53.         $array = [];
  54.         foreach ($rows as $c) {
  55.             $array[] = $this->serialize($c);
  56.         }
  57.         return new JsonResponse($array);
  58.     }
  59.     /**
  60.      * Lookup public par slug.
  61.      * GET /missions/categories/by-slug/{slug}?locale=fr
  62.      *
  63.      * Retourne la catégorie, ses sous-catégories directes, et son fil d'ariane
  64.      * (parent direct si présent).
  65.      */
  66.     public function bySlug(Request $requeststring $slug): JsonResponse
  67.     {
  68.         $locale strtolower(trim((string) $request->query->get('locale''fr')));
  69.         $repo   $this->em->getRepository(MissionsCategories::class);
  70.         $category $repo->findOneBy([
  71.             'locale'  => $locale,
  72.             'slug'    => $slug,
  73.             'enabled' => true,
  74.         ]);
  75.         if (!$category) {
  76.             return new JsonResponse(['error' => 'not_found'], Response::HTTP_NOT_FOUND);
  77.         }
  78.         // Sous-catégories directes
  79.         $childrenRows $repo->findBy(
  80.             ['parent' => $category'enabled' => true],
  81.             ['position' => 'ASC''name' => 'ASC']
  82.         );
  83.         $children = [];
  84.         foreach ($childrenRows as $c) {
  85.             $children[] = $this->serialize($c);
  86.         }
  87.         // Breadcrumb (parent direct)
  88.         $breadcrumb = [];
  89.         $parent $category->getParent();
  90.         if ($parent) {
  91.             $breadcrumb[] = $this->serialize($parent);
  92.         }
  93.         return new JsonResponse([
  94.             'category'   => $this->serialize($category),
  95.             'children'   => $children,
  96.             'breadcrumb' => $breadcrumb,
  97.         ]);
  98.     }
  99.     /**
  100.      * Liste complète admin (inclut désactivées, toutes locales).
  101.      * GET /missions/categories/all
  102.      */
  103.     public function listAll(): JsonResponse
  104.     {
  105.         if (!$this->isAdmin()) {
  106.             return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  107.         }
  108.         $rows $this->em->createQueryBuilder()
  109.             ->select('c')
  110.             ->from(MissionsCategories::class, 'c')
  111.             ->leftJoin('c.parent''p')->addSelect('p')
  112.             ->orderBy('c.locale''ASC')
  113.             ->addOrderBy('c.position''ASC')
  114.             ->addOrderBy('c.name''ASC')
  115.             ->getQuery()->getResult();
  116.         $array = [];
  117.         foreach ($rows as $c) {
  118.             $array[] = $this->serialize($c);
  119.         }
  120.         return new JsonResponse($array);
  121.     }
  122.     /**
  123.      * Création d'une catégorie (admin).
  124.      * POST /missions/categories
  125.      * Body : { locale, code, slug?, name, description?, icon?, color?,
  126.      *          seo_title?, seo_description?, position?, enabled?, parent_code? }
  127.      */
  128.     public function create(Request $request): JsonResponse
  129.     {
  130.         if (!$this->isAdmin()) {
  131.             return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  132.         }
  133.         $data json_decode($request->getContent(), true) ?: [];
  134.         if (empty($data['name'])) {
  135.             return new JsonResponse(['error' => 'Le nom est requis'], Response::HTTP_BAD_REQUEST);
  136.         }
  137.         $locale strtolower(trim((string) ($data['locale'] ?? 'fr')));
  138.         $code   = !empty($data['code']) ? trim($data['code']) : $this->slugify($data['name']);
  139.         $slug   = !empty($data['slug']) ? $this->slugify($data['slug']) : $this->slugify($data['name']);
  140.         $repo $this->em->getRepository(MissionsCategories::class);
  141.         if ($repo->findOneBy(['locale' => $locale'code' => $code])) {
  142.             return new JsonResponse(['error' => "Une catégorie avec ce code existe déjà en {$locale}"], Response::HTTP_BAD_REQUEST);
  143.         }
  144.         if ($repo->findOneBy(['locale' => $locale'slug' => $slug])) {
  145.             return new JsonResponse(['error' => "Une catégorie avec ce slug existe déjà en {$locale}"], Response::HTTP_BAD_REQUEST);
  146.         }
  147.         $cat = new MissionsCategories();
  148.         $cat->setLocale($locale);
  149.         $cat->setCode($code);
  150.         $cat->setSlug($slug);
  151.         $cat->setName(trim($data['name']));
  152.         $cat->setDescription($data['description'] ?? null);
  153.         $cat->setIcon($data['icon'] ?? null);
  154.         $cat->setColor($data['color'] ?? null);
  155.         $cat->setSeoTitle($data['seo_title'] ?? null);
  156.         $cat->setSeoDescription($data['seo_description'] ?? null);
  157.         $cat->setPosition((int) ($data['position'] ?? 0));
  158.         $cat->setEnabled((bool) ($data['enabled'] ?? true));
  159.         if (!empty($data['parent_code'])) {
  160.             $parent $repo->findOneBy(['locale' => $locale'code' => trim($data['parent_code'])]);
  161.             if (!$parent) {
  162.                 return new JsonResponse(['error' => "Parent introuvable : {$data['parent_code']}"], Response::HTTP_BAD_REQUEST);
  163.             }
  164.             $cat->setParent($parent);
  165.         }
  166.         $this->em->persist($cat);
  167.         $this->em->flush();
  168.         return new JsonResponse(['success' => true'category' => $this->serialize($cat)], Response::HTTP_CREATED);
  169.     }
  170.     /**
  171.      * Mise à jour (admin).
  172.      * POST /missions/categories/{id}
  173.      */
  174.     public function update(Request $requestint $id): JsonResponse
  175.     {
  176.         if (!$this->isAdmin()) {
  177.             return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  178.         }
  179.         $repo $this->em->getRepository(MissionsCategories::class);
  180.         $cat  $repo->find($id);
  181.         if (!$cat) {
  182.             return new JsonResponse(['error' => 'Catégorie introuvable'], Response::HTTP_NOT_FOUND);
  183.         }
  184.         $data json_decode($request->getContent(), true) ?: [];
  185.         if (isset($data['name']))                       $cat->setName(trim($data['name']));
  186.         if (array_key_exists('description'$data))     $cat->setDescription($data['description']);
  187.         if (array_key_exists('icon'$data))            $cat->setIcon($data['icon']);
  188.         if (array_key_exists('color'$data))           $cat->setColor($data['color']);
  189.         if (array_key_exists('seo_title'$data))       $cat->setSeoTitle($data['seo_title']);
  190.         if (array_key_exists('seo_description'$data)) $cat->setSeoDescription($data['seo_description']);
  191.         if (isset($data['position']))                   $cat->setPosition((int) $data['position']);
  192.         if (isset($data['enabled']))                    $cat->setEnabled((bool) $data['enabled']);
  193.         if (isset($data['code'])) {
  194.             $newCode trim($data['code']);
  195.             $existing $repo->findOneBy(['locale' => $cat->getLocale(), 'code' => $newCode]);
  196.             if ($existing && $existing->getId() !== $cat->getId()) {
  197.                 return new JsonResponse(['error' => 'Code déjà utilisé en cette locale'], Response::HTTP_BAD_REQUEST);
  198.             }
  199.             $cat->setCode($newCode);
  200.         }
  201.         if (isset($data['slug'])) {
  202.             $newSlug $this->slugify($data['slug']);
  203.             $existing $repo->findOneBy(['locale' => $cat->getLocale(), 'slug' => $newSlug]);
  204.             if ($existing && $existing->getId() !== $cat->getId()) {
  205.                 return new JsonResponse(['error' => 'Slug déjà utilisé en cette locale'], Response::HTTP_BAD_REQUEST);
  206.             }
  207.             $cat->setSlug($newSlug);
  208.         }
  209.         if (array_key_exists('parent_code'$data)) {
  210.             $pc $data['parent_code'];
  211.             if ($pc === null || $pc === '') {
  212.                 $cat->setParent(null);
  213.             } else {
  214.                 $parent $repo->findOneBy(['locale' => $cat->getLocale(), 'code' => trim($pc)]);
  215.                 if (!$parent) {
  216.                     return new JsonResponse(['error' => "Parent introuvable : {$pc}"], Response::HTTP_BAD_REQUEST);
  217.                 }
  218.                 if ($parent->getId() === $cat->getId()) {
  219.                     return new JsonResponse(['error' => 'Une catégorie ne peut pas être son propre parent'], Response::HTTP_BAD_REQUEST);
  220.                 }
  221.                 $cat->setParent($parent);
  222.             }
  223.         }
  224.         $cat->setUpdatedAt(new \DateTime('now'));
  225.         $this->em->persist($cat);
  226.         $this->em->flush();
  227.         return new JsonResponse(['success' => true'category' => $this->serialize($cat)]);
  228.     }
  229.     /**
  230.      * Suppression (admin).
  231.      * DELETE /missions/categories/{id}
  232.      */
  233.     public function delete(int $id): JsonResponse
  234.     {
  235.         if (!$this->isAdmin()) {
  236.             return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  237.         }
  238.         $cat $this->em->getRepository(MissionsCategories::class)->find($id);
  239.         if (!$cat) {
  240.             return new JsonResponse(['error' => 'Catégorie introuvable'], Response::HTTP_NOT_FOUND);
  241.         }
  242.         $this->em->remove($cat);
  243.         $this->em->flush();
  244.         return new JsonResponse(['success' => true]);
  245.     }
  246.     /**
  247.      * Export JSON (admin).
  248.      * GET /missions/categories/export
  249.      */
  250.     public function export(): JsonResponse
  251.     {
  252.         if (!$this->isAdmin()) {
  253.             return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  254.         }
  255.         $rows $this->em->createQueryBuilder()
  256.             ->select('c')
  257.             ->from(MissionsCategories::class, 'c')
  258.             ->leftJoin('c.parent''p')->addSelect('p')
  259.             ->orderBy('c.locale''ASC')
  260.             ->addOrderBy('c.position''ASC')
  261.             ->getQuery()->getResult();
  262.         $array = [];
  263.         foreach ($rows as $c) {
  264.             $array[] = [
  265.                 'locale'          => $c->getLocale(),
  266.                 'code'            => $c->getCode(),
  267.                 'slug'            => $c->getSlug(),
  268.                 'name'            => $c->getName(),
  269.                 'description'     => $c->getDescription(),
  270.                 'icon'            => $c->getIcon(),
  271.                 'color'           => $c->getColor(),
  272.                 'seo_title'       => $c->getSeoTitle(),
  273.                 'seo_description' => $c->getSeoDescription(),
  274.                 'position'        => $c->getPosition(),
  275.                 'enabled'         => $c->isEnabled(),
  276.                 'parent_code'     => $c->getParent() ? $c->getParent()->getCode() : null,
  277.             ];
  278.         }
  279.         return new JsonResponse([
  280.             'exported_at' => (new \DateTime('now'))->format('c'),
  281.             'count'       => count($array),
  282.             'categories'  => $array,
  283.         ]);
  284.     }
  285.     /**
  286.      * Import JSON (admin) — délégué au service dédié.
  287.      * POST /missions/categories/import
  288.      */
  289.     public function import(Request $request\App\Services\WhileProject\CategoriesImportExportService $importer): JsonResponse
  290.     {
  291.         if (!$this->isAdmin()) {
  292.             return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  293.         }
  294.         $result $importer->importFromJson($request->getContent());
  295.         return new JsonResponse(['success' => true] + $result);
  296.     }
  297.     // ─────────────────── Helpers ───────────────────
  298.     private function isAdmin(): bool
  299.     {
  300.         $user $this->getUser();
  301.         return $user && in_array('ROLE_ADMIN'$user->getRoles(), true);
  302.     }
  303.     private function slugify(string $input): string
  304.     {
  305.         $slug transliterator_transliterate('Any-Latin; Latin-ASCII; Lower()'$input) ?: strtolower($input);
  306.         $slug preg_replace('/[^a-z0-9]+/''-'$slug);
  307.         $slug trim($slug'-');
  308.         return mb_substr($slug ?: 'categorie'0140);
  309.     }
  310.     /**
  311.      * Sérialisation principale — renvoie TOUS les champs nécessaires
  312.      * au front (locale, code, parent_code) pour reconstruire l'arbre.
  313.      */
  314.     private function serialize(MissionsCategories $c): array
  315.     {
  316.         return [
  317.             'id'              => $c->getId(),
  318.             'locale'          => $c->getLocale(),
  319.             'code'            => $c->getCode(),
  320.             'slug'            => $c->getSlug(),
  321.             'name'            => $c->getName(),
  322.             'description'     => $c->getDescription(),
  323.             'icon'            => $c->getIcon(),
  324.             'color'           => $c->getColor(),
  325.             'seo_title'       => $c->getSeoTitle(),
  326.             'seo_description' => $c->getSeoDescription(),
  327.             'position'        => $c->getPosition(),
  328.             'enabled'         => $c->isEnabled(),
  329.             'parent_code'     => $c->getParent() ? $c->getParent()->getCode() : null,
  330.             'parent_id'       => $c->getParent() ? $c->getParent()->getId() : null,
  331.             'created_at'      => $c->getCreatedAt() ? $c->getCreatedAt()->format(\DateTime::ATOM) : null,
  332.             'updated_at'      => $c->getUpdatedAt() ? $c->getUpdatedAt()->format(\DateTime::ATOM) : null,
  333.         ];
  334.     }
  335. }