Après avoir comparer Ibexa Commerce à Sylius, entrons maintenant plus dans les détails techniques.
Comme pour Ibexa DXP, vous avez besoin d'une clé d'autorisation et d'un token pour vous authentifier sur les serveurs d'Ibexa et télécharger les dépendances.
L'installation est similaire à tout projet initialisé avec composer . Les commandes post-installation fournies dans la documentation fonctionnent bien pour MariaDB.
Par défaut, tous les projets sont installés avec une configuration par défaut de Symfony et Docker pour l'utilisation de PostgreSQL. Comme j'utilise Docker sur mon poste de développement, j'ai du modifié la configuration Docker Compose et Symfony ( .env ) pour passer de PostgreSQL à MariaDB.
Entrons directement dans la façon utilisée pour stocker les données dans le module commerce. Les choix réalisés par les équipes d'Ibexa sont simples : utiliser la même recette que pour les contenus. Nous trouvons donc le "Product Type" dans les mêmes tables que les "Content Type" et les "Products" sont dans les mêmes tables que les "Contents".
Les attributs, qu'il est possible d'ajouter à un type de produit, sont considérés comme un type de champs personnalisés pour le stockage.
A part cela, il y a de nouvelles tables donc voici celles qui ont retenues mon attention :
Maintenant, essayons de mettre en place le transporteur dans notre instance d'Ibexa Commerce en suivant la documentation.
Avant de commencer, nous avons besoin de définir le format des tranches de poids avec le prix correspondant.
Pour faire simple, les poids sont notés en gramme et les prix en centime. Le poids et le prix sont séparés par un deux-points ":" et les différents prix sont séparés par un point-virgule ";". Enfin, les poids doivent être dans l'ordre croissant pour faciliter l'utilisation de la liste.
Voici donc la valeur à stocker (ce n'est pas la vrai grille de prix de la poste) : 125:50;250:120;1000:1280;5000:2850
Nous avons également besoin de connaitre la monnaie utilisée dans la grille.
Voilà pour les données que nous gérons avec le transporteur. Nous avons besoin que tous les produits transportables aient une propriété "poids" (weight en anglais) de type "Measurement (single)".
La première chose réalisée est l'ajout d'un type de transporteur. Cette première étape définie certaines choses pour toute la suite.
services:
app.shipping.shipping_method_type.custom:
class: Ibexa\Shipping\ShippingMethod\ShippingMethodType
arguments:
$identifier: 'custom'
tags:
- name: ibexa.shipping.shipping_method_type
alias: custom
Le terme "custom" doit être remplacé par votre type personnalisé. Ce terme sera utilisé par la suite pour toutes les configurations. Dans notre exemple, j'ai choisi "grid" car c'est un transporteur utilisant une grille de prix.
La prochaine subtilité est au niveau du formulaire qui sera utilisé pour la configuration du transporteur.
Le type de champ à utiliser pour la monnaie est Ibexa\Bundle\ProductCatalog\Form\Type\CurrencyChoiceType . Mais pour éviter les erreurs de conversion de type, il faut utiliser le transformer Ibexa\Bundle\Shipping\Form\DataTransformer\CurrencyTransformer .
Voici la classe complète :
<?php
declare(strict_types=1);
namespace App\ShippingMethodType\Form\Type;
use Ibexa\Bundle\ProductCatalog\Form\Type\CurrencyChoiceType;
use Ibexa\Bundle\Shipping\Form\DataTransformer\CurrencyTransformer;
use Ibexa\Contracts\ProductCatalog\CurrencyServiceInterface;
use Ibexa\Contracts\ProductCatalog\Values\Currency\Query\Criterion\IsCurrencyEnabledCriterion;
use JMS\TranslationBundle\Translation\TranslationContainerInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use JMS\TranslationBundle\Model\Message;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class GridShippingMethodOptionsType extends AbstractType implements TranslationContainerInterface
{
private CurrencyServiceInterface $currencyService;
public function __construct(CurrencyServiceInterface $currencyService)
{
$this->currencyService = $currencyService;
}
public function getBlockPrefix(): string
{
return 'ibexa_shipping_method_grid';
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('price_grid', TextType::class);
$builder->add('currency', CurrencyChoiceType::class, [
'criterion' => new IsCurrencyEnabledCriterion(),
'required' => true,
'disabled' => $options['translation_mode'],
]);
$builder->get('currency')
->addModelTransformer(new CurrencyTransformer($this->currencyService));
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'translation_domain' => 'ibexa_shipping',
'translation_mode' => false,
]);
$resolver->setAllowedTypes('translation_mode', 'bool');
}
public static function getTranslationMessages(): array
{
return [
Message::create('ibexa.shipping_types.grid.name', 'ibexa_shipping')->setDesc('Grid'),
];
}
}
Après avoir demandé des informations à l'utilisateur, il est obligatoire de valider que la saisie est correcte.
Avec ce service Symfony, j'ai vérifié que la grille des poids et des prix est cohérente et ne contient pas de caractères étranges.
Voici une implémentation simple de la vérification :
public function validateOptions(OptionsBag $options): array
{
$priceGrid = $options->get('price_grid');
if ($priceGrid === null) {
return [
new OptionsValidatorError('[price_grid]', self::MESSAGE),
];
}
$errors = [];
$ranges = explode(';', $priceGrid);
foreach ($ranges as $key => $range) {
if (empty($range)) {
$errors[] = new OptionsValidatorError(
'[price_grid]',
'Empty range is not allowed (index: ' . ($key + 1) . ')'
);
continue;
}
if (str_contains($range, ':') === false) {
$errors[] = new OptionsValidatorError(
'[price_grid]',
'Invalid range format. Use ":" to separate weight and price (index: ' . ($key + 1) . ')'
);
continue;
}
[$weight, $price] = explode(':', $range);
if (!preg_match('/^[0-9]+$/', $weight)) {
$errors[] = new OptionsValidatorError(
'[price_grid]',
'Invalid weight format. Type the weight in gramme (index: ' . ($key + 1) . ', wrong value: '.$weight.')'
);
}
if (!preg_match('/^[0-9]+$/', $price)) {
$errors[] = new OptionsValidatorError(
'[price_grid]',
'Invalid price format. Type the price in cent (index: ' . ($key + 1) . ', wrong value: '.$price.')'
);
}
}
return $errors;
}
Pour le stockage, j'ai personnalisé les objets pour correspondre à ce schéma de base de données:
create table ibexa_shipping_method_region_grid
(
id int auto_increment primary key,
price_grid text not null,
currency_id int not null,
shipping_method_region_id int not null
);
Le fichier de définition de schéma :
<?php
declare(strict_types=1);
namespace App\ShippingMethodType\Storage;
use Doctrine\DBAL\Types\Types;
use Ibexa\Contracts\Shipping\Local\ShippingMethod\StorageDefinitionInterface;
use Ibexa\Shipping\Persistence\Legacy\ShippingMethod\AbstractOptionsStorageSchema;
final class StorageDefinition implements StorageDefinitionInterface
{
public function getColumns(): array
{
return [
AbstractOptionsStorageSchema::COLUMN_SHIPPING_METHOD_REGION_ID => Types::INTEGER,
StorageSchema::COLUMN_PRICE_GRID => Types::STRING,
StorageSchema::COLUMN_CURRENCY => Types::INTEGER,
];
}
public function getTableName(): string
{
return StorageSchema::TABLE_NAME;
}
}
Voici la classe de conversion des données entre la base de données et l'objet configuration du transporteur :
<?php
declare(strict_types=1);
namespace App\ShippingMethodType\Storage;
use Ibexa\Contracts\Shipping\Local\ShippingMethod\StorageConverterInterface;
final class StorageConverter implements StorageConverterInterface
{
public function fromPersistence(array $data)
{
$value['price_grid'] = $data['price_grid'];
$value['currency'] = $data['currency_id']??null;
return $value;
}
public function toPersistence($value): array
{
return [
StorageSchema::COLUMN_PRICE_GRID => $value['price_grid'],
StorageSchema::COLUMN_CURRENCY => $value['currency'],
];
}
}
Toutes les constantes sont définies dans le fichier src/ShippingMethodType/Storage/StorageSchema.php et contiennent les noms de la table et de la colonne présentes dans la base de données.
Pour mon test, le voter active systématiquement le transporteur. Mais il est intéressant de calculer à ce moment si le poids total de la commande n'est pas supérieur au poids maximum de la grille de tarif.
En cas de dépassement, le transporteur ne sera pas sélectionnable lors du choix du transporteur et du moyen de paiement.
Une autre façon de faire est de diviser le poids du colis par le poids maximum du transporteur. En arrondissant au supérieur cela donne une idée du nombre de colis.
Multiplier le prix de la dernière tranche par le nombre de colis donne un prix.
Ce calcul n'est valable que si vous savez qu'aucun produit n'a un poids supérieur au poids maximum du transporteur. Il existe également d'autre possibilité de calcul que je vous laisse imaginer.
Lorsque vous ajoutez un transporteur, il faut pouvoir afficher la grille de prix de façon plus lisible pour les administrateurs.
Voici le code utilisé pour transformer la grille:
<?php
declare(strict_types=1);
namespace App\ShippingMethodType\Formatter;
use Ibexa\Contracts\Shipping\ShippingMethod\CostFormatterInterface;
use Ibexa\Contracts\Shipping\Value\ShippingMethod\ShippingMethodInterface;
final class GridCostFormatter implements CostFormatterInterface
{
public function formatCost(ShippingMethodInterface $shippingMethod, array $parameters = []): ?string
{
$listPrices = explode(';', $shippingMethod->getOptions()->get('price_grid') ?? '');
$listPrices = array_map(function ($item) {
[$weight, $price] = explode(':', $item);
$weight = (int)$weight / 100;
$price = (int)$price / 100;
return sprintf('%0.2f kg => %0.2f €', $weight, $price);
}, $listPrices);
return implode(' ; ', $listPrices);
}
}
Et c'est tout ! Avez-vous la même sensation que moi ?
La documentation est terminée et pourtant à aucun moment nous utilisons la grille pour calculer le prix d'une commande. Voyons comment ajouter ce service.
La première chose est l'ajout d'une classe PHP qui implémente l'interface "Ibexa\Contracts\Shipping\ShippingMethod\CostCalculatorInterface ".
C'est cette classe qui recevra le transporteur avec sa configuration et la commande pour laquelle il est nécessaire de calculer un prix de transport.
Voici mon implémentation :
<?php
declare(strict_types=1);
namespace App\ShippingMethodType\Calculator;
use Ibexa\Contracts\Cart\Value\CartInterface;
use Ibexa\Contracts\Measurement\Value\SimpleValueInterface;
use Ibexa\Contracts\ProductCatalog\CurrencyServiceInterface;
use Ibexa\Contracts\Shipping\ShippingMethod\CostCalculatorInterface;
use Ibexa\Contracts\Shipping\Value\ShippingMethod\ShippingMethodInterface;
use Ibexa\ProductCatalog\Money\DecimalMoneyFactory;
use Money\Currency;
use Money\Money;
final class GridCalculator implements CostCalculatorInterface
{
private DecimalMoneyFactory $decimalMoneyFactory;
private CurrencyServiceInterface $currencyService;
public function __construct(
DecimalMoneyFactory $decimalMoneyFactory,
CurrencyServiceInterface $currencyService
) {
$this->decimalMoneyFactory = $decimalMoneyFactory;
$this->currencyService = $currencyService;
}
public function calculate(ShippingMethodInterface $method, CartInterface $cart): Money
{
$listPrices = explode(';', $method->getOptions()->get('price_grid') ?? '');
$prices = [];
foreach ($listPrices as $item) {
[$weight, $price] = explode(':', $item);
$prices[$weight] = number_format($price / 100, 2, '.', '');
}
$currencyId = $method->getOptions()->get('currency');
$currency = $this->currencyService->getCurrency($currencyId);
$weight = 0;
foreach ($cart->getEntries() as $entry) {
foreach ($entry->getProduct()->getAttributes() as $key => $attribute) {
if ($key === 'weight') {
$value = $attribute->getValue();
if ($value instanceof SimpleValueInterface === false) {
continue;
}
$weight += ($value->getValue() * $entry->getQuantity());
}
}
}
$price = "500.00";
foreach ($prices as $maxWeight => $levelPrice) {
if ($maxWeight < $weight) {
continue;
}
$price = $levelPrice;
}
return $this->decimalMoneyFactory->getMoneyParser()->parse(
$price,
new Currency($currency->getCode())
);
}
}
Si vous avez l’œil, vous aurez remarqué que le prix du transport en cas de dépassement de la grille est de 500 €.
Il reste une étape. La configuration du service dans le fichier service.
services:
App\ShippingMethodType\Calculator\GridCalculator:
tags:
- { name: 'ibexa.shipping.shipping.cost_calculator', method: 'grid' }
Et voilà le résultat dans le tunnel de commande:
L'ajout d'un transporteur est une procédure un peu longue mais la documentation est bien fournie et presque complète.
Pour ma part, j'aurais préféré l’implémentation d'un tag automatique sur les interfaces avec une fonction statique dans le service pour obtenir le type de transporteur. Cela simplifie la configuration tant que le service est utilisé pour un seul type de transporteur.
L'ajout de ce type de transporteur a mis en évidence que pour Ibexa Commerce, il n'est pas possible de gérer le multi-colis pour une commande. Il n'est pas possible de gérer un numéro de colis pour ajouter un lien de suivi dans l'espace client. Cette fonctionnalité même si elle est moins utile pour les professionnels reste intéressante à ajouter dans votre projet.
How to dynamically calculate shipping costs? The official documentation that explains how to create shipping ...
How to create a new product attribute type in Ibexa Commerce
A la recherche d'un poste de travail temporaire ou permanent ? Vous recherchez un environnement ...
Après une découverte de surface d'Ibexa Commerce, entrons plus dans le détail pour comprendre son ...
Ibexa DXP propose un module pour gérer des produits pour la réalisation d'un site e-commerce. ...
Voici une présentation d'IbexaMailing, un module qui ajoute la gestion des newsletters à Ibexa. IbexaMailing est ...
C'est la dernière occasion de vous souhaitez le meilleur pour cette année 2024 et surtout ...
En ce début d'année, en ce mois de janvier, mois des grandes résolutions, dépensons moins!Prenez ...
Nous sommes très heureux et fiers d'être nominés aux Ibexa Partner Excellence Awards 🏆 dans ...
How to dynamically calculate shipping costs? The official documentation that explains how to create shipping ...
How to create a new product attribute type in Ibexa Commerce
A la recherche d'un poste de travail temporaire ou permanent ? Vous recherchez un environnement ...
Après une découverte de surface d'Ibexa Commerce, entrons plus dans le détail pour comprendre son ...
Ibexa DXP propose un module pour gérer des produits pour la réalisation d'un site e-commerce. ...
Voici une présentation d'IbexaMailing, un module qui ajoute la gestion des newsletters à Ibexa. IbexaMailing est ...
C'est la dernière occasion de vous souhaitez le meilleur pour cette année 2024 et surtout ...
En ce début d'année, en ce mois de janvier, mois des grandes résolutions, dépensons moins!Prenez ...
Nous sommes très heureux et fiers d'être nominés aux Ibexa Partner Excellence Awards 🏆 dans ...