Prestashop : créer un admin controller pour un module

Nous allons voir les différentes étapes pour réaliser facilement un controller admin basique lié à un module Prestashop.
Puis nous verrons ensuite les caractéristiques de base d’un controller admin, cet article sera complété par un autre pour voir les fonctionnalités avancées 😉

Pour cela nous allons créer un module “samplemodule”.
Le code complet est visible sur github : https://github.com/nenes25/prestashop_samplemodule/tree/admin

Les prérequis suivants sont nécessaires pour réaliser un controller admin pour votre module :

  • Création d’une “Tab”
  • Utilisation d’un objet Prestashop héritant de la classe ObjectModel ( et de la base de données qui lui est associée )

Pour l’objet Prestashop nous partirons sur un objet fictif “Sample” avec les propriétés suivantes :

  • name : nom de l’objet
  • code : code de l’objet
  • email : email de l’objet
  • title : titre de l’objet ( différent en fonction de la langue )
  • description : description de l’objet ( différent en fonction de la langue )

Le code suivant sera dans le fichier classes/Sample.php du module

class Sample extends ObjectModel
{
 
    public $id;
    public $code;
    public $email;
    public $title;
    public $description;
 
    public static $definition = [
        'table' => 'hh_sample',
        'primary' => 'id_sample',
        'multilang' => true,
        'fields' => [
            // Champs Standards
            'name' => ['type' => self::TYPE_STRING, 'validate' => 'isName', 'size' => 255, 'required' => true],
            'code' => ['type' => self::TYPE_STRING, 'validate' => 'isLinkRewrite', 'size' => 255, 'required' => true],
            'email' => ['type' => self::TYPE_STRING, 'validate' => 'isEmail', 'size' => 255, 'required' => true],
            //Champs langue
            'title' => ['type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isCleanHtml', 'size' => 255,],
            'description' => ['type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isCleanHtml',],
        ],
    ];
}

 

Pour la création de la tab et de la base de donnée c’est le module qui se charge de gérer tout cela lors de son installation.

//Inclusion du modèle Sample
require_once _PS_MODULE_DIR_ . '/samplemodule/classes/Sample.php';
 
class SampleModule extends Module
{
    public function __construct()
    {
        $this->author = 'hhennes';
        $this->name = 'samplemodule';
        $this->tab = 'hhennes';
        $this->version = '0.1.1';
        $this->need_instance = 0;
 
        parent::__construct();
 
        $this->displayName = $this->l('Prestashop sample Module');
        $this->description = $this->l('Prestashop sample Module with front controller');
    }
 
    /**
     * Installation du module
     * @return boolean
     */
    public function install()
    {
        return parent::install() && $this->_installSql() && $this->_installTab();
    }
 
    /**
     * Désinstallation du module
     * @return boolean
     */
    public function uninstall()
    {
        return parent::uninstall() && $this->_uninstallSql() && $this->_uninstallTab();
    }
 
    /**
     * Création de la base de donnée
     * @return boolean
     */
    protected function _installSql()
    {
        $sqlCreate = "CREATE TABLE `" . _DB_PREFIX_ . Sample::$definition['table'] . "` (
                `id_sample` int(11) unsigned NOT NULL AUTO_INCREMENT,
                `name` varchar(255) DEFAULT NULL,
                `code` varchar(255) DEFAULT NULL,
                `email` varchar(255) DEFAULT NULL,
                `date_add` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
                `date_upd` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
                PRIMARY KEY (`id_sample`)
                ) ENGINE=InnoDB DEFAULT CHARSET=latin1;";
 
        $sqlCreateLang = "CREATE TABLE `" . _DB_PREFIX_ . Sample::$definition['table'] . "_lang` (
              `id_sample` int(11) unsigned NOT NULL AUTO_INCREMENT,
              `id_lang` int(11) NOT NULL,
              `title` varchar(255) DEFAULT NULL,
              `description` text,
              PRIMARY KEY (`id_sample`)
            ) ENGINE=InnoDB DEFAULT CHARSET=latin1;";
 
        return Db::getInstance()->execute($sqlCreate) && Db::getInstance()->execute($sqlCreateLang);
    }
 
    /**
     * Installation du controller dans la backoffice
     * @return boolean
     */
    protected function _installTab()
    {
        $tab = new Tab();
        $tab->class_name = 'AdminHhSample';
        $tab->module = $this->name;
        $tab->id_parent = (int)Tab::getIdFromClassName('DEFAULT');
        $tab->icon = 'settings_applications';
        $languages = Language::getLanguages();
        foreach ($languages as $lang) {
            $tab->name[$lang['id_lang']] = $this->l('HH Sample Admin controller');
        }
        try {
            $tab->save();
        } catch (Exception $e) {
            echo $e->getMessage();
            return false;
        }
 
        return true;
    }
 
    /**
     * Désinstallation du controller admin
     * @return boolean
     */
    protected function _uninstallTab()
    {
        $idTab = (int)Tab::getIdFromClassName('AdminHhSample');
        if ($idTab) {
            $tab = new Tab($idTab);
            try {
                $tab->delete();
            } catch (Exception $e) {
                echo $e->getMessage();
                return false;
            }
        }
        return true;
    }
 
    /**
     * Suppression de la base de données
     */
    protected function _uninstallSql()
    {
        $sql = "DROP TABLE ".Sample::$definition['table'].",".Sample::$definition['table']."_lang";
        return Db::getInstance()->execute($sql);
    }
}

Création du controller admin

Celui-ci doit être créé dans le dossier /controllers/admin/ de votre module.
Pour l’exemple celui-ci se nommera AdminHhSample et le fichier AdminHhSample.php

Je rappelle que les fonctions basiques d’un controller admin dans le cadre d’un module sont :

  • d’afficher une liste d’objets Prestashop
  • d’ajouter et d’éditer ces objets

Voici le code basique de ce fichier

require_once _PS_MODULE_DIR_ . '/samplemodule/classes/Sample.php';
class AdminHhSampleController extends ModuleAdminController
{
}

Le nom de classe du controller est nomdufichierController et il doit étendre la classe
Dans celui-ci on inclus également le fichier php contenant la classe de l’objet que nous allons gérer.

 

Liste des objets :

La fonction par défaut du controller est d’afficher une liste des objets.
Pour cela il faut renseigner toutes les informations nécessaires dans la fonction __construct() de la classe
J’ai commenté les différents paramètres pour expliquer leur fonctionnement.

A noter que les champs $this->fields_list sont ensuite utilisés dans une liste généré via la classe HelperList, si vous souhaitez visualiser toutes les possibilités.

   /**
     * Instanciation de la classe
     * Définition des paramètres basiques obligatoires
     */
    public function __construct()
    {
        $this->bootstrap = true; //Gestion de l'affichage en mode bootstrap 
        $this->table = Sample::$definition['table']; //Table de l'objet
        $this->identifier = Sample::$definition['primary']; //Clé primaire de l'objet
        $this->className = Sample::class; //Classe de l'objet
        $this->lang = true; //Flag pour dire si utilisation de langues ou non
 
        //Appel de la fonction parente pour pouvoir utiliser la traduction ensuite
        parent::__construct();
 
        //Liste des champs de l'objet à afficher dans la liste
        $this->fields_list = [
            'id_sample' => [ //nom du champ sql
                'title' => $this->module->l('ID'), //Titre
                'align' => 'center', // Alignement
                'class' => 'fixed-width-xs' //classe css de l'élément
            ],
            'name' => [
                'title' => $this->module->l('name'),
                'align' => 'left',
            ],
            'code' => [
                'title' => $this->module->l('code'),
                'align' => 'left',
            ],
            'email' => [
                'title' => $this->module->l('email'),
                'align' => 'left',
            ],
            'title' => [
                'title' => $this->module->l('title'),
                'lang' => true, //Flag pour dire d'utiliser la langue
                'align' => 'left',
            ]
        ];
 
        //Ajout d'actions sur chaque ligne
        $this->addRowAction('edit');
        $this->addRowAction('delete');
    }

Avec ce code nous aurons l’affichage suivant :

blog listing admin

Ajout / Édition d’un objet

Pour l’ajout ou l’édition d’un objet le controller doit implémenter la fonction renderForm avec le contenu suivant :
Pour voir l’ensemble des champs possibles vous pouvez visualiser le controller natif de AdminPatterns via l’url : http://www.votre-site/admin/index.php?controller=AdminPatterns

/**
     * Affichage du formulaire d'ajout / création de l'objet
     * @return string
     * @throws SmartyException
     */
    public function renderForm()
    {
        //Définition du formulaire d'édition
        $this->fields_form = [
            //Entête
            'legend' => [
                'title' => $this->module->l('Edit Sample'),
                'icon' => 'icon-cog'
            ],
            //Champs
            'input' => [
                [
                    'type' => 'text', //Type de champ
                    'label' => $this->module->l('name'), //Label
                    'name' => 'name', //Nom
                    'class' => 'input fixed-width-sm', //classes css
                    'size' => 50, //longueur maximale du champ
                    'required' => true, //Requis ou non
                    'empty_message' => $this->l('Please fill the postcode'), //Message d'erreur si vide
                    'hint' => $this->module->l('Enter sample name') //Indication complémentaires de saisie
                ],
                [
                    'type' => 'text',
                    'label' => $this->module->l('code'),
                    'name' => 'code',
                    'class' => 'input fixed-width-sm',
                    'size' => 5,
                    'required' => true,
                    'empty_message' => $this->module->l('Please fill the code'),
                ],
                [
                    'type' => 'text',
                    'label' => $this->module->l('email'),
                    'name' => 'email',
                    'class' => 'input fixed-width-sm',
                    'size' => 5,
                    'required' => true,
                    'empty_message' => $this->module->l('Please fill email'),
                ],
                [
                    'type' => 'text',
                    'label' => $this->module->l('Title'),
                    'name' => 'title',
                    'class' => 'input fixed-width-sm',
                    'lang' => true, //Flag pour utilisation des langues
                    'required' => true,
                    'empty_message' => $this->l('Please fill the title'),
                ],
                [
                    'type' => 'textarea',
                    'label' => $this->module->l('Title'),
                    'name' => 'title',
                    'lang' => true,
                    'autoload_rte' => true, //Flag pour éditeur Wysiwyg
                ],
            ],
            //Boutton de soumission
            'submit' => [
                'title' => $this->l('Save'), //On garde volontairement la traduction de l'admin par défaut
            ]
        ];
        return parent::renderForm();
    }

Le rendu sera ensuite le suivant :

Blog édition sample

 

Avec ces 2 éléments vous avez à présent un controller admin fonctionnel pour votre module qui permet de gérer facilement votre entité Prestashop 🙂

Bonus : Un bouton d’ajout dans la toolbar

Le bouton d’ajout d’un élément est relativement petit par défaut, voici comment un ajouter un plus visible
Ajouter la fonction initPageHeaderToolbar() dans votre controller avec le contenu suivant :

  /**
     * Gestion de la toolbar
     */
    public function initPageHeaderToolbar()
    {
 
        //Bouton d'ajout
        $this->page_header_toolbar_btn['new'] = array(
            'href' => self::$currentIndex . '&add' . $this->table . '&token=' . $this->token,
            'desc' => $this->module->l('Add new Sample'),
            'icon' => 'process-icon-new'
        );
 
        parent::initPageHeaderToolbar();
    }
