referentiel-produit.jpg

Loïc Veroudart / Insight
3/28/24 5:07 PM

Liferay Commerce - Adaptation du référentiel produits aux spécificités du client

Quelle solution pour étendre les données d’un produit ?

Liferay offre la possibilité de personnaliser ses propres entités via les champs personnalisés (Custom Field). Cette fonctionnalité permet d’étendre les entités Liferay avec des données spécifiques. Parmi les entités extensibles figurent notamment les objets de Liferay Commerce (Product, Specification, Option, SKU, Media).

La création d’un champ personnalisé est possible via l’interface back-office. Chaque nouvel attribut nécessite une déclaration propre. Cependant, en fonction du nombre d’attributs personnalisés à créer et du nombre d’éléments à traiter sur le portail, on peut se retrouver confronté à des problématiques de performance. Cela implique également un suivi des créations/déclarations des attributs à chaque nouveau champ et pour chaque environnement ; cela génère donc une complexité supplémentaire pour la maintenance du site.

Nous allons maintenant vous présenter une autre approche consistant à déclarer un seul champ supplémentaire pour chaque entité. Ce champ contiendra une version JSON du contenu des champs supplémentaires de l'entité.

La structure de données

Afin de structurer notre code back-end, nous avons allons créer un DTO (Data Transfer Object) correspondant aux nouveaux champs du produit. Nous utiliserons également un service dédié qui procède à la conversion automatique entre la valeur stockée au niveau du champ personnalisé de Liferay et notre DTO.

Pour notre exemple, nous déclarons deux nouveaux attributs pimReference et externalStatus.

@JsonIgnoreProperties(ignoreUnknown = true)
public class ProductExpandoDTO {

    private String pimReference;

    private String externalStatus;
    
}

L’annotation @JsonIgnoreProperties a son importance : elle permet d’ignorer les éventuelles propriétés qui pourraient se trouver dans le JSON mais qui ne sont plus dans le DTO.

La conversion DTO → JSON

Pour la conversion Object en JSON, on utilise la classe ObjectMapper. Cette classe est également utilisée au niveau des modules natifs de Liferay.

Tout d’abord il est nécessaire d’initialiser l'ObjectMapper :

   private static final ObjectMapper _OBJECT_MAPPER = new ObjectMapper() {
        {
            configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
            disable(SerializationFeature.INDENT_OUTPUT);
            setSerializationInclusion(JsonInclude.Include.NON_NULL);
        }
    };

Puis la fonction toJson permettra la conversion en JSON. Nous avons choisi de retourner un JSON vide dans le cas où la conversion échouerait. Une alternative serait de lever une exception dédiée pour interrompre la conversion.

    public static String toJson(ProductExpandoDTO dto) {
        if (dto != null) {
            try {
                return _OBJECT_MAPPER.writeValueAsString(dto);
            } catch (JsonProcessingException e) {
                return JSONFactoryUtil.createJSONObject().toJSONString();
            }
        } else {
            return JSONFactoryUtil.createJSONObject().toJSONString();
        }
    }

Ensuite une fonction générique déclenche la modification du champ personnalisé. Cette fonction pourra être réutilisée ultérieurement pour d’autres entités ou attributs. Le dernier paramètre false de la fonction setAttribute désactive le contrôle des permissions : nous faisons le choix de désactiver les contrôles car la modification est principalement réalisée par un traitement.

    private void setExpandoValue(BaseModel model, String expandoName, Serializable value) {
        if (model != null && Validator.isNotNull(expandoName)) {
            model.getExpandoBridge().setAttribute(expandoName, value, false);
        }
    }

Enfin la fonction suivante combine les éléments précédents, ainsi que la déclaration de la clé à utiliser.

   public void setProductCustomField(CPDefinition cpDefinition, ProductExpandoDTO expandoDTO) {
        setExpandoValue(cpDefinition, "PRODUCT_CUSTOM_FIELD", toJson(expandoDTO));
    }

La conversion JSON → DTO

Comme pour la conversion DTO vers JSON, ici on considère qu’en cas de problème de conversion, l’objet est corrompu et qu’un nouvel objet vide est initialisé.

    public static ProductExpandoDTO getDTO(String json) {
        try {
            if (Validator.isNotNull(json)) {
                return ObjectMapperUtil.readValue(ProductExpandoDTO.class, json);
            } else {
                return new ProductExpandoDTO();
            }
        } catch (Exception e) {
            return new ProductExpandoDTO();
        }
    }

