diff --git a/composer.json b/composer.json
index 6f6f67223f678d3d5f18ecc662db66059653fa7b..820066e93838030f885a12026a0faf8c4136a68a 100644
--- a/composer.json
+++ b/composer.json
@@ -33,7 +33,7 @@
         "symfony/intl": "5.4.*",
         "symfony/mailer": "5.4.*",
         "symfony/mime": "5.4.*",
-        "symfony/monolog-bundle": "^3.0",
+        "symfony/monolog-bundle": "^3.8",
         "symfony/notifier": "5.4.*",
         "symfony/process": "5.4.*",
         "symfony/property-access": "5.4.*",
diff --git a/src/Controller/ApiGraphyListController.php b/src/Controller/ApiGraphyListController.php
index a3b37ad7dd129bfa036da870971f5bb4efe998f2..e0f0bf40dd651fd2a1a9a198c1165e72ad3c640c 100644
--- a/src/Controller/ApiGraphyListController.php
+++ b/src/Controller/ApiGraphyListController.php
@@ -270,9 +270,10 @@ class ApiGraphyListController extends ApiBaseController
      */
     public function delete(GraphyList $graphyList)
     {
+        $response = $this->createJsonResponse(200, ['success' => sprintf("Liste %s (id: %s) supprimée", $graphyList->getName(), $graphyList->getId())]);
         $this->doctrine->getManager()->remove($graphyList);
         $this->doctrine->getManager()->flush();
 
-        return $this->createJsonResponse(200, ['success' => sprintf("Liste %s (id: %s) supprimée", $graphyList->getName(), $graphyList->getId())]);
+        return $response;
     }
 }