Pas de commentaires

Magento 2 : Ajouter une colonne dans la facture pdf

Voyons ensemble comment ajouter une colonne dans la facture sous Magento 2.
Nous souhaitons ajouter une colonne “Tax Rate” qui affichera le taux de taxe appliqué à chaque élément de la facture.

Le rendu final sera le suivant :

Invoice Magento 2

Pour faire cela nous allons créer un module Hhennes_Sales
( je ne détaille pas le processus de création basique du module )

Celui-ci devra surcharger les classes suivantes :

  • Magento\Sales\Model\Order\Pdf\Invoice
  • Magento\Sales\Model\Order\Pdf\Items\Invoice\DefaultInvoice

Pour cela il faudra mettre le contenu suivant dans le fichier etc/di.xml du module :

   <?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Magento\Sales\Model\Order\Pdf\Invoice"
                type="Hhennes\Sales\Model\Sales\Order\Pdf\Invoice"></preference>
    <preference for="Magento\Sales\Model\Order\Pdf\Items\Invoice\DefaultInvoice"
                type="Hhennes\Sales\Model\Sales\Order\Pdf\Items\Invoice\DefaultInvoice"></preference>
</config>

Concernant les surcharges :

La notion importante est la valeur de “feed” , qui correspond à la position en x du contenu dans le pdf
Pour obtenir un ensemble visuellement harmonieux, il peut être nécessaire de jouer avec ces valeurs pour l’ensemble des éléments.

Pour la surcharge de classe Magento\Sales\Model\Order\Pdf\Invoice : qui correspond à la classe de la facture en général, on ajoute une colonne dans la fonction _drawHeader

<?php
namespace Hhennes\Sales\Model\Sales\Order\Pdf;
 
class Invoice extends \Magento\Sales\Model\Order\Pdf\Invoice
{
    /**
     * @param \Zend_Pdf_Page $page
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    protected function _drawHeader(\Zend_Pdf_Page $page)
    {
        /* Add table head */
        $this->_setFontRegular($page, 10);
        $page->setFillColor(new \Zend_Pdf_Color_RGB(0.93, 0.92, 0.92));
        $page->setLineColor(new \Zend_Pdf_Color_GrayScale(0.5));
        $page->setLineWidth(0.5);
        $page->drawRectangle(25, $this->y, 570, $this->y - 15);
        $this->y -= 10;
        $page->setFillColor(new \Zend_Pdf_Color_RGB(0, 0, 0));
 
        //columns headers
        $lines[0][] = ['text' => __('Products'), 'feed' => 35];
 
        $lines[0][] = ['text' => __('SKU'), 'feed' => 250, 'align' => 'right'];
 
        $lines[0][] = ['text' => __('Price'), 'feed' => 300, 'align' => 'left'];
 
        $lines[0][] = ['text' => __('Qty'), 'feed' => 350, 'align' => 'left'];
 
        // Tutoriel : Affichage du taux de taxe
        $lines[0][] = ['text' => __('Tax Rate'), 'feed' => 450, 'align' => 'right'];
        // Tutoriel : Affichage du taux de taxe
 
        $lines[0][] = ['text' => __('Tax'), 'feed' => 495, 'align' => 'right'];
 
        $lines[0][] = ['text' => __('Subtotal'), 'feed' => 565, 'align' => 'right'];
 
        $lineBlock = ['lines' => $lines, 'height' => 5];
 
 
        $this->drawLineBlocks($page, [$lineBlock], ['table_header' => true]);
        $page->setFillColor(new \Zend_Pdf_Color_GrayScale(0));
        $this->y -= 20;
    }
}

Pour la classe Magento\Sales\Model\Order\Pdf\Items\Invoice\DefaultInvoice qui correspond à l’affichage par défaut d’un élément de la commande, nous allons également ajouter une colonne

<?php
namespace Hhennes\Sales\Model\Sales\Order\Pdf\Items\Invoice;
 
 
class DefaultInvoice extends \Magento\Sales\Model\Order\Pdf\Items\Invoice\DefaultInvoice
{
 
    /**
     * Draw item line
     *
     * @return void
     */
    public function draw()
    {
        $order = $this->getOrder();
        $item = $this->getItem();
        $pdf = $this->getPdf();
        $page = $this->getPage();
        $lines = [];
 
        // draw Product name
        $lines[0] = [['text' => $this->string->split($item->getName(), 25, true, true), 'feed' => 35]];
 
        // draw SKU
        $lines[0][] = [
            'text' => $this->string->split($this->getSku($item), 17),
            'feed' => 250,
            'align' => 'right',
        ];
 
        // draw QTY
        $lines[0][] = ['text' => $item->getQty() * 1, 'feed' => 350, 'align' => 'left'];
 
        // draw item Prices
        $i = 0;
        $prices = $this->getItemPricesForDisplay();
        $feedPrice = 300;
        $feedSubtotal = $feedPrice + 250;
        foreach ($prices as $priceData) {
            if (isset($priceData['label'])) {
                // draw Price label
                $lines[$i][] = ['text' => $priceData['label'], 'feed' => $feedPrice, 'align' => 'left'];
                // draw Subtotal label
                $lines[$i][] = ['text' => $priceData['label'], 'feed' => $feedSubtotal, 'align' => 'right'];
                $i++;
            }
            // draw Price
            $lines[$i][] = [
                'text' => $priceData['price'],
                'feed' => $feedPrice,
                'font' => 'bold',
                'align' => 'right',
            ];
            // draw Subtotal
            $lines[$i][] = [
                'text' => $priceData['subtotal'],
                'feed' => $feedSubtotal,
                'font' => 'bold',
                'align' => 'right',
            ];
            $i++;
        }
 
        // Tutoriel : Affichage du taux du taxe
        $lines[0][] = [
            'text' => number_format($item->getOrderItem()->getTaxPercent(),2),
            'feed' => 450, //Attention à la position  du feed
            'font' => 'bold',
            'align' => 'right',
        ];
        // Fin Tutoriel : Affichage du taux du taxe
 
        // draw Tax
        $lines[0][] = [
            'text' => $order->formatPriceTxt($item->getTaxAmount()),
            'feed' => 495,
            'font' => 'bold',
            'align' => 'right',
        ];
 
        // custom options
        $options = $this->getItemOptions();
        if ($options) {
            foreach ($options as $option) {
                // draw options label
                $lines[][] = [
                    'text' => $this->string->split($this->filterManager->stripTags($option['label']), 40, true, true),
                    'font' => 'italic',
                    'feed' => 35,
                ];
 
                if ($option['value']) {
                    if (isset($option['print_value'])) {
                        $printValue = $option['print_value'];
                    } else {
                        $printValue = $this->filterManager->stripTags($option['value']);
                    }
                    $values = explode(', ', $printValue);
                    foreach ($values as $value) {
                        $lines[][] = ['text' => $this->string->split($value, 30, true, true), 'feed' => 40];
                    }
                }
            }
        }
 
        $lineBlock = ['lines' => $lines, 'height' => 20];
 
        $page = $pdf->drawLineBlocks($page, [$lineBlock], ['table_header' => true]);
        $this->setPage($page);
    }
 
}

 

3 commentaires

Utiliser plusieurs version mysql avec docker

J’ai récemment souhaité tester un module sur d’anciennes version de prestashop, mais celle-ci ne fonctionnaient pas correctement avec mysql 5.7 qui est installé par défaut sur mon serveur de développement ( basé sur ubuntu 16.04 )

Ce service devant tourner de manière ponctuelle, l’installer en doublon de la version 5.6 n’était pas une option.
La solution la plus simple est donc d’utiliser docker, nous allons voir comment avoir un autre serveur de base de données avec la version 5.6 en quelques minutes 🙂

Installation de docker

Pour cela je vous envoie vers la documentation officielle qui explique tout très bien : https://docs.docker.com/install/linux/docker-ce/ubuntu/#extra-steps-for-aufs

Installer docker-compose

sudo apt-get install docker-compose

Création du fichier docker-compose

Pour le ranger j’ai  créer un dossier /mysql56 dans mon dossier home

mkdir ~/mysql56

Dans lequel on va créer un dossier /data qui contiendra le contenu des bases de données du container.
Ceci permettra de persister les données en éteignant le container.

On va ensuite installer l’image mysql5.6 en créant un fichier docker-compose.yml dans le dossier avec le contenu suivant

version: '2'
services:
  mysql5.6:
    image: mysql:5.6
    restart: always
    volumes:
     - ./data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
    ports:
        - "3309:3309"
    networks:
      network:
         ipv4_address: 172.20.0.2
networks:
    network:
        ipam:
            config:
                - subnet: 172.20.0.0/16

Celui-ci va lancer  une base de données mysql 5.6 en complément de la version 5.7 ,celle-ci sera accessible via l’ip 172.20.0.2 et le login / mot de passe : root / root

Pour lancer le container il suffit ensuite de lancer la commande :

sudo docker-compose up

Si vous souhaitez lancer la commande en background vous pouvez rajouter l’option “-d”

Vous pouvez ensuite consulter les containers en cours d’éxécution via la commande :

sudo docker-compose ps

Et les arrêter via la commande

sudo docker-compose stop
Pas de commentaires

Magento 2 : Ajouter des link alternate sur les pages cms

Sur magento 2 ( comme sur magento 1 ) , les contenus et notamment les pages cms sont accessibles via plusieurs url avec ou sans slash à la fin.

Vous pouvez le constater sur la page de la politique de confidentialité qui est à la fois disponible sous les urls :
http://magento2-demo.nexcess.net/privacy-policy-cookie-restriction-mode et http://magento2-demo.nexcess.net/privacy-policy-cookie-restriction-mode/

Au niveau Seo c’est assez moyen c’est pourquoi il est utile de préciser à Google quelle est la version à indexer.
Ceci peut être réalisé via l’ajout d’une balise link rel=”canonical”.

Nous allons voir ensemble comment réaliser cela pour magento 2 via un module qui s’appellera Hhennes/Cms
Ce module ajoutera automatiquement une balise canonical sur les pages cms, il est possible de configurer en back office quelle sera l’url à utiliser ( avec ou sans le slash )

Ce module sera dépendant du module Magento_Cms.

Je vais juste détailler le fonctionnement global, vous trouverez le lien vers github en fin d’article pour le télécharger. (sans la partie création initiale )

La logique est relativement simple , nous allons rajouter un block spécifique sur les pages cms dans le container “head.additional”
Pour cela il faut créer le fichier ( dans app/Code/Hennes/Cms ) view/frontend/layout/cms_page_view.xml ( il sera évalué uniquement lors de l’affichage d’une page cms ) avec le contenu suivant :

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="head.additional">
            <block class="Hhennes\Cms\Block\Page\Canonical" name="cms_page.canonical"/>
        </referenceContainer>
    </body>
</page>

 

Le contenu du block qui sera situé dans Block/Page/Canonical.php sera le suivant (version simplifiée ) :

<?php
 
namespace Hhennes\Cms\Block\Page;
 
use \Magento\Framework\View\Element\AbstractBlock;
 
class Canonical extends AbstractBlock
{
 
    /** @var \Magento\Cms\Model\Page */
    protected $_page;
 
    /** @var \Hhennes\Cms\Helper\Data */
    protected $_helper;
 
