• Développement
  • Magento

La recette pour un bon modèle de données Magento 2- partie 3

kevin_2234efd45f
Kevin Weyhaupt, Développeur back-end
Le 22 mars 2018
cooking_magento_2_f8c45f0d28
  • ecommerce

Lecture :10 minutes

EDIT : Le contenu de cet article peut ne plus être d'actualité suite aux nouvelles bonnes pratiques suggérées par Magento.

Partie 3 : La touche du chef

L’ingrédient secret, l’Entity Manager

Rappel, retrouvez le code complet et commenté : https://github.com/blackbird-agency/magento-2-data-model-sample.

L’entity manager est arrivé avec la version 2.1 de Magento et comme son nom l’indique c’est un gestionnaire d’entité. Mais avant toutes choses je tiens à prévenir que, depuis la version 2.2, son utilisation n’est pas recommandée par Magento. Voici ce que spécifie le framework dans la classe “EntityManager” :
“Il n’est pas recommandé d’utiliser l’EntityManager et son infrastructure pour les entités persistantes. Dans un futur proche va apparaître un nouvel ‘Persistence Entity Manager’ afin de couvrir les besoins de la couche de persistance avec les requêtes API pour la lecture des données.
Actuellement il est recommandé d'utiliser le ‘Resource Model’ en utilisant Magento\Framework\Model\ResourceModel\Db\AbstractDb ou encore Magento\Eav\Model\Entity\AbstractEntity s’il s'agit d’un modèle EAV.
Pour les collections et les filtres il est recommandé d’utiliser Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection”

Malgré cela il faut tout de même admettre que l’EntityManager s'avère très utile pour certains cas, notamment le nôtre qui est de gérer une table relationnelle dans le modèle de données utilisé pour cet article. Et donc comment se construit-il ?
Tout d’abord nous allons à nouveau faire un tour auprès de l’injection de dépendance, donc rendez-vous dans le di.xml. On va déclarer nos entités pour le “MetadataPool” qui va permettre de faire ce qu’on faisait avant dans notre Resource Model, c’est à dire, faire le lien entre notre entité (en utilisant à nouveau l’interface de l’API) et la base de données en spécifiant la table et sa clé primaire.

<type name="Magento\Framework\EntityManager\MetadataPool">
 <arguments>
  <argument name="metadata" xsi:type="array">
    <item name="MyVendor\MyModule\Api\Data\StudentInterface" xsi:type="array">
     <item name="entityTableName" xsi:type="string">student</item>
     <item name="identifierField" xsi:type="string">student_id</item>
    </item>
    <item name="MyVendor\MyModule\Api\Data\TeacherInterface" xsi:type="array">
      <item name="entityTableName" xsi:type="string">teacher</item>
      <item name="identifierField" xsi:type="string">teacher_id</item>
    </item>
  </argument>
 </arguments>
</type>

Maintenant on se dirige vers les Resource Models MyVendor\MyModule\Model\ResourceModel\Student et Teacher pour surcharger les méthodes du CRUD afin d’utiliser l’EntityManager. Bien évidemment il faut penser à ajouter l’EntityManager dans nos classes.

public function save(AbstractModel $object)
{
  return $this->entityManager->save($object);
}

public function load(\Magento\Framework\Model\AbstractModel $object, $value, $field = null)
{
  return <$this->entityManager->load($object, $value);
}

public function delete(\Magento\Framework\Model\AbstractModel $object)
{
  $this->entityManager->delete($object);
}

Et pour finir avec la mise en place de l’EntityManager on va hydrater nos objets. Mais que cela signifie-t-il ? Hydrater un objet c’est lui donner les éléments nécessaire pour fonctionner, comme par exemple donner des valeurs à ses attributs (comme ce que fait le constructeur au moment d’instancier un objet).

