La déclaration XML des composants de l’interface utilisateur (UI Components) dans l’administration de Magento 2 permet de créer facilement des grilles mais enlève un peu de flexibilité en terme de données à ajouter. Récemment, j’ai du ajouter des données issues d’un JSON dans un tableau. Deux obstacles se présentaient à moi. Le premier était l’impossibilité de savoir à l’avance les colonnes que je devrais ajouter. Le second était que les colonnes avec des dates devaient être affichées comme des dates.
Adaptation de la liste à l’injection dynamique de colonnes
La première chose à faire est de prévoir que les colonnes seront créées dynamiquement. Pour cela, il faut oublier le composant PHP de base. Dans le xml déclaratif, les colonnes seront servies par votre classe adaptée et plus par \Magento\Ui\Component\Listing\Columns
<columns name="crealoz_example_columns" class="\Crealoz\Example\Ui\Component\Listing\Columns">
Ensuite, afin de faciliter le travail de création de colonnes, il est nécessaire de créer des colonnes « génériques ». Dans mon cas, j’avais trois types (texte, date et booléen). J’ai donc créé trois colonnes génériques (generic_input, generic_date et generic_yesno) afin de me faciliter la création des colonnes dynamiques. Pour cet exemple, on n’en gardera qu’une.
<column name="generic_input">
<settings>
<sortable>false</sortable>
<label>GenericInput</label>
</settings>
</column>
Enfin, il sera nécessaire d’adapter votre fournisseur de données (DataProvider) pour avoir un fournisseur capable de retourner les colonnes en fonction de l’objet JSON.
<dataProvider class="Crealoz\Example\Ui\Component\Listing\DynamicFieldProvider" name="crealoz_example_listing_data_source">
Voilà pour le fichier crealoz_example_listing.xml. Je vous laisse adapter cela à votre goût.
Le composant PHP pour les colonnes
Pour avoir des colonnes dynamiques, il faut récupérer le JSON et le gérer. Ce n’est pas la partie que je veux montrer dans cet article et il vous faudra avoir recours à une fonction qui récupère les données et les transforme en tableau qui dont chaque entrée ressemblera à ça :
"dynamic_field_first_name" => array:6 [▼
"type" => "text"
"mandatory" => true
"label" => "First name"
"column" => "left"
"session_field" => "firstname"
"max_length" => 40
]
L’entrée type doit correspondre à l’argument dataType de votre xml. Chaque entrée du tableau correspond à une colonne dynamique.
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\View\Element\UiComponent\ContextInterface;
use Magento\Framework\View\Element\UiComponentFactory;
use Psr\Log\LoggerInterface;
use Crealoz\Example\Service\DynamicDataGetter;
class Columns extends \Magento\Ui\Component\Listing\Columns
{
public function __construct(
ContextInterface $context,
protected DynamicDataGetter $dynamicDataGetter,
protected readonly UiComponentFactory $componentFactory,
protected readonly LoggerInterface $logger,
array $components = [],
array $data = []
)
{
parent::__construct($context, $components, $data);
}
/**
* Prépare les colonnes en utilisant le tableau arrivant de dynamicDataGetter
*
* @return void
*/
public function prepare()
{
$sourceColumn = $this->components['generic_input'] ?? null;
if ($sourceColumn) {
/** Récupère les données de configuration depuis la colonne générique **/
$config = $sourceColumn->getConfiguration();
$jsConfig = $sourceColumn->getJsConfig($sourceColumn);
$columns = $this->dynamicDataGetter->getDynamicColumns();
$sortOrder = 35;
foreach ($columns as $columnName => $columnConfig) {
$config['label'] = $this->prepareName($columnName);
/** Prépare les arguments à l'aide de la configuration récupérée depuis la colonne generic_input **/
$arguments = [
'data' => [
'config' => $config,
'js_config' => $jsConfig,
'sortOrder' => $sortOrder
],
'context' => $sourceColumn->getContext(),
];
$sortOrder += 5;
try {
/** On créé la colonne **/
$component = $this->componentFactory->create($columnName, 'column', $arguments);
$this->addComponent($columnName, $component);
} catch (LocalizedException $e) {
$this->logger->error($e->getMessage());
}
}
} else {
$this->logger->error('Source column is needed to add dynamic columns');
}
/**
* Retire la colonne generic_input
*/
unset($this->components['generic_input']);
parent::prepare();
uasort($this->components, [$this, 'sortColumns']);
}
/**
* Retire dynamic_field_ du nom, ajoute des espaces à la place des underscores et met la première lettre en
* capitale.
*
* @param $name
* @return string
*/
protected function prepareName($name)
{
$name = str_replace('dynamic_field_', '', $name);
$name = str_replace('_', ' ', $name);
return ucfirst($name);
}
/**
* Trie les colonnes en fonction de la valeur de sortOrder.
*
* @param $a
* @param $b
* @return int
*/
protected function sortColumns($a, $b)
{
return $a['sortOrder'] <=> $b['sortOrder'];
}
}
Dans mon cas, j’utilise un service qui me permet de respecter le principe de responsabilité unique. Il est injecté sous forme de dépendance dans le constructeur de ma classe.
Récupération et traitement des données
Il faut garder en tête que, dans Magento 2, chaque entrée d’un tableau de l’administration est en réalité un résultat de recherche. Il vous faudra donc traiter votre objet JSON contenant les données pour en faire un résultat de recherche.
/** La collection peut/doit être filtrée en utilisant les critères de recherche **/
$items = $collection->getItems();
foreach ($items as $item) {
$itemData = $item->getData();
/** Pour chaque ligne, on va créer un résultat de recherche */
$itemDocument = $this->documentFactory->create();
$itemDocument->setIdFieldName('entity_id');
foreach ($item->getData() as $key => $value) {
if ($key === 'JSON') {
continue;
}
$itemDocument->setData($key, $value);
}
$jsonData = $this->jsonSerializer->unserialize($item->getData('JSON'));
if (is_array($jsonData)) {
foreach ($jsonData as $field => $value) {
if (key_exists($field, $itemData) && in_array($field, self::COLUMNS_EXCLUDED)) {
continue;
}
$attribute = $this->attributeFactory->create();
$attribute->setAttributeCode('dynamic_field_' . $field);
$attribute->setValue($value);
$itemDocument->setCustomAttribute('dynamic_field_' . $field, $attribute);
}
}
$loadedData['items'][] = $itemDocument;
}
if (!isset($loadedData['totalRecords'])) {
$loadedData['totalRecords'] = $collection->getSize();
}
Chaque document créé à partir de documentFactory étend \Magento\Framework\DataObject. Il doit avoir les getter et setter de votre base de données d’origine en plus de getCustomAttributes, setCustomAttributes, getCustomAttribute et setCustomAttribute. Cela permet l’affichage des champs dynamiques dans la table de données.
Les instructions précédentes peuvent être dans votre dataProvider ou dans un service à part. A mon avis, et pour respecter le principe de responsabilité unique, il vaut mieux utiliser un service. Nous avons donc pu récupérer les colonnes et les données, il est temps de passer à leur affichage.
Le fournisseur de données (DataProvider)
Par rapport au fournisseur de base de Magento 2, il sera nécessaire de surcharger deux fonctions : getSearchResult et searchResultToOutput. La première permet de changer la façon dont on récupère les résultats de recherche pour utiliser la logique implémentée dans le point précédent.
/**
* Dans ce cas, on utilise un service DynamicDataGetter qui contient les instructions pour changer le JSON vers
* des données sous forme de tableau. On utilise une fabrique (Magento\Ui\DataProvider\SearchResultFactory) pour
* retourner les données au format souhaité en gardant les critères de recherche.
*
* @return array
*/
public function getSearchResult($forFront = false)
{
$data = $this->dynamicDataGetter->getData([], $this->getSearchCriteria());
return $this->searchResultFactory->create(
$data['items'],
$data['totalRecords'],
$this->getSearchCriteria(),
'request_id'
);
}
La seconde fonction va permettre de mettre en forme des résultats déjà filtrés et d’ajouter les colonnes manquantes à chaque résultat.
/**
* Prépare les données
*
* @param SearchResultInterface $searchResult
* @return array
*/
protected function searchResultToOutput(SearchResultInterface $searchResult)
{
$arrItems = [];
$arrItems['items'] = [];
foreach ($searchResult->getItems() as $item) {
$itemData = [];
/** On peut ajouter les attributs personalisés comme entrée de la ligne de résultat */
foreach ($item->getCustomAttributes() as $attribute) {
$itemData[$attribute->getAttributeCode()] = $attribute->getValue();
if ($attribute->getAttributeCode() === 'custom_attributes') {
foreach ($attribute->getValue() as $childAttribute) {
$attributeCode = $childAttribute["attribute_code"];
$value = $childAttribute["value"];
$itemData[$attributeCode] = $value;
}
}
}
$arrItems['items'][] = $itemData;
}
$arrItems['totalRecords'] = $searchResult->getTotalCount();
/** Le résultat doit être un tableau contenant le nombre de résultats en plus de ces derniers */
return $arrItems;
}
Avec cela et un peu de travail, vous devriez être capable de retourner sous forme de tableau, les réponses d’un webservice ou toute autre donnée au format JSON. Si le résultats contient beaucoup de données ou est régulièrement demandé, il peut être important de cacher les résultats en plus du cache de Magento2
Laisser un commentaire
Vous devez vous connecter pour publier un commentaire.