    /**
     * Canonical constructor.
     * @param \Magento\Framework\View\Element\Context $context
     * @param array $data
     * @param \Magento\Cms\Api\Data\PageInterface $page
     * @param \Hhennes\Cms\Helper\Data $helper
     */
    public function __construct(
        \Magento\Framework\View\Element\Context $context,
        array $data = [],
        \Magento\Cms\Api\Data\PageInterface $page,
        \Hhennes\Cms\Helper\Data $helper
    )
    {
        $this->_page = $page;
        $this->_helper = $helper;
        parent::__construct($context, $data);
    }
 
    /**
     * @return \Magento\Cms\Api\Data\PageInterface
     */
    public function getPage()
    {
        return $this->_page;
    }
 
    /**
     * Get Canonical Page Url ( simplified version )
     */
    public function getCanonicalPageUrl()
    {
        if ($this->getPage()) {
 
                return $this->getUrl() . $this->getPage()->getIdentifier();
        } else {
            return false;
        }
    }
 
    /**
     * Display block
     * @return string
     */
    public function _toHtml()
    {
        if ($this->getCanonicalPageUrl()) {
            return "\n" . '<link rel="canonical" href="' . $this->getCanonicalPageUrl() . '"/>' . "\n";
        }
 
        return '';
    }
 
}

Pour télécharger le module vous pouvez vous rendre sur github : https://github.com/nenes25/magento2-cms
Ou l’installer directement via composer avec la commande suivante :

 composer require hhennes/module-cms dev-master

Une fois installé la balise canonical sera bien présente sur la page :

Canonical magento 2

Pas de commentaires

Magento : Liste des ressources utilisables dans system.xml

Un petit article rapide d’aide mémoire pour la gestion des fichiers system.xml dans magento 1 .
Ces fichiers peuvent utiliser souvent les éléments suivants :

 Source Model

Il doivent être utilisés pour des champs de types “select” ou “multiselect”

Ex :

<demo_field_source translate="label">
    <label>Demo Field source model</label>
    <frontend_type>select</frontend_type>
    <!-- Définition de la source -->
    <source_model>adminhtml/system_config_source_yesno</source_model>
    <!-- Fin Définition de la source -->
    <sort_order>1</sort_order>
    <show_in_default>1</show_in_default>
    <show_in_website>1</show_in_website>
    <show_in_store>1</show_in_store>
</demo_field_source>

Les plus utiles à retenir sont les suivants :

  • adminhtml/system_config_source_yesno ( Oui / Non )
  • adminhtml/system_config_source_country (Liste des pays )
  • adminhtml/system_config_source_cms_page ( Liste des pages cms )
  • adminhtml/system_config_source_email_template ( Listes des emails transactionnels )

Liste complète :

adminhtml/system_config_source_yesno
adminhtml/system_config_source_admin_page
adminhtml/system_config_source_catalog_gridPerPage
adminhtml/system_config_source_catalog_listMode
adminhtml/system_config_source_catalog_listPerPage
adminhtml/system_config_source_catalog_listSort
adminhtml/system_config_source_catalog_search_type
adminhtml/system_config_source_catalog_timeFormat
adminhtml/system_config_source_checktype
adminhtml/system_config_source_cms_page
adminhtml/system_config_source_cms_wysiwyg_enabled
adminhtml/system_config_source_country
adminhtml/system_config_source_cron_frequency
adminhtml/system_config_source_currency
adminhtml/system_config_source_currency_service
adminhtml/system_config_source_customer_address_type
adminhtml/system_config_source_customer_forgotpassword
adminhtml/system_config_source_customer_group
adminhtml/system_config_source_design_robots
adminhtml/system_config_source_email_identity
adminhtml/system_config_source_email_method
adminhtml/system_config_source_email_smtpauth
adminhtml/system_config_source_email_template
adminhtml/system_config_source_enabledisable
adminhtml/system_config_source_frequency
adminhtml/system_config_source_locale
adminhtml/system_config_source_locale_currency_all
adminhtml/system_config_source_locale_timezone
adminhtml/system_config_source_locale_weekdaycodes
adminhtml/system_config_source_locale_weekdaycodes
adminhtml/system_config_source_locale_weekdays
adminhtml/system_config_source_nooptreq
adminhtml/system_config_source_notification_frequency
adminhtml/system_config_source_order_status_new
adminhtml/system_config_source_order_status_newprocessing
adminhtml/system_config_source_order_status_processing
adminhtml/system_config_source_payment_allspecificcountries
adminhtml/system_config_source_payment_cctype
adminhtml/system_config_source_price_scope
adminhtml/system_config_source_price_step
adminhtml/system_config_source_product_thumbnail
adminhtml/system_config_source_reports_scope
adminhtml/system_config_source_security_domainpolicy
adminhtml/system_config_source_shipping_allspecificcountries
adminhtml/system_config_source_shipping_flatrate
adminhtml/system_config_source_shipping_tablerate
adminhtml/system_config_source_shipping_taxclass
adminhtml/system_config_source_storage_media_database
adminhtml/system_config_source_storage_media_storage
adminhtml/system_config_source_store
adminhtml/system_config_source_tax_apply_on
adminhtml/system_config_source_tax_basedon
adminhtml/system_config_source_watermark_position
adminhtml/system_config_source_web_redirect
adminhtml/system_config_source_yesno
adminhtml/system_config_source_yesnocustom
backup/config_source_type
captcha/config_font
captcha/config_form_backend
captcha/config_form_frontend
captcha/config_mode
catalog/product_attribute_source_msrp_type
cataloginventory/source_backorders
checkout/config_source_cart_summary
configurableswatches/system_config_source_catalog_product_configattribute
configurableswatches/system_config_source_catalog_product_configattribute_select
customer/config_share
downloadable/system_config_source_contentdisposition
downloadable/system_config_source_orderitemstatus
googleanalytics/system_config_source_type
googlebase/source_accounttype
googlebase/source_country
log/adminhtml_system_config_source_loglevel
pagecache/system_config_source_controls
paygate/authorizenet_source_cctype
paygate/authorizenet_source_cctype
paygate/authorizenet_source_paymentAction
payment/source_invoice
paypal/config::getApiAuthenticationMethods
paypal/config::getExpressCheckoutBASignupOptions
paypal/config::getExpressCheckoutButtonFlavors
paypal/config::getExpressCheckoutSolutionTypes
paypal/config::getPayflowproCcTypesAsOptionArray
paypal/config::getWppCcTypesAsOptionArray
paypal/config::getWppPeCcTypesAsOptionArray
paypal/system_config_source_bmlPosition::getBmlPositionsCCP
paypal/system_config_source_bmlPosition::getBmlPositionsCheckout
paypal/system_config_source_bmlPosition::getBmlPositionsCPP
paypal/system_config_source_bmlPosition::getBmlPositionsHP
paypal/system_config_source_bmlSize::getBmlSizeCCPC
paypal/system_config_source_bmlSize::getBmlSizeCCPS
paypal/system_config_source_bmlSize::getBmlSizeCheckoutC
paypal/system_config_source_bmlSize::getBmlSizeCheckoutN
paypal/system_config_source_bmlSize::getBmlSizeCPPC
paypal/system_config_source_bmlSize::getBmlSizeCPPN
paypal/system_config_source_bmlSize::getBmlSizeHPH
paypal/system_config_source_bmlSize::getBmlSizeHPS
paypal/system_config_source_buyerCountry
paypal/system_config_source_fetchingSchedule
paypal/system_config_source_logo
paypal/system_config_source_merchantCountry
paypal/system_config_source_paymentActions
paypal/system_config_source_paymentActions_express
paypal/system_config_source_requireBillingAddress
paypal/system_config_source_urlMethod
paypal/system_config_source_yesnoShortcut
salesrule/system_config_source_coupon_format
shipping/source_handlingAction
shipping/source_handlingType
tax/system_config_source_algorithm
tax/system_config_source_apply
tax/system_config_source_priceType
tax/system_config_source_tax_country
tax/system_config_source_tax_display_type
tax/system_config_source_tax_region
usa/shipping_carrier_abstract_source_mode
usa/shipping_carrier_abstract_source_requesttype
usa/shipping_carrier_dhl_international_source_contenttype
usa/shipping_carrier_dhl_international_source_method_doc
usa/shipping_carrier_dhl_international_source_method_freedoc
usa/shipping_carrier_dhl_international_source_method_freenondoc
usa/shipping_carrier_dhl_international_source_method_nondoc
usa/shipping_carrier_dhl_international_source_method_size
usa/shipping_carrier_dhl_international_source_method_unitofmeasure
usa/shipping_carrier_dhl_source_dutypaymenttype
usa/shipping_carrier_dhl_source_freemethod
usa/shipping_carrier_dhl_source_method
usa/shipping_carrier_dhl_source_protection_rounding
usa/shipping_carrier_dhl_source_protection_value
usa/shipping_carrier_dhl_source_shipmenttype
usa/shipping_carrier_fedex_source_dropoff
usa/shipping_carrier_fedex_source_freemethod
usa/shipping_carrier_fedex_source_method
usa/shipping_carrier_fedex_source_packaging
usa/shipping_carrier_fedex_source_unitofmeasure
usa/shipping_carrier_ups_source_container
usa/shipping_carrier_ups_source_destType
usa/shipping_carrier_ups_source_freemethod
usa/shipping_carrier_ups_source_method
usa/shipping_carrier_ups_source_originShipment
usa/shipping_carrier_ups_source_pickup
usa/shipping_carrier_ups_source_type
usa/shipping_carrier_ups_source_unitofmeasure
usa/shipping_carrier_usps_source_container
usa/shipping_carrier_usps_source_freemethod
usa/shipping_carrier_usps_source_machinable
usa/shipping_carrier_usps_source_method
usa/shipping_carrier_usps_source_size
weee/config_source_display
weee/config_source_fpt_tax
wishlist/config_source_summary

 

 Frontend Model :

Le frontend model est un block de template magento  et  qui permets de gérer un affichage particulier pour l’élément de configuration.
Voici 3 exemples possibles :

<demo_field_frontend_model1>
    <label>Demo Field frontend model heading</label>
    <frontend_model>adminhtml/system_config_form_field_heading</frontend_model>
    <sort_order>6</sort_order>
    <show_in_default>1</show_in_default>
    <show_in_website>1</show_in_website>
    <show_in_store>1</show_in_store>
</demo_field_frontend_model1>
<demo_field_frontend_model2>
    <label>Demo Field frontend model 2</label>
    <type>text</type>
    <frontend_model>adminhtml/catalog_form_renderer_config_yearRange</frontend_model>
    <sort_order>7</sort_order>
    <show_in_default>1</show_in_default>
    <show_in_website>1</show_in_website>
    <show_in_store>1</show_in_store>
</demo_field_frontend_model2>
<demo_field_frontend_model3>
    <label>Demo Field frontend model 3</label>
    <type>text</type>
    <frontend_model>adminhtml/system_config_form_field_regexceptions</frontend_model>
    <sort_order>8</sort_order>
    <show_in_default>1</show_in_default>
    <show_in_website>1</show_in_website>
    <show_in_store>1</show_in_store>
</demo_field_frontend_model3>

Et le rendu associé à ce code

Frontend model

Liste complète :