<type name="Magento\Framework\EntityManager\HydratorPool">
  <arguments>
      <argument name="hydrators" xsi:type="array">
          <item name="MyVendor\MyModule\Api\Data\StudentInterface" xsi:type="string">Magento\Framework\EntityManager\AbstractModelHydrator</item>
          <item name="MyVendor\MyModule\Api\Data\TeacherInterface" xsi:type="string">Magento\Framework\EntityManager\AbstractModelHydrator</item>
      </argument>
  </arguments>
</type>

Vous voulez en savoir encore plus sur cet EntityManager, notamment sur ce qu’il offre pour gérer les tables associatives ? Je vous invite à lire la suite pour connaître la cerise sur le gâteau !

La cerise sur le gâteau : les « handlers »

Les “handlers” sont un aspect apporté par l’EntityManager qui va permettre de gérer les tables associatives (comme dans notre exemple avec « teacher_students »).Ce sont donc des classes qui vont être très utiles pour gérer le CRUD sur les tables associatives à l’aide de “l’Entity Manager”. Et ainsi grâce à l’injection de dépendance, on évite de gérer le CRUD dans le Resource Model avec les « after/beforeDelete/Save/Update ».
Les classes des “Handlers”, étendant “ExtensionInterface” de l’Entity Manager, seront appelées par leur méthode “execute” qui elle se chargera de faire ce que vous lui demandez au moment de lire, mettre à jour, créer ou supprimer une entité.
Avant de créer nos handlers, on va ajouter une méthode à notre Resource Model (MyVendor\MyModule\Model\ResourceModel\Student) qui va nous permettre de récupérer pour un “Student” tous les “Teacher” associés. Cela va nous être utile pour la récupération, modification ou sauvegarde d’un “Student”.

protected function lookupTeacherIds($studentId)
  {
      $connection = $this->getConnection();

      $entityMetadata = $this->metadataPool->getMetadata(StudentInterface::class);
      $linkField = $entityMetadata->getLinkField();

      $select = $connection->select()
          ->from(['ts' => $this->getTable('teacher_students')], 'id_teacher')
          ->join(
              ['stud' => $this->getMainTable()],
              'stud.' . $linkField . ' = ts.' . $linkField,
              []
          )
          ->where('ts.' . $entityMetadata->getIdentifierField() . ' = :id_student');

      return $connection->fetchCol($select, ['id_student' => (int)$studentId]);
  }