Comme pour l’enregistrement, la récupération de la valeur nécessite l’entité source et le nom du champ. On utilise également ici le paramètre false avec la fonction getAttribute afin de ne pas tenir compte des permissions.

    private Serializable getExpandoValue(BaseModel model, String expandoName) {
        if (model != null) {
            return model.getExpandoBridge().getAttribute(expandoName, false);
        } else {
            return null;
        }
    }

Il ne reste plus qu’à combiner les précédentes fonctions pour obtenir le contenu du champ personnalisé. La clé utilisée pour le champ personnalisé doit être définie dans un fichier de constantes, afin d'éviter l’utilisation malencontreuse de clés différentes entre la lecture et l'écriture.

    public ProductExpandoDTO getProductCustomField(CPDefinition cpDefinition) {
        return getDTO((String) getExpandoValue(cpDefinition, "PRODUCT_CUSTOM_FIELD"));
    }

Avec cette approche, nous bénéficions donc maintenant d'un objet annexe au CPDefinition, qu’il est facile de récupérer et d'enregistrer. Au besoin, si l’on souhaite étoffer ces propriétés, il suffit d’ajouter un nouvel attribut au DTO, et il sera automatiquement intégré au JSON. Attention cependant à toujours prévoir le cas des objects pré-existants qui ne disposent pas encore de la nouvelle propriété. Pour cela, un UpgradeProcess peut s'avérer utile.

Les autres fonctionnalités liées au produit

Cette solution peut ensuite être déclinée sur les autres entités de la partie produit :

  • Spécification ou Option d’un produit (CPDefinitionSpecificationOptionValue / CPdefinitionOptionValueRel), afin d’ajouter par exemple une icône en plus de la valeur

  • SKU d’un produit (CPInstance) pour l'ajout de champs poids, priorité, code...

  • Média d’un produit (CPAttachmentFileEntry) avec type de fichier, langues disponibles.

Mise en forme des informations

Nous venons de voir comment stocker simplement et efficacement au niveau du produit les données d’un point de vue back-end. Cependant la lecture et l'interprétation des données peuvent paraître contraignantes, en particulier pour les utilisateurs ne maîtrisant pas les subtilités du JSON. Il peut également être intéressant de présenter ces données avec une mise en forme particulière (couleur en fonction du statut, formatage d’une date ou d’une valeur…)

 

Nous allons maintenant voir comment créer un nouvel onglet au niveau de la fiche produit du back-office de Liferay Commerce, et y intégrer l’affichage des informations issues du champ personnalisé que l'on vient de créer.

Le Screen Navigation framework

Screen Navigation est un framework mis à disposition par Liferay et permet d’intégrer dynamiquement des onglets dans des écrans existants. Liferay utilise cette mécanique pour ses fonctionnalités natives. Attention, chaque onglet nécessite sa propre déclaration.

Dans notre cas nous allons ajouter un onglet “Product Expando” sur l'écran back-office de gestion des données produit.

Pour cela, il est nécessaire d’implémenter plusieurs interfaces fournies par Liferay.

Tout d’abord, l’interface ScreenNavigationCategory permet de déclarer le nouvel onglet et de le rattacher à l'écran de gestion d’un produit.

@Component(
    property = "screen.navigation.category.order:Integer=130",
    service = ScreenNavigationCategory.class
)
public class CPDefinitionExpandoScreenNavigationCategory implements ScreenNavigationCategory {
    @Override
    public String getCategoryKey() {
        return "productExpando";
    }

    @Override
    public String getLabel(Locale locale) {
        return "Product Expando";
    }

    @Override
    public String getScreenNavigationKey() {
        return CPDefinitionScreenNavigationConstants.
                SCREEN_NAVIGATION_KEY_CP_DEFINITION_GENERAL;
    }
}

CategoryKey représente l'identifiant de notre nouvelle catégorie et de l'onglet associé. Le label sert à déclarer le libellé de votre nouvel onglet, et screenNavigationKey correspond à la référence du portlet cible, ici la page de consultation et d'édition d’un produit.

Il faut ensuite déclarer un DisplayContext propre à notre futur écran. Pour l’instant, nous allons partir sur une version simple, avec uniquement l’implémentation de base. On pourra ensuite aller plus loin et intégrer dans le DisplayContext des services supplémentaires.

public class CPDefinitionExpandoDisplayContext extends BaseCPDefinitionsDisplayContext {
    public CPDefinitionExpandoDisplayContext(
        ActionHelper actionHelper, HttpServletRequest httpServletRequest) {

        super(actionHelper, httpServletRequest);
    }
}