adminhtml/system_config_form_field_notification
adminhtml/catalog_form_renderer_config_dateFieldsOrder
adminhtml/catalog_form_renderer_config_yearRange
adminhtml/customer_system_config_validatevat
adminhtml/report_config_form_field_mtdStart
adminhtml/report_config_form_field_ytdStart
adminhtml/system_config_form_field_heading
adminhtml/system_config_form_field_regexceptions
adminhtml/system_config_form_field_select_flatcatalog
adminhtml/system_config_form_field_select_flatproduct
adminhtml/system_config_form_fieldset_modules_disableOutput
adminhtml/system_config_system_storage_media_synchronize
cataloginventory/adminhtml_form_field_minsaleqty
directory/adminhtml_frontend_currency_base
paypal/adminhtml_system_config_apiWizard
paypal/adminhtml_system_config_bmlApiWizard
paypal/adminhtml_system_config_field_country
paypal/adminhtml_system_config_field_hidden
paypal/adminhtml_system_config_field_solutionType
paypal/adminhtml_system_config_fieldset_deprecated
paypal/adminhtml_system_config_fieldset_expanded
paypal/adminhtml_system_config_fieldset_group
paypal/adminhtml_system_config_fieldset_hint
paypal/adminhtml_system_config_fieldset_location
paypal/adminhtml_system_config_fieldset_pathDependent
paypal/adminhtml_system_config_fieldset_payment
paypal/adminhtml_system_config_payflowlink_advanced
paypal/adminhtml_system_config_payflowlink_info
tax/adminhtml_frontend_region_updater
usa/adminhtml_dhl_unitofmeasure

 Backend Model :

Ces modèles permettent de traiter la donnée de configuration pour gérer  son affichage ou son enregistrement en base de données.
( Ajouter des vérifications suplémentaires, sérialiser/désérializer des données , encoder un mot de passe … )

Visuellement cela n’a pas d’incidence, voici un exemple d’utilisation :

<base_url translate="label">
    <label>Base URL</label>
    <frontend_type>text</frontend_type>
    <backend_model>adminhtml/system_config_backend_baseurl</backend_model>
    <sort_order>10</sort_order>
    <show_in_default>1</show_in_default>
    <show_in_website>1</show_in_website>
    <show_in_store>1</show_in_store>
</base_url>

Liste complète :

adminhtml/system_config_backend_admin_custom
adminhtml/system_config_backend_admin_custompath
adminhtml/system_config_backend_admin_password_link_expirationperiod
adminhtml/system_config_backend_admin_usecustom
adminhtml/system_config_backend_admin_usecustompath
adminhtml/system_config_backend_admin_usesecretkey
adminhtml/system_config_backend_baseurl
adminhtml/system_config_backend_catalog_inventory_managestock
adminhtml/system_config_backend_catalog_search_type
adminhtml/system_config_backend_cookie
adminhtml/system_config_backend_currency_allow
adminhtml/system_config_backend_currency_base
adminhtml/system_config_backend_currency_cron
adminhtml/system_config_backend_currency_default
adminhtml/system_config_backend_customer_address_street
adminhtml/system_config_backend_customer_password_link_expirationperiod
adminhtml/system_config_backend_customer_show_address
adminhtml/system_config_backend_customer_show_customer
adminhtml/system_config_backend_datashare
adminhtml/system_config_backend_datashare
adminhtml/system_config_backend_design_exception
adminhtml/system_config_backend_design_package
adminhtml/system_config_backend_email_address
adminhtml/system_config_backend_email_logo
adminhtml/system_config_backend_email_sender
adminhtml/system_config_backend_encrypted
adminhtml/system_config_backend_filename
adminhtml/system_config_backend_image
adminhtml/system_config_backend_image_favicon
adminhtml/system_config_backend_image_pdf
adminhtml/system_config_backend_layer_children
adminhtml/system_config_backend_locale
adminhtml/system_config_backend_locale_timezone
adminhtml/system_config_backend_log_cron
adminhtml/system_config_backend_price_scope
adminhtml/system_config_backend_product_alert_cron
adminhtml/system_config_backend_secure
adminhtml/system_config_backend_seo_product
adminhtml/system_config_backend_shipping_tablerate
adminhtml/system_config_backend_sitemap
adminhtml/system_config_backend_sitemap_cron
adminhtml/system_config_backend_storage_media_database
adminhtml/system_config_backend_store
adminhtml/system_config_backend_translate
adminhtml/system_config_backend_web_secure_offloaderheader
backup/config_backend_cron
catalog/system_config_backend_catalog_category_flat
catalog/system_config_backend_catalog_product_flat
catalog/system_config_backend_catalog_url_rewrite_suffix
cataloginventory/system_config_backend_minqty
cataloginventory/system_config_backend_minsaleqty
cataloginventory/system_config_backend_qtyincrements
catalogsearch/system_config_backend_sitemap
contacts/system_config_backend_links
customer/config_share
importexport/product_attribute_backend_urlkey
paypal/system_config_backend_cert
paypal/system_config_backend_merchantCountry
rss/system_config_backend_links
tax/config_notification
tax/config_price_include
xmlconnect/adminhtml_system_config_backend_baseurl
xmlconnect/adminhtml_system_config_backend_currency_default

Pas de commentaires

Prestashop 1.7 : Ajouter une navigation à facettes dans les listings

La gestion des listings à complétement été réécrite sur Prestashop 1.7 , et la bonne nouvelle et qu’ils gèrent maintenant nativement la navigation à facettes ( Tout du moins la partie front 😉 )
Nous allons voir ensemble comment mettre en place une navigation à facette basique sur la page des nouveaux produits.

Sommaire :

  1. Fonctionnement général
  2. Affichage de facettes
  3. Tri et gestion des facettes

Cet article est plus dans une démarche d’explication du fonctionnement, que dans la réalisation d’un module purement fonctionnel, et la réalisation est assez chronophage, dans la majorité des cas il sera préférable de passer par un module qui fera cela.
( Le module de navigation à facette de Prestashop le permets uniquement sur les catégories )

Fonctionnement général

Le premier point essentiel à noter et qu’il n’est pas nécessaire de faire de surcharge, tout peut être géré via un module 🙂

Avant de voir ce qu’il faut coder, il est important de comprendre comment fonctionnent les pages de listing sur prestashop 1.7
Dans mon exemple on va utiliser le controller NewProductsController ( controllers/front/listing/NewProductsController.php ) qui gère les nouveaux produits.

Lors de son initialisation il appelle la fonction doProductSearch de la classe parente ProductListingFrontController

    public function init()
    {
        parent::init();
        $this->doProductSearch('catalog/listing/new-products');
    }

Celle-ci appelle la fonction getProductSearchVariables ( j’omets volontairement le cas getAjaxProductSearchVariables qui est parlant puisqu’il rajoute des information spécifiques uniquement lors d’un appel ajax )

Le code est un peu long, mais toute la logique des controller listing sous Prestashop est dans cette fonction :
Je laisse les commentaires de base en anglais qui sont plutôt clairs.

<!--?php    
     /**
     * This returns all template variables needed for rendering
     * the product list, the facets, the pagination and the sort orders.
     *
     * @return array variables ready for templating
     */
    protected function getProductSearchVariables()
    {
        /*
         * To render the page we need to find something (a ProductSearchProviderInterface)
         * that knows how to query products.
         */
 
        // the search provider will need a context (language, shop...) to do its job
        $context = $this-&gt;getProductSearchContext();
 
        // the controller generates the query...
        $query = $this-&gt;getProductSearchQuery();
 
        // ...modules decide if they can handle it (first one that can is used)
        // Dans notre cas c'est ICI que le module devra être appellé
        $provider = $this-&gt;getProductSearchProviderFromModules($query);
 
        // if no module wants to do the query, then the core feature is used
        if (null === $provider) {
            $provider = $this-&gt;getDefaultProductSearchProvider();
        }
 
        $resultsPerPage = (int) Tools::getValue('resultsPerPage');
        if ($resultsPerPage &lt;= 0 || $resultsPerPage &gt; 36) {
            $resultsPerPage = Configuration::get('PS_PRODUCTS_PER_PAGE');
        }
 
        // we need to set a few parameters from back-end preferences
        $query
            -&gt;setResultsPerPage($resultsPerPage)
            -&gt;setPage(max((int) Tools::getValue('page'), 1))
        ;
 
        // set the sort order if provided in the URL
        if (($encodedSortOrder = Tools::getValue('order'))) {
            $query-&gt;setSortOrder(SortOrder::newFromString(
                $encodedSortOrder
            ));
        }
 
        // get the parameters containing the encoded facets from the URL
        $encodedFacets = Tools::getValue('q');
 
        /*
         * The controller is agnostic of facets.
         * It's up to the search module to use /define them.
         *
         * Facets are encoded in the "q" URL parameter, which is passed
         * to the search provider through the query's "$encodedFacets" property.
         */
 
        $query-&gt;setEncodedFacets($encodedFacets);
 
        // We're ready to run the actual query!
 
        $result = $provider-&gt;runQuery(
            $context,
            $query
        );
 
        // sort order is useful for template,
        // add it if undefined - it should be the same one
        // as for the query anyway
        if (!$result-&gt;getCurrentSortOrder()) {
            $result-&gt;setCurrentSortOrder($query-&gt;getSortOrder());
        }
 
        // prepare the products
        $products = $this-&gt;prepareMultipleProductsForTemplate(
            $result-&gt;getProducts()
        );
 
        // render the facets
        if ($provider instanceof FacetsRendererInterface) {
            // with the provider if it wants to
            $rendered_facets = $provider-&gt;renderFacets(
                $context,
                $result
            );
            $rendered_active_filters = $provider-&gt;renderActiveFilters(
                $context,
                $result
            );
        } else {
            // with the core
            $rendered_facets = $this-&gt;renderFacets(
                $result
            );
            $rendered_active_filters = $this-&gt;renderActiveFilters(
                $result
            );
        }
 
        $pagination = $this-&gt;getTemplateVarPagination(
            $query,
            $result
        );
 
        // prepare the sort orders
        // note that, again, the product controller is sort-orders
        // agnostic
        // a module can easily add specific sort orders that it needs
        // to support (e.g. sort by "energy efficiency")
        $sort_orders = $this-&gt;getTemplateVarSortOrders(
            $result-&gt;getAvailableSortOrders(),
            $query-&gt;getSortOrder()-&gt;toString()
        );
 
        $sort_selected = false;
        if (!empty($sort_orders)) {
            foreach ($sort_orders as $order) {
                if (isset($order['current']) &amp;&amp; true === $order['current']) {
                    $sort_selected = $order['label'];
                    break;
                }
            }
        }
 
        $searchVariables = array(
            'label' =&gt; $this-&gt;getListingLabel(),
            'products' =&gt; $products,
            'sort_orders' =&gt; $sort_orders,
            'sort_selected' =&gt; $sort_selected,
            'pagination' =&gt; $pagination,
            'rendered_facets' =&gt; $rendered_facets,
            'rendered_active_filters' =&gt; $rendered_active_filters,
            'js_enabled' =&gt; $this-&gt;ajax,
            'current_url' =&gt; $this-&gt;updateQueryString(array(
                'q' =&gt; $result-&gt;getEncodedFacets(),
            )),
        );
 
        Hook::exec('filterProductSearch', array('searchVariables' =&gt; &amp;$searchVariables));
        Hook::exec('actionProductSearchAfter', $searchVariables);
 
        return $searchVariables;
    }