Bien évidemment prenez soin de vérifier que vous avez le nécessaire pour que la méthode fonctionne, notamment le “MetadataPool”. De plus dans notre cas il faudrait aussi le faire pour le Resource Model de “Teacher” mais ayant déjà pas mal de code dans cet article, il n’est pas montré ici sachant que les changements sont minimes (mais il est tout de même disponible sur le Github https://github.com/blackbird-agency/magento-2-data-model-sample).

Le ReadHandler

Le ReadHandler va permettre, dans ce cas, de gérer la lecture des “Teacher” associés à un “Student”.

<?php

namespace MyVendor\MyModule\Model\ResourceModel\Student\Relation;

use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\EntityManager\Operation\ExtensionInterface;
use MyVendor\MyModule\Model\ResourceModel\Student;

class ReadHandler implements ExtensionInterface
{
  private $metadataPool;

  private $resourceStudent;

  publicfunction __construct(
      MetadataPool $metadataPool,
      Student $resourceStudent
  ) {
      $this->metadataPool = $metadataPool;
      $this->resourceStudent = $resourceStudent;
  }

  publicfunction execute($entity, $arguments = [])
  {
      if ($entity->getStudentId()) {
          $studentTeacherIds = $this->resourceStudent->lookupTeacherIds((int)$entity->getStudentId());
          $entity->setData('teacher_ids', $studentTeacherIds);
      }
      return $entity;
  }
}

Le SaveHandler

Ici le SaveHandler sera utilisé pour la création et la mise à jour d’un “Student”. Dans le cas où c’est une mise à jour, on va vérifier si il y a eu l’ajout ou la suppression d’un ou plusieurs “Teacher” et ainsi on modifie la table associative.

<?php

namespace MyVendor\MyModule\Model\ResourceModel\Student\Relation;


use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\EntityManager\Operation\ExtensionInterface;
use MyVendor\MyModule\Api\Data\StudentInterface;
use MyVendor\MyModule\Model\ResourceModel\Student;

class SaveHandler implements ExtensionInterface
{
  protected $metadataPool;

  protected $resourceStudent;

  publicfunction __construct(
      MetadataPool $metadataPool,
      Student $resourceStudent
  ) {
      $this->metadataPool = $metadataPool;
      $this->resourceStudent = $resourceStudent;
  }

  publicfunction execute($entity, $arguments = [])
  {
      $entityMetadata = $this->metadataPool->getMetadata(StudentInterface::class);
      $linkField      = $entityMetadata->getLinkField();

      $connection = $entityMetadata->getEntityConnection();

      $oldTeachers = $this->resourceStudent->lookupTeacherIds((int)$entity->getStudentId());
      $newTeacher = (array)$entity->getTeacherIds();

      $table = $this->resourceStudent->getTable('teacher_students');

      $delete = array_diff($oldTeachers, $newTeacher);
      if ($delete) {
          $where = [
              $linkField . ' = ?'        => $entity->getStudentId(),
              'teacher_id IN (?)' => $delete,
          ];
          $connection->delete($table, $where);
      }

      $insert = array_diff($newTeacher, $oldTeachers);
      if ($insert) {
          $data = [];
          foreach ($insert as $teacherId) {
              $data[] = [
                  $linkField => $entity->getStudentId(),
                  'teacher_id' => (int)$teacherId
              ];
          }
          $connection->insertMultiple($table, $data);
      }

      return $entity;
  }
}

Après cela on peut très bien imaginer un “DeleteHandler” mais dans notre cas, si le modèle de données a bien été construit avec des “DeleteCascade”, il n’est pas nécessaire de gérer la suppression dans la table associative.

Injection de dépendance pour les handlers

Pour finir, il est encore une dernière fois nécessaire d’apporter certains ajouts au di.xml afin de gérer le CRUD et ainsi utiliser nos handlers. Pour cela nous définissons les “ExtensionPool” pour faire le lien entre l’action (create, read, update, delete), notre Handler et le modèle concerné.

<type name="Magento\Framework\EntityManager\Operation\ExtensionPool">
  <arguments>
      <argument name="extensionActions" xsi:type="array">
          <item name="MyVendor\MyModule\Api\Data\StudentInterface" xsi:type="array">
              <item name="read" xsi:type="array">
                  <item name="studentReader" xsi:type="string">MyVendor\MyModule\Model\ResourceModel\Student\Relation\ReadHandler</item>
              </item>
              <item name="create" xsi:type="array">
                  <item name="studentCreator" xsi:type="string">MyVendor\MyModule\Model\ResourceModel\Student\Relation\SaveHandler</item>
              </item>
              <item name="update" xsi:type="array">
                  <item name="studentUpdater" xsi:type="string">MyVendor\MyModule\Model\ResourceModel\Student\Relation\SaveHandler</item>
              </item>
          </item>
          <item name="MyVendor\MyModule\Api\Data\TeacherInterface" xsi:type="array">
              <item name="read" xsi:type="array">
                  <item name="teacherReader" xsi:type="string">MyVendor\MyModule\Model\ResourceModel\Teacher\Relation\ReadHandler</item>
              </item>
              <item name="create" xsi:type="array">
                  <item name="teacherCreator" xsi:type="string">MyVendor\MyModule\Model\ResourceModel\Teacher\Relation\SaveHandler</item>
              </item>
              <item name="update" xsi:type="array">
                  <item name="teacherUpdater" xsi:type="string">MyVendor\MyModule\Model\ResourceModel\Teacher\Relation\SaveHandler</item>
              </item>
          </item>
      </argument>
  </arguments>
</type>


Et voilà, notre recette touche à sa fin. Bonne dégustation à vous ! En espérant que cela vous a plu et que la digestion ne sera pas trop difficile. 

Nos articles

Découvrir aussi

Abonnez-vous au blog pour ne rien louper