Ensuite la déclaration du ScreenNavigationEntry permet de définir la JSP à utiliser et de procéder à l’initialisation du contexte nécessaire à la génération de la page.

@Component(
        property = "screen.navigation.entry.order:Integer=11",
        service = ScreenNavigationEntry.class
)
public class CPDefinitionExpandoScreenNavigationEntry 
            extends CPDefinitionExpandoScreenNavigationCategory 
            implements ScreenNavigationEntry<CPDefinition> {

    @Override
    public String getEntryKey() {
        return getCategoryKey();
    }

    @Override
    public boolean isVisible(User user, CPDefinition cpDefinition) {
        return true;
    }

    @Override
    public void render(
            HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse)
            throws IOException {

        CPDefinitionExpandoDisplayContext cpDefinitionExpandoDisplayContext =
                new CPDefinitionExpandoDisplayContext(_actionHelper,
                    httpServletRequest);
        httpServletRequest.setAttribute(
                WebKeys.PORTLET_DISPLAY_CONTEXT,
                cpDefinitionExpandoDisplayContext);
        
        try {
            httpServletRequest.setAttribute("productExpando",
                    productExpandoService.getProductCustomField(
                        cpDefinitionExpandoDisplayContext.getCPDefinition()));
        } catch (PortalException e) {
        }

        _jspRenderer.renderJSP(
                _servletContext, httpServletRequest, httpServletResponse,
                "/edit_cp_definition_expando.jsp");
    }

    @Reference
    private ProductExpandoService productExpandoService;

    @Reference
    private ActionHelper _actionHelper;

    @Reference
    private JSPRenderer _jspRenderer;

    @Reference(
            target = "(osgi.web.symbolicname=product)"
    )
    private ServletContext _servletContext;
}

La propriété screen.navigation.entry.order déclarée au niveau du composant permet de définir l’ordre d’affichage de l’onglet par rapport aux onglets existants. Actuellement, l’onglet configuration qui est le dernier de la liste est inscrit avec un ordre de 10.

La fonction isVisible permet de paramétrer la visibilité de façon dynamique. Dans notre exemple, elle est toujours visible, mais on pourrait envisager de rendre l’onglet visible seulement selon certaines informations disponibles au niveau du produit ou selon des permissions ou une caractéristique du profil de l'utilisateur.

Le render est découpé en 3 parties, une première où on initialise le contexte à partir des informations de la requête, une seconde partie où l’on récupère les informations de notre expando en réutilisant les fonctions décrites dans les paragraphes précédents, et enfin une dernière partie où l’on rend la JSP avec les informations contenues dans la requête.

Au niveau de la JSP, on reste sur une implémentation basique avec l’affichage des données sur 2 colonnes.

<%@ taglib uri="http://liferay.com/tld/commerce-ui" prefix="commerce-ui" %>
<%@ taglib uri="http://liferay.com/tld/aui" prefix="aui" %>

<div class="pt-4">
    <commerce-ui:panel title='Product expando'  >
        <div class="col-6">
            <aui:input ignoreRequestValue="<%= true %>" label="PIM reference" name="pimReference" type="text" value="${productExpando.pimReference}">
            </aui:input>
        </div>
        <div class="col-6">
            <aui:input ignoreRequestValue="<%= true %>" label="External status" name="externalStatus" type="text" value="${productExpando.externalStatus}">
            </aui:input>
        </div>
    </commerce-ui:panel>
</div> 

On récupère ici les données de notre extension du produit “productExpando” qui ont été exposées précédemment et diffusées dans la requête.

Une fois que le module est déployé, on obtient une nouvelle page dans la fiche produit.


 

Résumé

Dans cet article, nous avons exploré une méthode permettant de personnaliser de manière simple et évolutive les informations liées aux produits dans Liferay en utilisant les champs personnalisés de Liferay. Nous avons vu comment mettre en place notre modèle de données spécifique, puis dans un deuxième temps les possibilités offertes par Liferay pour mettre en forme ces données. Nous nous sommes concentrés sur la consultation de données liées au produit. Cependant cette logique peut également être appliquée aux autres éléments comme les propriétés ou options d’un produit, les SKUs, les documents attachés au produit...

Partager cet article :
Lien copié

Insight

Autres articles qui pourraient vous plaire…

Card image cap

/

Card image cap

/

Card image cap

/