</pre>
<p>En raccourci dans cette fonction notre module pourra renvoyer une classe de provider spécifique grâce à cet appel :</p>
<pre lang="php" escaped="true"> $provider = $this-&gt;getProductSearchProviderFromModules($query);
</pre>
<p>Ensuite c'est la fonction</p>
<pre lang="php" escaped="true">// We're ready to run the actual query!
 
        $result = $provider-&gt;runQuery(
            $context,
            $query
        );
</pre>
<p>qui va appeller la fonction runQuery du module en lui passant le contexte et la requête de recherche.</p>
<p>La suite de l'affichage ( facettes , tris, et pagination ) seront gérés automatiquement à partir des informations retournées par le provider de notre module.</p>
<p>&nbsp;</p>
<p><a name="affichage"></a><strong>Création du module et affichage de facettes</strong></p>
<p>Maintenant que la base est comprise nous pouvons passer à la création du module, celui-ci s'appellera  <strong>hh_facetedSearch</strong></p>
<p>Voici le code de base du module</p>
<pre lang="php" escaped="true">require_once dirname(__FILE__).'/classes/Hh_facetedSearchProductSearchProvider.php';
class Hh_FacetedSearch extends Module
{
 
    public function __construct()
    {
 
        $this-&gt;author = 'hhennes';
        $this-&gt;name = 'hh_facetedsearch';
        $this-&gt;tab = 'test';
        $this-&gt;version = '0.1.0';
        $this-&gt;bootstrap = true;
        parent::__construct();
 
        $this-&gt;displayName = $this-&gt;l('HH Faceted Search');
        $this-&gt;description = $this-&gt;l('HH Sample Facets Implementation');
 
 
    }
 
    /**
     * Module installation.
     *
     * @return bool Success of the installation
     */
    public function install()
    {
        return parent::install()
            &amp;&amp; $this-&gt;registerHook('productSearchProvider');
    }
 
    /**
     * Dans ce hook on intercepte la requête des nouveaux produits pour y ajouter des facettes
     * @param $params
     * @return Hh_facetedSearchProductSearchProvider
     */
    public function hookProductSearchProvider($params)
    {
        $query = $params['query'];
        if ($query-&gt;getQueryType() == 'new-products') {
            return new Hh_facetedSearchProductSearchProvider($this);
        }
    }
}
</pre>
<p>La point essentiel est qu'il doit être greffé sur le hook <strong>productSearchProvider </strong>et qu'il doit retourner une classe qui implémente l'interface <em>PrestaShop\PrestaShop\Core\Product\Search\ProductSearchProviderInterface</em><br ?-->
Nous créerons le fichier dans <em>classes/Hh_facetedSearchProductSearchProvider.php</em>

Dans cette étape nous allons uniquement afficher des facettes ce qui est géré nativement par le controller prestashop.
Voici à présent le code du Product Search Provider, avec les commentaires explicatifs pour faire cela :

La séléction / déselection est fonctionnelle lorsqu’on clique sur les différents éléments et tous les paramètres sont bien passés.
Notre navigation à facette est en place ( sans prise en compte des données )

Tri et gestion des facettes

Maintenant que la navigation à facette est fonctionnelle la prochaine étape et de renvoyer uniquement les facettes et les produits disponibles pour la sélection demandées.
Pour cette partie nous allons donc rajouter un filtre sur les catégories par défaut des produits.

Pour cela nous allons devoir rajouter 2 nouvelles fonction dans notre module :

  • getNewProducts => Reprends la fonction Product::getNewProducts et lui appliquera les filtres sélectionnés
  • getNewProductsCategoryFilters => Renverra la liste des filtres disponibles pour la séléection des produits

Voici le code complet du module fonctionnel qui permets de filtrer par les catégories par défaut du produit.
Fichier classes/Hh_facetedSearchProductSearchProvider.php

 

