<?phpnamespace App\Entity\WhileProject;use Doctrine\Common\Collections\ArrayCollection;use Doctrine\Common\Collections\Collection;use Doctrine\ORM\Mapping as ORM;/** * Catégories de missions WhileProject. * * Modèle "1 row = 1 langue" : * - `locale` (fr|en|...) identifie la langue de la traduction * - `code` est l'identifiant stable cross-langue * (ex: "dev-web" existe en FR ET en EN, avec un name différent) * - `slug` est l'URL-friendly, peut différer par locale * * Hiérarchie : self-FK via `parent` / `children`. Une sous-catégorie DOIT * avoir la même locale que son parent (validé en service à l'import). * * @ORM\Entity(repositoryClass="App\Repository\WhileProject\MissionsCategoriesRepository") * @ORM\Table(name="whileproject_missions_categories", * uniqueConstraints={ * @ORM\UniqueConstraint(name="uk_missions_categories_locale_slug", columns={"locale","slug"}), * @ORM\UniqueConstraint(name="uk_missions_categories_locale_code", columns={"locale","code"}) * }, * indexes={ * @ORM\Index(name="idx_missions_categories_parent", columns={"parent_id"}), * @ORM\Index(name="idx_missions_categories_locale", columns={"locale"}), * @ORM\Index(name="idx_missions_categories_enabled", columns={"enabled"}) * } * ) */class MissionsCategories{ /** * @ORM\Id() * @ORM\GeneratedValue() * @ORM\Column(type="integer") */ private $id; /** * Langue de cette traduction. Ex: "fr", "en", "de", "es". * * @ORM\Column(type="string", length=5) */ private $locale = 'fr'; /** * Identifiant stable cross-langue (ex: "dev-web"). * Permet de relier la version FR et EN d'une même catégorie. * * @ORM\Column(type="string", length=80) */ private $code; /** * Slug URL-friendly (peut être différent du code, et différent par langue). * * @ORM\Column(type="string", length=140) */ private $slug; /** * @ORM\Column(type="string", length=120) */ private $name; /** * @ORM\Column(type="text", nullable=true) */ private $description; /** * @ORM\Column(type="string", length=80, nullable=true) */ private $icon; /** * @ORM\Column(type="string", length=20, nullable=true) */ private $color; /** * @ORM\Column(type="string", length=180, nullable=true) */ private $seoTitle; /** * @ORM\Column(type="string", length=250, nullable=true) */ private $seoDescription; /** * @ORM\Column(type="integer") */ private $position = 0; /** * @ORM\Column(type="boolean") */ private $enabled = true; /** * Catégorie parente (NULL = racine). * * @ORM\ManyToOne(targetEntity="App\Entity\WhileProject\MissionsCategories", inversedBy="children") * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true, onDelete="SET NULL") */ private $parent; /** * Sous-catégories. * * @ORM\OneToMany(targetEntity="App\Entity\WhileProject\MissionsCategories", mappedBy="parent") * @ORM\OrderBy({"position"="ASC", "name"="ASC"}) */ private $children; /** * @ORM\Column(type="datetime") */ private $createdAt; /** * @ORM\Column(type="datetime") */ private $updatedAt; public function __construct() { $this->children = new ArrayCollection(); $this->createdAt = new \DateTime('now'); $this->updatedAt = new \DateTime('now'); } // ----- Getters / setters -------------------------------------------------- public function getId(): ?int { return $this->id; } public function getLocale(): string { return $this->locale; } public function setLocale(string $v): self { $this->locale = strtolower($v); return $this; } public function getCode(): ?string { return $this->code; } public function setCode(string $v): self { $this->code = $v; return $this; } public function getSlug(): ?string { return $this->slug; } public function setSlug(string $v): self { $this->slug = $v; return $this; } public function getName(): ?string { return $this->name; } public function setName(string $v): self { $this->name = $v; return $this; } public function getDescription(): ?string { return $this->description; } public function setDescription(?string $v): self { $this->description = $v; return $this; } public function getIcon(): ?string { return $this->icon; } public function setIcon(?string $v): self { $this->icon = $v; return $this; } public function getColor(): ?string { return $this->color; } public function setColor(?string $v): self { $this->color = $v; return $this; } public function getSeoTitle(): ?string { return $this->seoTitle; } public function setSeoTitle(?string $v): self { $this->seoTitle = $v; return $this; } public function getSeoDescription(): ?string { return $this->seoDescription; } public function setSeoDescription(?string $v): self { $this->seoDescription = $v; return $this; } public function getPosition(): int { return $this->position; } public function setPosition(int $v): self { $this->position = $v; return $this; } public function isEnabled(): bool { return $this->enabled; } public function setEnabled(bool $v): self { $this->enabled = $v; return $this; } public function getParent(): ?self { return $this->parent; } public function setParent(?self $v): self { // Sécurité : un parent doit être dans la même locale if ($v !== null && $v->getLocale() !== $this->locale) { throw new \InvalidArgumentException(sprintf( 'Parent locale (%s) doit correspondre à child locale (%s)', $v->getLocale(), $this->locale )); } // Sécurité : pas d'auto-référence if ($v !== null && $v === $this) { throw new \InvalidArgumentException('Une catégorie ne peut pas être son propre parent'); } $this->parent = $v; return $this; } /** @return Collection<int, self> */ public function getChildren(): Collection { return $this->children; } public function addChild(self $child): self { if (!$this->children->contains($child)) { $this->children->add($child); $child->setParent($this); } return $this; } public function removeChild(self $child): self { if ($this->children->removeElement($child)) { if ($child->getParent() === $this) { $child->setParent(null); } } return $this; } public function getCreatedAt(): ?\DateTimeInterface { return $this->createdAt; } public function setCreatedAt(\DateTimeInterface $v): self { $this->createdAt = $v; return $this; } public function getUpdatedAt(): ?\DateTimeInterface { return $this->updatedAt; } public function setUpdatedAt(\DateTimeInterface $v): self { $this->updatedAt = $v; return $this; } // ----- Helpers ------------------------------------------------------------ public function isRoot(): bool { return $this->parent === null; } public function hasChildren(): bool { return !$this->children->isEmpty(); } /** Profondeur dans l'arbre (0 = racine). */ public function getDepth(): int { $depth = 0; $current = $this->parent; while ($current !== null) { $depth++; $current = $current->getParent(); if ($depth > 10) break; // garde-fou anti-boucle } return $depth; }}