diff --git a/src/Controller/ApiLabelController.php b/src/Controller/ApiLabelController.php
index a6f9e3bf9a26b044613503f0f4cc91901111e28f..6c628c50d9fd812daad813ea982ae0cd5daecc12 100644
--- a/src/Controller/ApiLabelController.php
+++ b/src/Controller/ApiLabelController.php
@@ -250,10 +250,11 @@ class ApiLabelController extends ApiBaseController
      */
     public function delete(Label $label)
     {
+        $response = $this->createJsonResponse(200, ['success' => sprintf("Label %s (id: %s) supprimé", $label->getName(), $label->getId())]);
         $this->doctrine->getManager()->remove($label);
         $this->doctrine->getManager()->flush();
 
-        return $this->createJsonResponse(200, ['success' => sprintf("Label %s (id: %s) supprimé", $label->getName(), $label->getId())]);
+        return $response;
     }
 
     /**
diff --git a/src/Controller/ApiMetaInfoController.php b/src/Controller/ApiMetaInfoController.php
new file mode 100644
index 0000000000000000000000000000000000000000..8fa4c119a80fe53f74819f573e62d62b9495578c
--- /dev/null
+++ b/src/Controller/ApiMetaInfoController.php
@@ -0,0 +1,268 @@
+<?php
+
+namespace App\Controller;
+
+use App\Entity\Entry;
+use App\Entity\Graphy;
+use App\Entity\Headword;
+use App\Entity\Label;
+use App\Entity\Lexicon;
+use App\Entity\MetaInfo;
+use App\Entity\MetaInfoTemplate;
+use App\Entity\User;
+use App\Manager\WiktionaryManager;
+use App\Repository\LabelRepository;
+use Doctrine\Persistence\ManagerRegistry;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Annotation\Route;
+use Nelmio\ApiDocBundle\Annotation\Security;
+use OpenApi\Annotations as OA;
+use Nelmio\ApiDocBundle\Annotation\Model;
+use Symfony\Component\Serializer\SerializerInterface;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
+
+/**
+ * @Route("/api/meta-info")
+ */
+class ApiMetaInfoController extends ApiBaseController
+{
+    /**
+     *
+     * Crée un modèle de méta-information.
+     *
+     * @Route("/create-template", name="api_meta_info_template_create", methods={"POST"})
+     *
+     * @OA\Response(response=200, description="success", @OA\JsonContent(type="string"))
+     * @OA\Response(response=401, description="error", @OA\JsonContent(type="string"))
+     * @OA\Response(response=403, description="error", @OA\JsonContent(type="string"))
+     * @OA\Response(response=500, description="error", @OA\JsonContent(type="string"))
+     * @OA\RequestBody(
+     *     required=true,
+     *     @OA\JsonContent(
+     *       required={"description", "category"},
+     *       @OA\Property(property="description", type="string", example="Nombre de fois où a été rencontré dans MW"),
+     *       @OA\Property(property="category", type="string", example="integer OR string OR boolean"),
+     *       @OA\Property(property="default", type="string", example="10 OR true OR false OR string value"),
+     *       @OA\Property(property="choices", type="array", description="Optionnel. Tableau des valeurs possibles quand category = string",
+     *             example={"connu", "non connu"},
+     *             @OA\Items(
+     *                 type="string"
+     *             )
+     *         )
+     *     )
+     * )
+     * @OA\Tag(name="MetaInfos")
+     * @Security(name="OAuth2")
+     */
+    public function createMetaInfoTemplate(Request $request): Response
+    {
+        $data = json_decode($request->getContent(), true);
+        if (!$data) {
+            return $this->createJsonResponse(401, ['error' => sprintf("Json non valide")]);
+        }
+        if ($missingFields = $this->getMissingFields($data, ['description', 'category'])) {
+            return $this->createJsonResponse(401, ['error' => sprintf("Veuillez fournir une valeur pour: %s", implode(', ', $missingFields))]);
+        }
+        if (!in_array($data['category'], MetaInfo::META_INFO_LIST_CATEGORIES)) {
+            return $this->createJsonResponse(401, ['error' => sprintf("La catégorie de méta-information %s n'existe pas", $data['category'])]);
+        }
+
+        $metaInfoTemplate = new MetaInfoTemplate($data['category']);
+        $metaInfoTemplate->setCreatedBy($this->getUser());
+        $metaInfoTemplate->setDescription($data['description']);
+        if ($data['default'] ?? null) {
+            $metaInfoTemplate->setDefault($data['default']);
+        }
+        if ($data['choices'] ?? null && $data['category'] === MetaInfo::META_INFO_CATEGORY_STRING) {
+            if (is_array($data['choices'])) {
+                $metaInfoTemplate->setComplementStringChoices($data['choices']);
+            } else {
+                return $this->createJsonResponse(401, ['error' => sprintf("Le paramètre choices doit être un tableau")]);
+            }
+        }
+        $this->doctrine->getManager()->persist($metaInfoTemplate);
+        $this->doctrine->getManager()->flush();
+
+        $this->success[] = sprintf("Modèle de Méta information id %s créé.", $metaInfoTemplate->getId());
+
+        return $this->createJsonResponse(200);
+    }
+
+    /**
+     * Supprime le metaInfoTemplate
+     *
+     * @Route("/delete-template/{id}", name="api_meta_info_template_delete", methods={"DELETE"})
+     *
+     * @OA\Response(
+     *     response=200,
+     *     description="Success"
+     * )
+     * @OA\Response(response=401, description="error", @OA\JsonContent(type="string"))
+     * @OA\Response(response=403, description="error", @OA\JsonContent(type="string"))
+     * @OA\Response(response=500, description="error", @OA\JsonContent(type="string"))
+     *
+     * @OA\Parameter(
+     *     name="id",
+     *     in="path",
+     *     description="id of the meta info template to be removed",
+     *     @OA\Schema(type="integer")
+     * )
+     * @OA\Tag(name="MetaInfos")
+     * @Security(name="OAuth2")
+     */
+    public function delete(MetaInfoTemplate $metaInfoTemplate)
+    {
+        $response = $this->createJsonResponse(200, ['success' => sprintf("Modèle de Méta information « %s » (id: %s) supprimé", $metaInfoTemplate->getDescription(), $metaInfoTemplate->getId())]);
+        $this->doctrine->getManager()->remove($metaInfoTemplate);
+        $this->doctrine->getManager()->flush();
+
+        return $response;
+    }
+
+    /**
+     *
+     * Si la graphie possède déjà une méta information de la même catégorie que le modèle template_id, on la met à jour sinon on crée une nouvelle méta-info.
+     * Si le paramètre complement n'est pas renseigné, on utilise la valeur par défaut spécifiée dans le modèle si elle existe
+     * Si le modèle est de catégorie "string" on vérifie que la valeur de complement est comprise dans la liste de choix si celle-ci existe
+     *
+     * @Route("/create-or-update", name="api_meta_info_create_or_update", methods={"POST"})
+     *
+     * @OA\Response(response=200, description="success", @OA\JsonContent(type="string"))
+     * @OA\Response(response=401, description="error", @OA\JsonContent(type="string"))
+     * @OA\Response(response=403, description="error", @OA\JsonContent(type="string"))
+     * @OA\Response(response=500, description="error", @OA\JsonContent(type="string"))
+     * @OA\RequestBody(
+     *     required=true,
+     *     @OA\JsonContent(
+     *         required={"template_id", "graphy_id"},
+     *         @OA\Property(property="template_id", type="integer", example="1"),
+     *         @OA\Property(property="graphy_id", type="string", example="2"),
+     *         @OA\Property(property="complement", type="string", example="10 OR true OR false OR string value")
+     *     )
+     * )
+     * @OA\Tag(name="MetaInfos")
+     * @Security(name="OAuth2")
+     */
+    public function createMetaInfo(Request $request): Response
+    {
+        $data = json_decode($request->getContent(), true);
+        if (!$data) {
+            return $this->createJsonResponse(401, ['error' => sprintf("Json non valide")]);
+        }
+        if ($missingFields = $this->getMissingFields($data, ['template_id', 'graphy_id'])) {
+            return $this->createJsonResponse(401, ['error' => sprintf("Veuillez fournir une valeur pour: %s", implode(', ', $missingFields))]);
+        }
+        $metaInfoTemplate = $this->doctrine->getRepository(MetaInfoTemplate::class)->find($data['template_id']);
+        if (!$metaInfoTemplate instanceof MetaInfoTemplate) {
+            return $this->createJsonResponse(401, ['error' => sprintf("Pas de modèle de méta information trouvé pour cette id")]);
+        }
+        $graphy = $this->doctrine->getRepository(Graphy::class)->find($data['graphy_id']);
+        if (!$graphy) {
+            return $this->createJsonResponse(401, ['error' => sprintf("Pas de graphie trouvée pour cette id")]);
+        }
+
+        $metaInfo =$this->doctrine->getRepository(MetaInfo::class)->findOneBy([
+            'graphy' => $graphy,
+            'metaInfoTemplate' => $metaInfoTemplate,
+        ]);
+        if (!$metaInfo) {
+            $metaInfo = new MetaInfo($metaInfoTemplate);
+            $this->doctrine->getManager()->persist($metaInfo);
+            $metaInfo->setCreatedBy($this->getUser());
+            $metaInfo->setGraphy($graphy);
+            $this->success[] = sprintf("MetaInfo « %s » créée pour graphie « %s ».", $metaInfo->getDescription(), $graphy->getId());
+        } else {
+            $this->success[] = sprintf("MetaInfo « %s » mise à jour pour graphie « %s ».", $metaInfo->getDescription(), $graphy->getId());
+        }
+        if ($data['complement'] ?? null) {
+            if ($metaInfoTemplate->isValidComplement($data['complement'])) {
+                $metaInfo->setComplement($data['complement']);
+            } else {
+                return $this->createJsonResponse(401, ['error' => sprintf("La valeur du complément n'est pas valide")]);
+            }
+        } elseif ($metaInfoTemplate->getDefault()) {
+            $metaInfo->setComplement($metaInfoTemplate->getDefault());
+        }
+        $this->doctrine->getManager()->flush();
+
+        return $this->createJsonResponse(200);
+    }
+
+    /**
+     * Recherche de metaInfos par date ou user ou action ou origin
+     *
+     * @Route("/search", name="api_meta_info_search", methods={"POST"})
+     *
+     * @OA\Response(response=200, description="success", @OA\JsonContent(type="string"))
+     * @OA\Response(response=401, description="error", @OA\JsonContent(type="string"))
+     * @OA\Response(response=403, description="error", @OA\JsonContent(type="string"))
+     * @OA\Response(response=500, description="error", @OA\JsonContent(type="string"))
+     * @OA\RequestBody(
+     *     required=true,
+     *     @OA\JsonContent(
+     *         @OA\Property(property="date", type="date"),
+     *         @OA\Property(property="date_client", type="date"),
+     *         @OA\Property(property="user_id", type="integer", example="4"),
+     *         @OA\Property(property="origin", type="string", example="Prisms"),
+     *         @OA\Property(property="action", type="string", example="action"),
+     *     )
+     * )
+     * @OA\Tag(name="MetaInfos")
+     * @Security(name="OAuth2")
+     */
+    public function search(Request $request, SerializerInterface $serializer): Response
+    {
+        $data = json_decode($request->getContent(), true);
+        if (!$data) {
+            return $this->createJsonResponse(401, ['error' => sprintf("Json non valide")]);
+        }
+        if (!isset($data['date']) && !isset($data['date_client']) && !isset($data['user_id']) && !isset($data['origin']) && !isset($data['action'])) {
+            return $this->createJsonResponse(401, ['error' => sprintf("Veuillez spécifier au moins un paramètre de recherche parmi: date, user_id, action, origin")]);
+        }
+
+        $filter = [];
+        if ($data['date'] ?? null) {
+            $date = date_create_from_format('Y-m-d', $data['date']);
+            if ($date === false) {
+                return $this->createJsonResponse(401, ['error' => sprintf("Veuillez fournir une date au format aaaa-mm-jj pour le paramètre «date»")]);
+            } else {
+                $filter['date'] = $date;
+            }
+        }
+        if ($data['date_client'] ?? null) {
+            $dateClient = date_create_from_format('Y-m-d', $data['date_client']);
+            if ($dateClient === false) {
+                return $this->createJsonResponse(401, ['error' => sprintf("Veuillez fournir une date au format aaaa-mm-jj pour le paramètre «date client»")]);
+            } else {
+                $filter['date_client'] = $dateClient;
+            }
+        }
+        if ($data["user_id"] ?? null) {
+            $user = $this->doctrine->getRepository(User::class)->find($data['user_id']);
+            if (!$user) {
+                return $this->createJsonResponse(401, ['error' => sprintf("Pas d'utilisateur trouvé pour l'id %s", $data['user_id'])]);
+            } else {
+                $filter['user'] = $user;
+            }
+        }
+        if ($data['origin'] ?? null) {
+            $filter['origin'] = $data['origin'];
+        }
+        if ($data['action'] ?? null) {
+            $filter['action'] = $data['action'];
+        }
+
+        $metaInfos = $this->doctrine->getRepository(MetaInfo::class)->search($filter);
+
+        $content = $serializer->serialize($metaInfos, 'json', [
+            'groups' => ["metaInfo:read"],
+            'datetime_format' => 'Y-m-d h:i:s.u'
+        ]);
+
+        return new JsonResponse($content, 200, [], true);
+    }
+}
diff --git a/src/Entity/Graphy.php b/src/Entity/Graphy.php
index c3e7ff4f68bfd1e345f2ab3eaa9523d3fb1590ac..e967b1efcabed6a6f6da5348ef7ac7d2eea03ee3 100644
--- a/src/Entity/Graphy.php
+++ b/src/Entity/Graphy.php
@@ -57,7 +57,7 @@ class Graphy
     private $graphyLists;
 
     /**
-     * @ORM\OneToMany(targetEntity=MetaInfo::class, mappedBy="graphies")
+     * @ORM\OneToMany(targetEntity=MetaInfo::class, mappedBy="graphy")
      */
     private $metaInfos;
 