<!--?php //Use pour la recherche standard use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchProviderInterface; use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchContext; use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery; use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchResult; use PrestaShop\PrestaShop\Core\Product\Search\SortOrderFactory; //Provider par défaut use PrestaShop\PrestaShop\Adapter\NewProducts\NewProductsProductSearchProvider; class Hh_facetedSearchProductSearchProvider implements ProductSearchProviderInterface { private $module; private $sortOrderFactory; public function __construct( Hh_FacetedSearch $module ) { $this->module = $module;
        $this->sortOrderFactory = new SortOrderFactory($this->module->getTranslator());
    }
 
    /**
     * @param ProductSearchContext $context
     * @param ProductSearchQuery $query
     * @return ProductSearchResult
     */
    public function runQuery(
        ProductSearchContext $context,
        ProductSearchQuery $query
    )
    {
 
        //Récupération des filtres actifs
        $activeFilters = explode('|',$query->getEncodedFacets());
 
        //Récupération des résultats initiaux ( page des nouveaux produits )
        if (!$products = $this->getProductsOrCount($context, $query, 'products',$activeFilters)) {
            $products = array();
        }
        $count = $this->getProductsOrCount($context, $query, 'count',$activeFilters);
 
 
        //Récupération des filtres de catégories dispo pour la sélection de produits
        $categoryfilters = $this->module->getNewProductsCategoryFilters($products,$activeFilters);
 
        /**
         * Dernière Etape : Gestion du résulat
         * Envoi de la productSearchResult
         */
        $results = new ProductSearchResult();
 
        //Définition des résultats des produits
        $results
            ->setTotalProductsCount($count)
            ->setProducts($products);
 
        //Définition des tris disponibles
        $results->setAvailableSortOrders(
            $this->sortOrderFactory->getDefaultSortOrders()
        );
 
        //Définition des facettes disponibles
        if ( sizeof($categoryfilters->getFacets())){
            $results->setFacetCollection(
                $categoryfilters //C'est ici qu'on assigne les filtres de notre fonction
            );
        }
 
        //Définition des facettes utilisées
        $results->setEncodedFacets(
          $query->getEncodedFacets()
        );
 
        return $results;
 
    }
 
    private function getProductsOrCount(
        ProductSearchContext $context,
        ProductSearchQuery $query,
        $type = 'products',
        $activeFilter = array()
    ) {
        //La fonction appellée ici est celle du module
        return $this->module->getNewProducts(
            $context->getIdLang(),
            $query->getPage(),
            $query->getResultsPerPage(),
            $type !== 'products',
            $query->getSortOrder()->toLegacyOrderBy(),
            $query->getSortOrder()->toLegacyOrderWay(),
            Context::getContext(),
            $activeFilter //Rajout d'un paramètre pour passer les filtre sélectionnés
        );
    }
 
    /**
     * Provider de recherche par défaut
     * @return SearchProductSearchProvider
     */
    protected function getDefaultProductSearchProvider()
    {
        return new NewProductsProductSearchProvider(
            Context::getContext()->getTranslator()
        );
    }
 
}
</pre>
<p>Et le fichier du module. <em>hh_facetedsearch.php</em></p>
<p>&nbsp;</p>
<pre lang="php" escaped="true">
<?php require_once dirname(__FILE__).'/classes/Hh_facetedSearchProductSearchProvider.php'; //Use pour les facettes use PrestaShop\PrestaShop\Core\Product\Search\FacetCollection; #Collection de facettes use PrestaShop\PrestaShop\Core\Product\Search\Facet; #Classe de la facette use PrestaShop\PrestaShop\Core\Product\Search\Filter; #Classe des filtres use PrestaShop\PrestaShop\Core\Product\Search\URLFragmentSerializer; #Pour transformer l'url class Hh_FacetedSearch extends Module { public function __construct() { $this->author = 'hhennes';
        $this->name = 'hh_facetedsearch';
        $this->tab = 'test';
        $this->version = '0.1.0';
        $this->bootstrap = true;
        parent::__construct();
 
        $this->displayName = $this->l('HH Faceted Search');
        $this->description = $this->l('HH Sample Facets Implementation');
 
 
    }
 
    /**
     * Module installation.
     *
     * @return bool Success of the installation
     */
    public function install()
    {
        return parent::install()
            && $this->registerHook('productSearchProvider');
    }
 
    /**
     * Dans ce hook on intercepte la requête des nouveaux produits pour y ajouter des facettes
     * @param $params
     * @return Hh_facetedSearchProductSearchProvider
     */
    public function hookProductSearchProvider($params)
    {
        //dump($this->context->controller);
        $query = $params['query'];
        if ($query->getQueryType() == 'new-products') {
            return new Hh_facetedSearchProductSearchProvider($this);
        }
    }
 
    /**
     * A partir des produits sélectionnés on déduit les filtres de catégories à afficher
     * @param array $activeFilters
     * @return FacetCollection
     */
    public function getNewProductsCategoryFilters($products , array $activeFilters)
    {
        //Récupération des catégories des produits et de leurs filtres
        $categoriesArray = [];
        foreach ( $products as $product )
        {
            if ( !array_key_exists($product['id_category_default'],$categoriesArray)){
                $categoriesArray[$product['id_category_default']] = 1;
            } else {
                $categoriesArray[$product['id_category_default']] = (int)$categoriesArray[$product['id_category_default']]+1;
            }
        }
 
        $activeFiltersQueryString ='';
        $activeFiltersQueryString .= implode('|',$activeFilters);
 
        //Création d'une collection de facettes
        $collection = new FacetCollection();
 
        if ( sizeof($categoriesArray)) {
 
            //Création d'une facette
            $facet = new Facet();
            $facet->setLabel($this->l('Catégories'))
                ->setType('category')
                ->setDisplayed(true)
                ->setWidgetType('checkbox')
                ->setMultipleSelectionAllowed(true);
 
            //Création des filtres avec les categories disponibles
            foreach ( $categoriesArray as $categoryId => $categoryCount) {
 
                $encodedFactetsUrl = $activeFiltersQueryString != '' ? $activeFiltersQueryString."|cat-".$categoryId : "cat-".$categoryId;
 
                $category = new Category($categoryId,$this->context->language->id);
                $filter = new Filter();
                $filter->setLabel($category->name)
                    ->setDisplayed(true)
                    ->setActive(in_array("cat-".$categoryId,$activeFilters) ? true : false )
                    ->setType('category')
                    ->setValue($categoryId)
                    ->setNextEncodedFacets($encodedFactetsUrl)
                    ->setMagnitude($categoryCount);
 
                //Ajout du filtre à la facette
                $facet->addFilter($filter);
            }
 
            //Ajout de la facette à la collection
            $collection->addFacet($facet);
        }
 
 
        return $collection;
    }
 
    /**
     * Get new products
     * ( Fonction modifiée de la classe produit avec gestion des filtres )
     *
     * @param int $id_lang Language id
     * @param int $pageNumber Start from (optional)
     * @param int $nbProducts Number of products to return (optional)
     * @return array New products
     */
    public function getNewProducts(
        $id_lang,
        $page_number = 0,
        $nb_products = 10,
        $count = false,
        $order_by = null,
        $order_way = null,
        Context $context = null ,
        $applieds_filters = array() //Nouveau paramètre de test
    )
    {
        $now = date('Y-m-d') . ' 00:00:00';
        if (!$context) {
            $context = Context::getContext();
        }
 
        $front = true;
        if (!in_array($context->controller->controller_type, array('front', 'modulefront'))) {
            $front = false;
        }
 
        if ($page_number < 1) {
            $page_number = 1;
        }
        if ($nb_products < 1) { $nb_products = 10; } if (empty($order_by) || $order_by == 'position') { $order_by = 'date_add'; } if (empty($order_way)) { $order_way = 'DESC'; } if ($order_by == 'id_product' || $order_by == 'price' || $order_by == 'date_add' || $order_by == 'date_upd') { $order_by_prefix = 'product_shop'; } elseif ($order_by == 'name') { $order_by_prefix = 'pl'; } if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay($order_way)) { die(Tools::displayError()); } $sql_groups = ''; if (Group::isFeatureActive()) { $groups = FrontController::getCurrentCustomerGroups(); $sql_groups = ' AND EXISTS(SELECT 1 FROM `'._DB_PREFIX_.'category_product` cp JOIN `'._DB_PREFIX_.'category_group` cg ON (cp.id_category = cg.id_category AND cg.`id_group` '.(count($groups) ? 'IN ('.implode(',', $groups).')' : '= '.(int)Configuration::get('PS_UNIDENTIFIED_GROUP')).') WHERE cp.`id_product` = p.`id_product`)'; } if (strpos($order_by, '.') > 0) {
            $order_by = explode('.', $order_by);
            $order_by_prefix = $order_by[0];
            $order_by = $order_by[1];
        }
 
        $nb_days_new_product = (int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT');
 
        if ($count) {
 
            //Gestion des conditions dans le count
            if ( sizeof($applieds_filters)) {
                $sqlFilters = "";
                foreach ($applieds_filters as $applieds_filter) {
 
                    if ( $applieds_filter == "" ) {
                        continue;
                    }
                    $catId = str_replace('cat-','',$applieds_filter);
                    if ( $catId == 0 ) {
                        continue;
                    }
                    $sqlFilters .= " INNER JOIN "._DB_PREFIX_."product ps_cat_".$catId.' ON  p.id_product = ps_cat_'.$catId.'.id_product AND ps_cat_'.$catId.'.id_category_default = '.$catId.' ';
                }
            }
 
            $sql = 'SELECT COUNT(p.`id_product`) AS nb
					FROM `'._DB_PREFIX_.'product` p 
                    '.$sqlFilters.'
					'.Shop::addSqlAssociation('product', 'p').'
					WHERE product_shop.`active` = 1
					AND product_shop.`date_add` > "'.date('Y-m-d', strtotime('-'.$nb_days_new_product.' DAY')).'"
					'.($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '').'
					'.$sql_groups;
 
            return (int)Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql);
        }
 
        $sql = new DbQuery();
        $sql->select(
            'p.*, product_shop.*, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`link_rewrite`, pl.`meta_description`,
			pl.`meta_keywords`, pl.`meta_title`, pl.`name`, pl.`available_now`, pl.`available_later`, image_shop.`id_image` id_image, il.`legend`, m.`name` AS manufacturer_name,
			(DATEDIFF(product_shop.`date_add`,
				DATE_SUB(
					"'.$now.'",
					INTERVAL '.$nb_days_new_product.' DAY
				)
			) > 0) as new'
        );
 
        $sql->from('product', 'p');
        $sql->join(Shop::addSqlAssociation('product', 'p'));
        $sql->leftJoin('product_lang', 'pl', '
			p.`id_product` = pl.`id_product`
			AND pl.`id_lang` = '.(int)$id_lang.Shop::addSqlRestrictionOnLang('pl')
        );
        $sql->leftJoin('image_shop', 'image_shop', 'image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop='.(int)$context->shop->id);
        $sql->leftJoin('image_lang', 'il', 'image_shop.`id_image` = il.`id_image` AND il.`id_lang` = '.(int)$id_lang);
        $sql->leftJoin('manufacturer', 'm', 'm.`id_manufacturer` = p.`id_manufacturer`');
 
        //Gestion des filtres ( Pour l'exemple on ne gère que des catégories )
        if ( sizeof($applieds_filters)) {
            foreach ($applieds_filters as $applieds_filter) {
 
                if ( $applieds_filter == "") {
                    continue;
                }
 
                $catId = str_replace('cat-','',$applieds_filter);
                if ( $catId == 0 ) {
                    continue;
                }
                $sql->innerJoin('product','ps_cat_'.$catId,'p.id_product = ps_cat_'.$catId.'.id_product AND ps_cat_'.$catId.'.id_category_default = '.$catId);
            }
        }
 
        //Fin Gestion des filtres
 
        $sql->where('product_shop.`active` = 1');
        if ($front) {
            $sql->where('product_shop.`visibility` IN ("both", "catalog")');
        }
        $sql->where('product_shop.`date_add` > "'.date('Y-m-d', strtotime('-'.$nb_days_new_product.' DAY')).'"');
        if (Group::isFeatureActive()) {
            $groups = FrontController::getCurrentCustomerGroups();
            $sql->where('EXISTS(SELECT 1 FROM `'._DB_PREFIX_.'category_product` cp
				JOIN `'._DB_PREFIX_.'category_group` cg ON (cp.id_category = cg.id_category AND cg.`id_group` '.(count($groups) ? 'IN ('.implode(',', $groups).')' : '= 1').')
				WHERE cp.`id_product` = p.`id_product`)');
        }
 
        $sql->orderBy((isset($order_by_prefix) ? pSQL($order_by_prefix).'.' : '').'`'.pSQL($order_by).'` '.pSQL($order_way));
        $sql->limit($nb_products, (int)(($page_number-1) * $nb_products));
 
        if (Combination::isFeatureActive()) {
            $sql->select('product_attribute_shop.minimal_quantity AS product_attribute_minimal_quantity, IFNULL(product_attribute_shop.id_product_attribute,0) id_product_attribute');
            $sql->leftJoin('product_attribute_shop', 'product_attribute_shop', 'p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shop.id_shop='.(int)$context->shop->id);
        }
        $sql->join(Product::sqlStock('p', 0));
 
        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
 
        if (!$result) {
            return false;
        }
 
        if ($order_by == 'price') {
            Tools::orderbyPrice($result, $order_way);
        }
        $products_ids = array();
        foreach ($result as $row) {
            $products_ids[] = $row['id_product'];
        }
        // Thus you can avoid one query per product, because there will be only one query for all the products of the cart
        Product::cacheFrontFeatures($products_ids, $id_lang);
        return Product::getProductsProperties((int)$id_lang, $result);
    }
 
}
</pre>
<p>Une fois tout cela en place vous devriez avoir une navigation à facette sur les catégories des produits en place comme sur la capture ci-dessous.<br ?-->
Ce principe est applicable à toutes les pages prestashop étendant les listings.
Et il est également possible de créer des pages de listings personnalisées ( j'y reviendrais peut être dans un article ultérieurement )

Navigation à facettes prestashop

 

 

 

 
Pas de commentaires

Magento : Créer une commande via l’api

J’ai récemment eut à créer des commandes Magento via l’api et j’ai rencontré pas mal de difficultés, j’en profite pour condenser l’ensemble du processus dans cet article 🙂

Voici un script qui permettra de créer une commande Magento à partir de l’api Soap de Magento ( à la fois avec la version api V1 et api V2 )
Cet exemple a été réalisé sur un Magento 1.9.3.x de démonstration utilisant les samples data Magento.

N’hésitez pas à consulter la documentation officielle des api, si des points ne sont pas assez clairs  : https://devdocs.magento.com/guides/m1x/api/soap/introduction.html

Le script part des postulats suivants :

  • Passage de commande pour un client existant
  • Avec une adresse de facturation par défaut de renseignée
  • Les adresses de livraison et de facturation seront similaires.
  • Un accès à l’api avec tous les accès nécessaires existe sur le site magento de destination.

Les actions effectuées par le script sont les suivantes :

  1. Login à l’api
  2. Récupération de l’identifiant du  client à partir de son email
  3. Récupération de l’identifiant de l’adresse de facturation/livraison du client
  4. Création d’un panier
  5. Assignation du client au panier
  6. Assignation des adresses du client au panier
  7. Ajout de produits au panier
    1. Produit simple
    2. Produit configurable
    3. Produit groupé
    4. Produit Bundle
  8. Récupération des modes de livraison disponibles pour le panier
  9. Assignation du mode de livraison du panier
  10. Récupération des modes de paiement disponibles pour le panier
  11. Assignation du mode de paiement du panier
  12. Transformation panier en commande

 

Voir le script complet pour l’api V1
Voir le script complet pour l’api V2

Avant de vous montrer le script complet voici un focus sur les éléments à comprendre

Ajout des produits au panier

En fonction du type de produit à ajouter au panier , les informations à envoyer ne seront pas les mêmes.
Nous allons donc voir en détails comment ajouter les produits et récupérer les informations pour le faire

Produit simple :
L’ajout est rapide peut se faire en ciblant le produit via son identifiant Magento ou son SKU
Ceci fonctionne également pour les produits virtuels et téléchargeables.

 //Ajout d'un produit simple ( fonctionne aussi pour virtuel et téléchargeable )
	$simpleProductData = array(
		'sku' => 'acj005', //Sku du produit simple
                //'product_id' => 10 , Identifiant du produit en alternative
		'qty' => 2, //Qté du produit simple
	);

Produit configurable :
En complément des informations du produit simple, il est nécessaire d’implémenter le tableau “super_attribute” pour que l’ajout se fasse correctement.
Les valeurs qui doivent être renseignées sont sous la forme
AttributeID => OptionID

Attention à bien renseigner tous les attributs obligatoires pour que l’ajout fonctionne.

 //Ajout d'un produit configurable (ex sur démo magento bowery-chino-pants-546.html )
	$configurableProductData = array(
		'sku' => 'mpd003c' ,//Sku produit 
		'qty' => 1, //Qté du produit 
		'super_attribute' => array( //Champ spécifique au produit configurable
			92 => 17, //sous la forme attributeID => optionID
			180 => 67,
		),
	);

Il est possible de trouver ces informations directement sur le site en inspectant la source avec votre navigateur :

Les selects contiennent l’identifiant du super attribute dans leur nom, et la valeur est stockée dans l’option.

 

Produit Groupé :
En complément des informations du produit simple, il est nécessaire d’implémenter le tableau “super_group” pour que l’ajout se fasse correctement.
Les valeurs qui doivent être renseignées sont sous la forme
SuperGroupId => Qty

Attention à bien renseigner tous les attributs obligatoires pour que l’ajout fonctionne.

 //Ajout d'un produit groupé : pearl-necklace-set-test.html
	$groupedProductData = array(
		'sku' => 'acj007',
		'qty' => 1,
		'super_group' => array(
			547 => 1, //SuperGroupId => Qty
			548 => 2,
			551 => 1,	
		)
	);

Il est possible de trouver ces informations directement sur le site en inspectant la source avec votre navigateur :

Magento api grouped

Produit Bundle :
En complément des informations du produit simple, il est nécessaire d’implémenter les tableaux “bundle_option” et “bundle_qty” pour que l’ajout se fasse correctement.

 //Ajout d'un produit bundle mp3-player-with-audio.html
	$bundleProductData = array(
	  'product_id' => 446,
          'qty' => 1, //Quantité à 1 pour le bundle	  
	  'bundle_option' => array( 
	  21 => 84, //Quantité des options du panier sous la forme OptionID => SelectionID
	  22 => 86 ), 
	  'bundle_quantity' => array(
	  84 => 2, //Quantité de chaque option ajoutée au panier sous la forme SelectionID => Qté
	  86 => 2 
	  ), 
	);

Il est possible de trouver ces informations directement sur le site en inspectant la source avec votre navigateur :

Magento api bundle

 

Récupération des modes de paiement et de livraison

En fonction des paramètres de votre commande et selon la configuration de votre site, les modes de paiement et de livraison disponibles peuvent être amenés à changer.
C’est pourquoi je recommande l’utilisation des méthodes de l’api qui permettent de voir les modes disponibles avant de les assigner, les appels à faire étant les suivants :

//Récupération des modes de livraison disponibles pour la commande
$shippingMethods = $client->call($session, ‘cart_shipping.list, $shoppingCartId);
var_dump($shippingMethods);
//Récupération des modes de paiement disponibles pour la commande
$paymentMethods = $client->call($session, ‘cart_payment.list, $shoppingCartId);
var_dump($paymentMethods);

 

Pour finir voici donc le script complet pour l’API V1

 

<?php
error_reporting(E_ALL);
ini_set('display_errors','on');
echo '<pre>';
 
$adr = "http://magento-1933.demo/api/?wsdl";
$client = new SoapClient($adr,array('trace' => 1));
$apiUser = "testuser";
$apiKey = "test2018";
 
/**
* Création d'une commande Magento
* Via l'api Soap V1
*/
try {
 
//login à magento
$session = $client->login($apiUser, $apiKey);
 
//Récupération d'un client à partir de son email
$customerFilers = array(
'email' => array('eq' => '[email protected]')
);
$customersList = $client->call($session,'customer.list',array($customerFilers));
 
if ( $customersList ) {
$customerID = $customersList[0]['customer_id'];
} else {
exit('Erreur : Client non existant');
}
 
//Récupération de l'adresse de facturation par défaut du client
$addressList = $client->call($session, 'customer_address.list',$customerID);
 
if ( $addressList ) {
$addressId = false;
foreach ( $addressList as $address ) {
if ( $address['is_default_billing'] === true ){
$addressId = $address['customer_address_id'];
break;
}
}
} else {
exit("Erreur : Pas d'adresses pour ce client");
}
 
if ( !$addressId ) {
exit("Erreur : Pas d'adresse par défaut pour ce client");
}
 
//Création d'un panier ( initial )
$shoppingCartId= $client->call( $session, 'cart.create', array(1) );
 
//Assignation du client au panier
$assignCustomer = $client->call($session,'cart_customer.set',array($shoppingCartId,
array(
'mode' => 'customer',
'customer_id' => $customerID
)
)
);
//Assignation de l'adresse du client au panier , possibilité de définir une adresse de facturation et de livraison différente
$Adresses = $client->call($session, 'cart_customer.addresses',
array( $shoppingCartId ,
array(
array('mode' => 'billing','address_id' => $addressId),
array('mode' => 'shipping','address_id' => $addressId )
),
)
);
 
//Ajout d'un produit simple ( fonctionne aussi pour virtuel et téléchargeable )
$simpleProductData = array(
'sku' => 'acj005', //Sku du produit simple
'qty' => 2, //Qté du produit simple
);
 
//Ajout d'un produit configurable (ex sur démo magento bowery-chino-pants-546.html )
$configurableProductData = array(
'sku' => 'mpd003c' ,//Sku produit
'qty' => 1, //Qté du produit
'super_attribute' => array( //Champ spécifique au produit configurable
92 => 17, //sous la forme attributeID => optionID
180 => 67,
),
);
 
//Ajout d'un produit groupé
$groupedProductData = array(
'sku' => 'acj007',
'qty' => 1,
'super_group' => array(
547 => 1, //SuperGroupId => Qty
548 => 2,
551 => 1,
)
);
 
//Ajout d'un produit bundle mp3-player-with-audio.html
$bundleProductData = array(
'product_id' => 446,
'qty' => 1, //Quantité à 1 pour le bundle
'bundle_option' => array(
21 => 84,
22 => 86 ), //Quantité des options du panier sous la forme OptionID => SelectionID
'bundle_quantity' => array(
84 => 2,
86 => 2
), //Quantité de chaque option ajoutée au panier sous la forme SelectionID => Qté*/
);
 
//Assignation de produits au panier
$addProductToCart = $client->call($session,'cart_product.add',
array(
'quote_id' => $shoppingCartId,
array(
$simpleProductData,
$configurableProductData,
$groupedProductData,
$bundleProductData
),
)
);
 
//Récupération des modes de livraison disponibles pour la commande
$shippingMethods = $client->call($session, 'cart_shipping.list', $shoppingCartId);
 
//Décommenter la ligne suivante pour voir les méthodes de livraison disponibles
//var_dump($shippingMethods);
 
//Assignation d'un mode de livraison ( par défault on prends le freeshipping )
$assignShippingMethod = $client->call($session, 'cart_shipping.method', array($shoppingCartId,'freeshipping_freeshipping'));
 
//Récupération des modes de paiement disponibles pour la commande
$paymentMethods = $client->call($session, 'cart_payment.list', $shoppingCartId);
 
//Décommenter la ligne suivante pour voir les méthodes de paiement disponibles
//var_dump($paymentMethods);
 
//Assignation d'un mode de paiement : par défaut cashondelivery
$assignPaymenMethod = $client->call($session, 'cart_payment.method', array($shoppingCartId,array( 'method' => 'cashondelivery')));
 
//Transformation du paiement en panier
$transformOrder = $client->call($session, 'cart.order', array($shoppingCartId));
 
if ( $transformOrder ) {
echo 'Création de la commande '.$transformOrder.'<br />';
}
 
} catch ( Exception $e ) {
 
echo $e->getMessage();
 
}

Et le script pour l’API V2

 

<?php
error_reporting(E_ALL);
ini_set('display_errors','on');
echo '<pre>';
 
//API V2
$adr = "http://magento-1933.demo/api/?wsdl";
$client = new SoapClient($adr,array('trace' => 1));
$apiUser = "testuser";
$apiKey = "test2018";
 
/**
* Création d'une commande Magento
* Via l'api Soap V2
*/
try {
 
//login à magento
$session = $client->login($apiUser, $apiKey);
 
//Création d'un panier ( initial )
$shoppingCartId= $client->shoppingCartCreate( $session,1);
 
//Récupération d'un client à partir de son email
$customerFilers = array(
'email' => array('eq' => '[email protected]')
);
$customersList = $client->customerCustomerList($session,array($customerFilers));
 
if ( $customersList ) {
$customerID = $customersList[0]->customer_id;
} else {
exit('Erreur : Client non existant');
}
 
//Assignation d'un client au panier
$assignCustomer = $client->shoppingCartCustomerSet($session,$shoppingCartId,
array(
'mode' => 'customer',
'customer_id' => $customerID
)
);
 
//Récupération de l'adresse de facturation par défaut du client
$addressList = $client->customerAddressList($session,$customerID);
 
if ( $addressList ) {
$addressId = false;
foreach ( $addressList as $address ) {
if ( $address->is_default_billing === true ){
$addressId = $address->customer_address_id;
break;
}
}
} else {
exit("Erreur : Pas d'adresses pour ce client");
}
 
if ( !$addressId ) {
exit("Erreur : Pas d'adresse par défaut pour ce client");
}
 
//Assignation de cette adresse au panier
$Adresses = $client->shoppingCartCustomerAddresses($session, $shoppingCartId ,
array(
array('mode' => 'billing','address_id' => $addressId),
array('mode' => 'shipping','address_id' => $addressId )
)
);
 
//Ajout d'un produit simple ( fonctionne aussi pour virtuel et téléchargeable )
$simpleProductData = array(
'sku' => 'acj005', //Sku du produit simple
'qty' => 2, //Qté du produit simple
);
 
//Ajout d'un produit configurable (ex sur démo magento bowery-chino-pants-546.html )
$configurableProductData = array(
'sku' => 'mpd003c' ,//Sku produit
'qty' => 1, //Qté du produit
'super_attribute' => array( //Champ spécifique au produit configurable
92 => 17, //sous la forme attributeID => optionID
180 => 67,
),
);
 
//Ajout d'un produit groupé
$groupedProductData = array(
'sku' => 'acj007',
'qty' => 1,
'super_group' => array(
547 => 1, //SuperGroupId => Qty
548 => 2,
551 => 1,
)
);
 
//Ajout d'un produit bundle mp3-player-with-audio.html
$bundleProductData = array(
'product_id' => 446,
'qty' => 1, //Quantité à 1 pour le bundle
'bundle_option' => array(
21 => 84,
22 => 86 ), //Quantité des options du panier sous la forme OptionID => SelectionID
'bundle_quantity' => array(
84 => 2,
86 => 2
), //Quantité de chaque option ajoutée au panier sous la forme SelectionID => Qté*/
);
 
//Assignation de produits au panier
$addProductToCart = $client->shoppingCartProductAdd($session,$shoppingCartId,
array(
$simpleProductData,
$configurableProductData,
$groupedProductData,
$bundleProductData
)
);
 
//Récupération des modes de livraison disponibles pour la commande
$shippingMethods = $client->shoppingCartShippingList ($session,$shoppingCartId);
 
//Décommenter la ligne suivante pour voir les méthodes de livraison disponibles
//var_dump($shippingMethods);
 
//Assignation d'un mode de livraison ( par défault on prends le freeshipping )
$assignShippingMethod = $client->shoppingCartShippingMethod($session,$shoppingCartId,'freeshipping_freeshipping');
 
//Récupération des modes de paiement disponibles pour la commande
$paymentMethods = $client->shoppingCartPaymentList($session,$shoppingCartId);
 
//Décommenter la ligne suivante pour voir les méthodes de paiement disponibles
//var_dump($paymentMethods);
 
//Assignation d'un mode de paiement : par défaut cashondelivery
$assignPaymenMethod = $client->shoppingCartPaymentMethod($session,$shoppingCartId,array( 'method' => 'cashondelivery'));
 
//Transformation du paiement en panier
$transformOrder = $client->shoppingCartOrder($session,$shoppingCartId);
 
if ( $transformOrder ) {
echo 'Création de la commande '.$transformOrder.'<br />';
}
 
} catch ( Exception $e ) {
echo $e->getMessage();
}
Pas de commentaires

Prestashop : Ajouter un objet dans l’api

Nous allons voir comment ajouter un nouvel objet personnalisé dans l’api Prestashop via un module.
Cette méthodologie fonctionne sous Prestashop 1.6 et 1.7

L’objectif est d’ajouter de rendre un nouvel objet “Sample” qui pourra être manipulé via l’api.
Pour cela nous allons créer un module hhapisample

Ce module va ajouter une entité “sample” qui sera gérable via l’API.

Cette entité aura uniquement les paramètres suivants :

  • référence
  • nom ( multilingue )
  • description ( multilingue )

Voici le code de cette entité à placer dans le fichier classes/Sample.php du module.

/**
 * Classe d'exemple pour le webservice
 */
class Sample extends ObjectModel {
 
    /** @var string Référence du document */
    public $reference;
 
    /** @var string nom */
    public $name;
 
    /** @var string description */
    public $description;
 
    /**
     * Définition des paramètres de la classe
     */
    public static $definition = array(
        'table' => 'sample',
        'primary' => 'id_sample',
        'multilang' => true,
        'multilang_shop' => false,
        'fields' => array(
            'reference' => array('type' => self::TYPE_STRING, 'validate' => 'isCleanHtml', 'size' => 255),
            'name' => array('type' => self::TYPE_STRING, 'validate' => 'isCleanHtml', 'size' => 255 , 'lang' => true),
            'description' => array('type' => self::TYPE_STRING, 'validate' => 'isCleanHtml' , 'lang' => true),
        ),
    );
 
    /**
     * Mapping de la classe avec le webservice
     * 
     * @var type 
     */
    protected $webserviceParameters = [
        'objectsNodeName' => 'samples', //objectsNodeName doit être la valeur déclarée dans le hookAddWebserviceResources ( liste des entités )
        'objectNodeName' => 'sample', // Détail d'une entité
        'fields' => []
    ];
}

La définition du webservice est géré dans l’objet est configuré dans la variable $webserviceParameters
Pour rendre disponible cette entité, il faut greffer votre module sur le hook addWebserviceResources

/**
     * Ajout de la nouvelle entité au webservice
     * @param $params
     * @return array
     */
    public function hookAddWebserviceResources($params) {
        return [
            'samples' => [ //Nom du paramètre $webserviceParameters['objectsNodeName'] de la classe Objet
                'description' => 'Sample new entity for API',
                'class' => 'Sample'
            ],
        ];
    }

Une fois le module installé, vous pouvez-voir dans la liste des objets de l’api que l’entité sample est bien visible.

En vous rendant dans “Paramètres avancés / Webservice”
Il faut autoriser les utilisateurs api souhaités à accéder à l’entité pour pouvoir la modifier.
Api prestashop

Lors de l’appel à la page de l’api avec un utilisateur qui a les droits nécessaire nous pourrons voir les informations suivantes :

Api Prestashop sample

Notre nouvel objet est donc bien disponible via l’api et il est possible de le géré comme tous les autres 🙂

Voici le code complet du module

//Inclusion de la classe du nouveau modèle
include_once dirname(__FILE__) . '/classes/Sample.php';
 
/**
 *
 * Sample module to add custom object API
 */
class HhApiSample extends Module {
 
    public function __construct() {
        $this->name = 'hhapisample';
        $this->tab = 'others';
        $this->version = '0.1.0';
        $this->author = 'hhennes';
        $this->need_instance = 0;
        $this->bootstrap = true;
 
        parent::__construct();
 
        $this->displayName = $this->l('HH api Sample');
        $this->description = $this->l('Sample module to add custom entity API');
    }
 
    /**
     * Installation du module
     * @return bool
     */
    public function install() {
 
        if (!parent::install() 
            || !$this->registerHook('addWebserviceResources') 
            || !$this->installSql()
        ) {
            return false;
        }
 
        return true;
    }
 
    /**
     * Désinstallation du module
     * @return bool
     */
    public function uninstall() {
        if (!parent::uninstall() || !$this->uninstallSql()
        ) {
            return false;
        }
 
        return true;
    }
 
    /**
     * Install Sql
     * @return bool
     */
    protected function installSql() {
        //Création de la table de l'entité "Sample"
        $sql = "CREATE TABLE `" . _DB_PREFIX_ . "sample` (
                  `id_sample` int(11) unsigned NOT NULL AUTO_INCREMENT,
                  `reference` int(11) DEFAULT NULL,
                  PRIMARY KEY (`id_sample`)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8;";
 
        //Création de la table de langue de l'entité "Sample"
        $sqlLang = "CREATE TABLE `" . _DB_PREFIX_ . "sample_lang` (
                  `id_sample` int(11) unsigned NOT NULL AUTO_INCREMENT,
                  `id_lang` int(11) unsigned NOT NULL,
                  `name` varchar(255) DEFAULT NULL,
                  `description` TEXT DEFAULT NULL,
                  PRIMARY KEY (`id_sample`,`id_lang`)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8;";
 
        return Db::getInstance()->execute($sql) && Db::getInstance()->execute($sqlLang);
    }
 
    /**
     * Uninstall Sql
     * @return bool
     */
    protected function unInstallSql() {
        $sql = "DROP TABLE IF EXISTS " . _DB_PREFIX_ . "sample;"
                . "DROP TABLE IF EXISTS " . _DB_PREFIX_ . "sample_lang;";
        return Db::getInstance()->execute($sql);
    }
 
    /**
     * Ajout de la nouvelle entité au webservice
     * @Todo Vérifier les versions compatibles
     * @param $params
     * @return array
     */
    public function hookAddWebserviceResources($params) {
        return [
            'samples' => [
                'description' => 'Sample new entity for API',
                'class' => 'Sample'
            ],
        ];
    }
 
}
Pas de commentaires

Prestashop 1.7 : Utilisation des formfields

La version 1.7 rajoute une nouvelle notion pour gérer les champs sur certains formulaires front ( Client et Adresse entre autre )
Ceux-ci sont géré via la classe FormField et leur affichage est géré dans le template themes/themeName/templates/_partials/form-fields.tpl

Les méthodes utiles pour gérer ces champs sont les suivantes :

  • setName: Défini le nom du champ
  • setType: Défini le type du champ ( text/checkbox .. ) voir liste ci-dessous
  • setRequired : Défini si le champ est obligatoire ou non
  • setLabel : Défini le label du champ
  • setValue : Défini la valeur du champ
  • setAvailableValues : Défini les valeurs disponibles pour le champ, pour les select entre autre
  • addAvailableValue : Ajout une valeur disponible pour le champ
  • setMaxLength: Longueur maximum du champ
  • setConstraint : Ajoute une contrainte de validation au champ , les méthodes appelables sont celles de la classe Validation ( ex isEmail )

Pour l’exemple j’ai ajouté des nouveaux champs au formulaire de création de compte client via le hook hookAdditionalCustomerFormFields

Voici l’ensemble des  types de champs possibles :

return [
    //Champ texte standard
    (new FormField)
        ->setName('professionnal_id')
        ->setType('text')
        ->setRequired(true) //Décommenter pour rendre obligatoire
        ->setValue("TEST")
        ->setMaxLength("10")
        ->setLabel($this->l('Professionnal id')),
    //Champ File
    (new FormField)
        ->setName('justificatif_upload')
        ->setType('file')
        ->setLabel($this->l('document ID')),
    //Select
    (new FormField)
        ->setName('select_field')
        ->setType('select')
        ->setAvailableValues(array('key' => 'value 1', 'key2' => 'value2'))
        ->setLabel($this->l('Select type')),
    //countrySelect ( idem select mais rajoute une classe js js-country
    (new FormField)
        ->setName('country_field')
        ->setType('countrySelect')
        ->setAvailableValues(['key' => 'value 1', 'key2' => 'value2'])
        ->setLabel($this->l('Country Select')),
    //Checkbox
    (new FormField)
        ->setName('checkbox_field[]')
        ->setType('checkbox')
        ->setValue(1)
        ->setLabel($this->l('Checkbox type')),
    //radio-buttons
    (new FormField)
        ->setName('radio_field')
        ->setType('radio-buttons')
        ->setAvailableValues(array('key' => 'value 1', 'key2' => 'value2'))
        ->setLabel($this->l('Radio buttons type')),
    //date
    (new FormField)
        ->setName('date_field')
        ->setType('date')
        ->setLabel($this->l('Date')),
    //birthday
    (new FormField)
        ->setName('birthday_field')
        ->setType('birthday')
        ->setLabel($this->l('birthday')),
    //password
    (new FormField)
        ->setName('password_field')
        ->setType('password_')
        ->setLabel($this->l('password_')),
    //Champ email
    (new FormField)
        ->setName('email_field')
        ->setType('email')
        ->setLabel($this->l('email type')),
    //Champ tel
    (new FormField)
        ->setName('phone_field')
        ->setType('phone')
        ->setLabel($this->l('Phone type')),
    //Champ caché
    (new FormField)
        ->setName('hidden_field')
        ->setType('hidden')
        ->setValue('My hidden value')
];

Et leur visualisation :

Form fields prestashop

Pas de commentaires

Prestashop 1.7 : Récupérer des informations personnalisées dans le panier

J’ai récemment fait face à un problème, car je souhaitais remontée un champ produit spécifique dans les informations du panier sur prestashop 1.7
( La modification décrite fonctionne également sur prestashop 1.6.1.x )

Après analyse, il s’avère que les informations disponibles sont remontées depuis la fonction getProducts de la classe Cart.

L’ensemble des champs remontés sont listé dans la requête sql de remontée des produits, et il n’est pas possible de récupérer nos nouveaux champs.

// Build query
$sql = new DbQuery();
 
// Build SELECT
$sql->select('cp.`id_product_attribute`, cp.`id_product`, cp.`quantity` AS cart_quantity, cp.id_shop, cp.`id_customization`, pl.`name`, p.`is_virtual`,
pl.`description_short`, pl.`available_now`, pl.`available_later`, product_shop.`id_category_default`, p.`id_supplier`,
p.`id_manufacturer`, m.`name` AS manufacturer_name, product_shop.`on_sale`, product_shop.`ecotax`, product_shop.`additional_shipping_cost`,
product_shop.`available_for_order`, product_shop.`show_price`, product_shop.`price`, product_shop.`active`, product_shop.`unity`, product_shop.`unit_price_ratio`,
stock.`quantity` AS quantity_available, p.`width`, p.`height`, p.`depth`, stock.`out_of_stock`, p.`weight`,
p.`available_date`, p.`date_add`, p.`date_upd`, IFNULL(stock.quantity, 0) as quantity, pl.`link_rewrite`, cl.`link_rewrite` AS category,
CONCAT(LPAD(cp.`id_product`, 10, 0), LPAD(IFNULL(cp.`id_product_attribute`, 0), 10, 0), IFNULL(cp.`id_address_delivery`, 0), IFNULL(cp.`id_customization`, 0)) AS unique_id, cp.id_address_delivery,
product_shop.advanced_stock_management, ps.product_supplier_reference supplier_reference');
 
// Build FROM
$sql->from('cart_product', 'cp');
 
...

Vous pouvez consulter le fichier complet ici : https://github.com/PrestaShop/PrestaShop/blob/1.7.3.x/classes/Cart.php#605

La bonne nouvelle c’est que la requête est effectuée via un objet Dbquery, ce qui va nous permettre d’interférer assez facilement avec la requête.

Pour ajouter une fonctionnalité de modification de cette requête nous allons donc créer une surcharge de la classe Cart qui va reprendre la function getProducts de la classe parente et rajouter le contenu suivant :

// Nouveau hook pour pouvoir modifier la requête de récupération des produits
Hook::exec('actionCartGetProductsSqlQuery',['query' => &$sql]);

juste avant l’exécution de la requête dans la ligne

$result = Db::getInstance()->executeS($sql);

Via ce hook nouvellement créé auquel l’instance de l’objet DqQuery est passé en paramètre, il est à présent possible de faire un peu prêt tout sur cette requête.
La variable étant passée par référence, les modifications sur la requête sont appliquées directement.

Pour ajouter un champ il suffira donc de créer un module et de le greffer sur le hook actionCartGetProductsSqlQuery

/**
 * Nouveau Hook pour modifier la requête de sélection des produits du panier
 * Cf. override du cart
 * @param array $params => 'query' => &DbQuery
 */
public function hookActionCartGetProductsSqlQuery($params)
{
    $params['query']->select('p.marque');
}
Pas de commentaires
Magento certified developper
Modules Prestashop
Compte Github