J'essaie de créer un bundle pour gérer et associer des tags à différents type de contenus sur mon site. Pour cela au niveau de ma base j'ai la structure suivante :

Tag: id, name, slug
TagRelation: id, taggable_type, taggable_id, tag_id

Pour administrer ces tags je souhaite créer un simple champs texte où on entrera les tags sous forme de liste séparée par une virgule (plus tard du JavaScript améliorera l'interface). Et c'est là que les soucis commencent :)

Pour traiter ce champs je suis parti sur un champs personnalisé qui a comme parent le TextType et je lui applique un transformer qui va convertir la chaine contenant tags en tableau de TagBundle\Entityt\Tag (ça ça marche). Je délègue la partie DQL dégueulasse dans le repository pour bien organiser le code et j'injecte le Repository dans mon Transformer.

class TagTransformer implements DataTransformerInterface {

    private $repository;

    public function __construct(TagRepository $tagRepository) {
        $this->repository = $tagRepository;
    }

    public function transform($tags)  {
        if ($tags === null) return '';
        return implode(',', $tags->map(function (Tag $tag) {
            return $tag->getName();
        })->toArray());
    }

    public function reverseTransform($names) {
        return $this->repository->create(explode(',', $names));
    }
}

Pour que ce champs fonctionne je créer des getter / setter sur mon entité Post via un trait

trait Taggable {

    private $tags;

    public function getTags()
    {
        return $this->tags;
    }

    public function setTags($tags)
    {
        $this->tags = new ArrayCollection($tags);
    }

Maintenant vient la partie problématique : créer et persister les TagRelations lorsque le formulaire est soumis du coup j'ai 2 approches :

  • Souscrire à l'évènement preFlush et créer les relations (et détruire les relations qui ne sont plus utilisées. Le souci est que le UnitOfWork ne me retourne aucun changement sur la clef tags de l'entité Post et il n'y a apparemment aucune méthode qui me permettrait de dire au UnitOfWork de rajouter les tags au changeset.
  • Souscrire à l'évènement avant le handleRequest du formulaire et supprimer / créer les TagRelation à ce moment là. Le souci est alors que certaines requêtes SQL vont se faire même si le formulaire n'est pas valide :(
  • Fuk it ! je créer une méthode que je devrais appeller dans tous les controleurs : $this->getDoctrine()->getRepository('TagBundle::TagRelation')->persistRelationsFor($post)
Question ?

Comment indiquer à symfony la logiquer à adopter pour persister les relations TagsRelation ? Idéalement je souhaiterais éviter de rajouter du code dans mes controlleurs.

11 réponses


Salut !
Dans tes entités qui auront des tags tu ajoutera bien une collection non ?
Dans les annotations de l'attibut Taggable::$tags tu peux définir les actions en cascade à effectuer, comme persist, update, delete :

/** 
     * @var ArrayCollection
     *
     * @ORM\OneToMany(
     *     targetEntity="Tags",
     *     mappedBy="Taggable",
     *     cascade={"persist"}
     * )
     */
    private $tags;

@BastosWeb: Le souci c'est que je ne peux pas utiliser une relation de base de doctrine car ma relation est polymorphique :(

C'est l'ORM ici qui est contrainiant, ce que tu recherche est normalement ça : http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html -> Class Table Inheritance

Mais globalement, (c'est mon point de vue) c'est mieux d'éviter les relations polymorphiques, car même si ça reste des tags, ils n'appartiennent pas au même domaine (Article, Commentaire, whatever), répondent souvent à des contraintes différentes (amoncellement de if), peuvent avoir des context d'utilisation différents (SEO, Recherche) du coup faire une table par type tags c'est peut être pas plus mal et ça t'affranchit de toute cette compléxité alors que tu pourrais simplement avoir article_tags & commentaire_tags représenter par une superMappedClass et coté repository, faire un répo par type (ArticleTagRepository), (CommentaireTagRepository) qui implémente une interface TagRepository, et si tu en as le besoin, faire un AggregatedTagRepository répondant à l'interface TagRepository.

Sans rentrer dans les différents context d'utlisation d'un tag, je pense que tu bénéficera d'une approche moins magic & four tout du coup plus lisible et compréhensible. La polymorphic est aussi une approche qui n'est pas fausse, mais c'est velu, et dans le temps deviendra plus un casse tête qu'un avantage.

Autre point, la class tagManager ne devrait pas allez récup l'entity tag au travers du repository (Sinon ça serais plus Un transformerFetcher). Il faut lui passer ton entity "article" possédant déjà une collection d'object tags et non l'inverse. Ton transformer il sait représenter un object Tag en text et faire l'inverse, mais certainement pas aller interroger la base donné d'un élément possédant des tags pour les récupérer :)

Tout ceci n'est que mon point de vue, et ça ne s'applique pas forcément à ton cas d'utilisation, je connais pas ton context donc c'est juste des tips à prendre (ou pas).

@Grafikart ouep au temps pour moi je n'avais pas retenu la notion de polymorphique. Mais après réflexion, même si tu mets l'annotation dans le Trait Tagglable ça ne fonctionne pas ?
J'avais un peu le même genre de relation pour travailler avec des traductions, et l'attribut "mappedBy" pouvait être de différentes classes selon les cas de l'entité à traduire.
Du coup si tu mets une annotation comme ça sur le Trait à mon avis ça devrait passer (je n'ai pas de quoi tester sous la main donc je m'avance sûrement un peu trop peut être ^^)

trait Taggable {

    /**
     * @var ArrayCollection
     * @ORM\ManyToMany(targetEntity="Tag", inversedBy="taggables").
     * @ORM\JoinTable(name="tag_relation",
     *   joinColumns={
     *     @ORM\JoinColumn(name="taggable_id", referencedColumnName="id")
     *   },
     *   inverseJoinColumns={
     *     @ORM\JoinColumn(name="tag_id", referencedColumnName="id")
     *   }
     * )
     */
    private $tags;

    public function getTags()
    {
        return $this->tags;
    }

    public function setTags($tags)
    {
        $this->tags = new ArrayCollection($tags);
    }
}

Et du côté de tag :

class Tag
{
    /**
     * @var ArrayCollection
     * @ORM\ManyToMany(
     *     targetEntity="Taggable",
     *     mappedBy="tags"
     * )
     */
    protected $taggables;

}

@BastosWeb On s'approche mais du coup là tu ne définis pas la condition à appliquer sur taggable_type non ?

Ça correspondrait à quoi taggabletype du coup ?

Le nom du model associé par exemple

| Attachabletype | Attachableid | Tagid |
| --------------- | ------------- | ------ |
| Post            | 1             | 2      |
| Post            | 1             | 4      |
| Tutoriel        | 2             | 3      |
| Post            | 4             | 1      |
|                 |               |        |

Hum okay, bah là je sais pas si Doctrine permettrait par le biais de l'annotation de le faire, mais je te dirais de rajouter une annotation PrePersist et PreUpdate qui récupèrera la class_name de l'entité associée

@Grafikart, tu as regardé du côté du tracking policy de Doctrine?
Va voir https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/change-tracking-policies.html
Regarde le mode NOTIFY, ça pourrait (peut-être) le faire?...

Bonsoir.
@Digivia: Je ne voudrais pas dire, mais ce sujet à au minimum 2 ans, et la dernière réponse précédant la tienne également, je pense donc que depuis le temps il a trouvé une solution à sa question.
Pour faire simple, tu ne fais que déterrer un vieux sujet.

Nop toujours pas trouvé du coup je regarde ça ;)