@@ -65,6 +65,7 @@ class Graphy
     {
         $this->headwords = new ArrayCollection();
         $this->graphyLists = new ArrayCollection();
+        $this->metaInfos = new ArrayCollection();
     }
 
     public function __toString()
@@ -187,4 +188,34 @@ class Graphy
 
         return $this;
     }
+
+    /**
+     * @return Collection<int, MetaInfo>
+     */
+    public function getMetaInfos(): Collection
+    {
+        return $this->metaInfos;
+    }
+
+    public function addMetaInfo(MetaInfo $metaInfo): self
+    {
+        if (!$this->metaInfos->contains($metaInfo)) {
+            $this->metaInfos[] = $metaInfo;
+            $metaInfo->setGraphy($this);
+        }
+
+        return $this;
+    }
+
+    public function removeMetaInfo(MetaInfo $metaInfo): self
+    {
+        if ($this->metaInfos->removeElement($metaInfo)) {
+            // set the owning side to null (unless already changed)
+            if ($metaInfo->getGraphy() === $this) {
+                $metaInfo->setGraphy(null);
+            }
+        }
+
+        return $this;
+    }
 }
diff --git a/src/Entity/MetaInfo.php b/src/Entity/MetaInfo.php
index 76fcb4712f4f539be53e63b19a26ba81f3d6aaa4..f4154bf2a2622ac400a099bad941b9ba25514d7d 100644
--- a/src/Entity/MetaInfo.php
+++ b/src/Entity/MetaInfo.php
@@ -15,8 +15,8 @@ use OpenApi\Annotations as OA;
  */
 class MetaInfo
 {
-    const META_INFO_CATEGORY_BOOLEAN = 'bool';
-    const META_INFO_CATEGORY_INTEGER = 'int';
+    const META_INFO_CATEGORY_BOOLEAN = 'boolean';
+    const META_INFO_CATEGORY_INTEGER = 'integer';
     const META_INFO_CATEGORY_STRING  = 'string';
 
     const META_INFO_LIST_CATEGORIES = [
@@ -49,15 +49,71 @@ class MetaInfo
      */
     private $createdBy;
 
+    /**
+     * @Groups({"metaInfo:read", "graphy:write"})
+     * @ORM\Column(type="string", length=255)
+     */
+    private $description;
+
+    /**
+     * @ORM\Column(type="string", length=80)
+     * @Groups({"metaInfo:read"})
+     */
+    private $category;
+
+    /**
+     * @ORM\Column(type="string", length=255, nullable=true)
+     * @Groups({"metaInfo:read"})
+     */
+    private $complementString;
+
+    /**
+     * @ORM\Column(type="integer", nullable=true)
+     * @Groups({"metaInfo:read"})
+     */
+    private $complementInteger;
+
+    /**
+     * @ORM\Column(type="boolean", nullable=true)
+     * @Groups({"metaInfo:read"})
+     */
+    private $complementBoolean;
+
+    /**
+     * @ORM\ManyToOne(targetEntity=MetaInfoTemplate::class, inversedBy="metaInfos")
+     */
+    private $metaInfoTemplate;
+
     /**
      * @ORM\ManyToOne(targetEntity=Graphy::class, inversedBy="metaInfos")
      * @Groups({"metaInfo:read"})
      */
-    private $graphies;
+    private $graphy;
+
+    public function setComplement($complement)
+    {
+        switch ($this->getCategory()) {
+            case self::META_INFO_CATEGORY_STRING: return $this->setComplementString($complement);
+            case self::META_INFO_CATEGORY_INTEGER: return $this->setComplementInteger($complement);
+            case self::META_INFO_CATEGORY_BOOLEAN: return $this->setComplementBoolean($complement);
+            default:
+                throw new \Exception(sprintf("Catégorie de méta information inconnue «%s» pour méta-info %s", $this->getCategory(), $this->getId()));
+        }
+    }
+
+    public function getComplement()
+    {
+        switch ($this->getCategory()) {
+            case self::META_INFO_CATEGORY_STRING: return $this->getComplementString();
+            case self::META_INFO_CATEGORY_INTEGER: return $this->getComplementInteger();
+            case self::META_INFO_CATEGORY_BOOLEAN: return $this->getComplementBoolean();
+            default: return 'Complement not found';
+        }
+    }
 
     public function __toString()
     {
-        return $this->getLabel();
+        return $this->getDescription() . ' : ' . $this->getComplement();
     }
 
     /**
@@ -72,9 +128,16 @@ class MetaInfo
         }
     }
 
-    public function __construct()
+    public function __construct(MetaInfoTemplate $metaInfoTemplate)
     {
         $this->createdAt = new \DateTimeImmutable();
+
+        $this->setMetaInfoTemplate($metaInfoTemplate);
+        $this->setCategory($metaInfoTemplate->getCategory());
+        $this->setDescription($metaInfoTemplate->getDescription());
+        $this->setComplementBoolean($metaInfoTemplate->getComplementBooleanDefaultValue());
+        $this->setComplementInteger($metaInfoTemplate->getComplementIntegerDefaultValue());
+        $this->setComplementString($metaInfoTemplate->getComplementStringDefaultValue());
     }
 
     public function getId(): ?int
@@ -94,38 +157,74 @@ class MetaInfo
         return $this;
     }
 
-    public function getOrigin(): ?string
+    public function getUpdatedAt(): ?\DateTimeImmutable
+    {
+        return $this->updatedAt;
+    }
+
+    public function setUpdatedAt(?\DateTimeImmutable $updatedAt): self
+    {
+        $this->updatedAt = $updatedAt;
+
+        return $this;
+    }
+
+    public function getDescription(): ?string
+    {
+        return $this->description;
+    }
+
+    public function setDescription(string $description): self
+    {
+        $this->description = $description;
+
+        return $this;
+    }
+
+    public function getCategory(): ?string
     {
-        return $this->origin;
+        return $this->category;
     }
 
-    public function setOrigin(?string $origin): self
+    public function setCategory(string $category): self
     {
-        $this->origin = $origin;
+        $this->category = $category;
 
         return $this;
     }
 
-    public function getAction(): ?string
+    public function getComplementString(): ?string
     {
-        return $this->action;
+        return $this->complementString;
     }
 
-    public function setAction(?string $action): self
+    public function setComplementString(?string $complementString): self
     {
-        $this->action = $action;
+        $this->complementString = $complementString;
 
         return $this;
     }
 
-    public function getAdditionalInfo(): ?array
+    public function getComplementInteger(): ?int
     {
-        return $this->additionalInfo;
+        return $this->complementInteger;
     }
 
-    public function setAdditionalInfo(?array $additionalInfo): self
+    public function setComplementInteger(?int $complementInteger): self
     {
-        $this->additionalInfo = $additionalInfo;
+        $this->complementInteger = $complementInteger;
+
+        return $this;
+    }
+
+    public function getComplementBoolean(): ?bool
+    {
+        return $this->complementBoolean;
+    }
+
+    public function setComplementBoolean(?bool $complementBoolean): self
+    {
+        $this->complementBoolean = $complementBoolean;
 
         return $this;
     }
@@ -141,4 +240,33 @@ class MetaInfo
 
         return $this;
     }
+
+    public function getMetaInfoTemplate(): ?MetaInfoTemplate
+    {
+        return $this->metaInfoTemplate;
+    }
+
+    public function setMetaInfoTemplate(?MetaInfoTemplate $metaInfoTemplate): self
+    {
+        $this->metaInfoTemplate = $metaInfoTemplate;
+
+        return $this;
+    }
+
+    public function isComplementBoolean(): ?bool
+    {
+        return $this->complementBoolean;
+    }
+
+    public function getGraphy(): ?Graphy
+    {
+        return $this->graphy;
+    }
+
+    public function setGraphy(?Graphy $graphy): self
+    {
+        $this->graphy = $graphy;
+
+        return $this;
+    }
 }
diff --git a/src/Entity/MetaInfoTemplate.php b/src/Entity/MetaInfoTemplate.php
new file mode 100644
index 0000000000000000000000000000000000000000..93be2e79aa4a4bd5fdb2a1d4a3cd8d6c1aa58c87
--- /dev/null
+++ b/src/Entity/MetaInfoTemplate.php
@@ -0,0 +1,292 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\MetaInfoTemplateRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+use OpenApi\Annotations as OA;
+
+/**
+ * @ORM\Entity(repositoryClass=MetaInfoTemplateRepository::class)
+ * @ORM\HasLifecycleCallbacks
+ */
+class MetaInfoTemplate
+{
+    /**
+     * @ORM\Id
+     * @ORM\GeneratedValue
+     * @ORM\Column(type="integer")
+     */
+    private $id;
+
+    /**
+     * @Groups({"metaInfo:read"})
+     * @ORM\Column(type="datetime_immutable")
+     */
+    private $createdAt;
+
+    /**
+     * @ORM\Column(type="datetime_immutable", nullable=true)
+     */
+    private $updatedAt;
+
+    /**
+     * @ORM\ManyToOne(targetEntity=User::class, inversedBy="metaInfos")
+     * @Groups({"metaInfo:read"})
+     */
+    private $createdBy;
+
+    /**
+     * @Groups({"metaInfo:read", "graphy:write"})
+     * @ORM\Column(type="string", length=255)
+     */
+    private $description;
+
+    /**
+     * @ORM\Column(type="string", length=80)
+     * @Groups({"metaInfo:read"})
+     */
+    private $category;
+
+    /**
+     * @ORM\Column(type="string", length=255, nullable=true)
+     * @Groups({"metaInfo:read"})
+     */
+    private $complementStringDefaultValue;
+
+    /**
+     * @ORM\Column(type="array", nullable=true)
+     * @Groups({"metaInfo:read"})
+     */
+    private $complementStringChoices;
+
+    /**
+     * @ORM\Column(type="integer", nullable=true)
+     * @Groups({"metaInfo:read"})
+     */
+    private $complementIntegerDefaultValue;
+
+    /**
+     * @ORM\Column(type="boolean", nullable=true)
+     * @Groups({"metaInfo:read"})
+     */
+    private $complementBooleanDefaultValue;
+
+    /**
+     * @ORM\OneToMany(targetEntity=MetaInfo::class, mappedBy="metaInfoTemplate")
+     */
+    private $metaInfos;
+
+    public function __toString()
+    {
+        return $this->getDescription();
+    }
+
+    /**
+     * @ORM\PrePersist
+     * @ORM\PreUpdate
+     */
+    public function updatedTimestamps(): void
+    {
+        $this->setUpdatedAt(new \DateTimeImmutable('now'));
+        if ($this->getCreatedAt() === null) {
+            $this->setCreatedAt(new \DateTimeImmutable('now'));
+        }
+    }
+
+    public function __construct($category)
+    {
+        if (!in_array($category, MetaInfo::META_INFO_LIST_CATEGORIES)) {
+            throw new \Exception("Invalid category for Meta Info Template");
+        }
+        $this->setCategory($category);
+        $this->createdAt = new \DateTimeImmutable();
+        $this->metaInfos = new ArrayCollection();
+    }
+
+    public function hasRestrictedChoices()
+    {
+        return $this->getCategory() === MetaInfo::META_INFO_CATEGORY_STRING && $this->getComplementStringChoices();
+    }
+
+    public function isValidComplement($complement)
+    {
+        switch ($this->getCategory()) {
+            case MetaInfo::META_INFO_CATEGORY_STRING:
+                return !$this->hasRestrictedChoices() || in_array($complement, $this->getComplementStringChoices());
+            case MetaInfo::META_INFO_CATEGORY_INTEGER: return ctype_digit((string) $complement);
+            case MetaInfo::META_INFO_CATEGORY_BOOLEAN: return in_array($complement, [true, false, 0, 1, "0", "1", "true", "false"], true);
+            default: throw new \Exception("Unknown meta info template category");
+        }
+
+    }
+
+    public function setDefault($defaultValue)
+    {
+        switch ($this->getCategory()) {
+            case MetaInfo::META_INFO_CATEGORY_STRING: $this->setComplementStringDefaultValue($defaultValue); break;
+            case MetaInfo::META_INFO_CATEGORY_INTEGER: $this->setComplementIntegerDefaultValue($defaultValue); break;
+            case MetaInfo::META_INFO_CATEGORY_BOOLEAN: $this->setComplementBooleanDefaultValue($defaultValue); break;
+            default:
+                throw new \Exception("Unknown meta info template category");
+        }
+    }
+
+    public function getDefault()
+    {
+        switch ($this->getCategory()) {
+            case MetaInfo::META_INFO_CATEGORY_STRING: $this->getComplementStringDefaultValue(); break;
+            case MetaInfo::META_INFO_CATEGORY_INTEGER: $this->getComplementIntegerDefaultValue(); break;
+            case MetaInfo::META_INFO_CATEGORY_BOOLEAN: $this->getComplementBooleanDefaultValue(); break;
+            default:
+                throw new \Exception("Unknown meta info template category");
+        }
+    }
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getCreatedAt(): ?\DateTimeImmutable
+    {
+        return $this->createdAt;
+    }
+
+    public function setCreatedAt(\DateTimeImmutable $createdAt): self
+    {
+        $this->createdAt = $createdAt;
+
+        return $this;
+    }
+
+    public function getUpdatedAt(): ?\DateTimeImmutable
+    {
+        return $this->updatedAt;
+    }
+
+    public function setUpdatedAt(?\DateTimeImmutable $updatedAt): self
+    {
+        $this->updatedAt = $updatedAt;
+
+        return $this;
+    }
+
+    public function getDescription(): ?string
+    {
+        return $this->description;
+    }
+
+    public function setDescription(string $description): self
+    {
+        $this->description = $description;
+
+        return $this;
+    }
+
+    public function getCategory(): ?string
+    {
+        return $this->category;
+    }
+
+    public function setCategory(string $category): self
+    {
+        $this->category = $category;
+
+        return $this;
+    }
+
+    public function getComplementStringDefaultValue(): ?string
+    {
+        return $this->complementStringDefaultValue;
+    }
+
+    public function setComplementStringDefaultValue(?string $complementStringDefaultValue): self
+    {
+        $this->complementStringDefaultValue = $complementStringDefaultValue;
+
+        return $this;
+    }
+
+    public function getComplementStringChoices(): ?array
+    {
+        return $this->complementStringChoices;
+    }
+
+    public function setComplementStringChoices(?array $complementStringChoices): self
+    {
+        $this->complementStringChoices = $complementStringChoices;
+
+        return $this;
+    }
+
+    public function getComplementIntegerDefaultValue(): ?int
+    {
+        return $this->complementIntegerDefaultValue;
+    }
+
+    public function setComplementIntegerDefaultValue(?int $complementIntegerDefaultValue): self
+    {
+        $this->complementIntegerDefaultValue = $complementIntegerDefaultValue;
+
+        return $this;
+    }
+
+    public function getComplementBooleanDefaultValue(): ?bool
+    {
+        return $this->complementBooleanDefaultValue;
+    }
+
+    public function setComplementBooleanDefaultValue(?bool $complementBooleanDefaultValue): self
+    {
+        $this->complementBooleanDefaultValue = $complementBooleanDefaultValue;
+
+        return $this;
+    }
+
+    public function getCreatedBy(): ?User
+    {
+        return $this->createdBy;
+    }
+
+    public function setCreatedBy(?User $createdBy): self
+    {
+        $this->createdBy = $createdBy;
+
+        return $this;
+    }
+
+    /**
+     * @return Collection<int, MetaInfo>
+     */
+    public function getMetaInfos(): Collection
+    {
+        return $this->metaInfos;
+    }
+
+    public function addMetaInfo(MetaInfo $metaInfo): self
+    {
+        if (!$this->metaInfos->contains($metaInfo)) {
+            $this->metaInfos[] = $metaInfo;
+            $metaInfo->setMetaInfoTemplate($this);
+        }
+
+        return $this;
+    }
+
+    public function removeMetaInfo(MetaInfo $metaInfo): self
+    {
+        if ($this->metaInfos->removeElement($metaInfo)) {
+            // set the owning side to null (unless already changed)
+            if ($metaInfo->getMetaInfoTemplate() === $this) {
+                $metaInfo->setMetaInfoTemplate(null);
+            }
+        }
+
+        return $this;
+    }
+
+}
diff --git a/src/Repository/MetaInfoTemplateRepository.php b/src/Repository/MetaInfoTemplateRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..9fc0e8a82cbbfb5594e61447ef258a05f64b8e33
--- /dev/null
+++ b/src/Repository/MetaInfoTemplateRepository.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\MetaInfoTemplate;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<MetaInfoTemplate>
+ *
+ * @method MetaInfoTemplate|null find($id, $lockMode = null, $lockVersion = null)
+ * @method MetaInfoTemplate|null findOneBy(array $criteria, array $orderBy = null)
+ * @method MetaInfoTemplate[]    findAll()
+ * @method MetaInfoTemplate[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class MetaInfoTemplateRepository extends ServiceEntityRepository
+{
+    public function __construct(ManagerRegistry $registry)
+    {
+        parent::__construct($registry, MetaInfoTemplate::class);
+    }
+
+    public function search($filter)
+    {
+        $qb = $this->createQueryBuilder('t');
+
+        if ($filter['date'] ?? null) {
+            $qb->andWhere('DATE(t.createdAt) = DATE(:date)')
+                ->setParameter('date', $filter['date'])
+            ;
+        }
+
+        if ($filter['user'] ?? null) {
+            $qb->andWhere('t.createdBy = :user')
+                ->setParameter('user', $filter['user'])
+            ;
+        }
+
+        if ($filter['action'] ?? null) {
+            $qb->andWhere('t.action = :action')
+                ->setParameter('action', $filter['action'])
+            ;
+        }
+
+        if ($filter['origin'] ?? null) {
+            $qb->andWhere('t.origin = :origin')
+                ->setParameter('origin', $filter['origin'])
+            ;
+        }
+
+        return $qb->getQuery()->getResult();
+
+    }
+
+    public function add(MetaInfoTemplate $entity, bool $flush = false): void
+    {
+        $this->getEntityManager()->persist($entity);
+
+        if ($flush) {
+            $this->getEntityManager()->flush();
+        }
+    }
+
+    public function remove(MetaInfoTemplate $entity, bool $flush = false): void
+    {
+        $this->getEntityManager()->remove($entity);
+
+        if ($flush) {
+            $this->getEntityManager()->flush();
+        }
+    }
+
+//    /**
+//     * @return MetaInfoTemplate[] Returns an array of MetaInfoTemplate objects
+//     */
+//    public function findByExampleField($value): array
+//    {
+//        return $this->createQueryBuilder('e')
+//            ->andWhere('e.exampleField = :val')
+//            ->setParameter('val', $value)
+//            ->orderBy('e.id', 'ASC')
+//            ->setMaxResults(10)
+//            ->getQuery()
+//            ->getResult()
+//        ;
+//    }
+
+//    public function findOneBySomeField($value): ?MetaInfoTemplate
+//    {
+//        return $this->createQueryBuilder('e')
+//            ->andWhere('e.exampleField = :val')
+//            ->setParameter('val', $value)
+//            ->getQuery()
+//            ->getOneOrNullResult()
+//        ;
+//    }
+}