Sylius est un framework PHP basé sur Symfony qui offre des fonctionnalités E-Commerce. En plein essor depuis plus de 5 ans maintenant, la technologie grandit et sa communauté avec !
Sylius se démarque notamment de ses principaux concurrents (Magento, Prestashop…) par sa grande adaptabilité aux besoins métiers spécifiques. A contrario, une plus grande souplesse implique une montée en compétence plus ardue et des connaissances solides.
Integral Service développe des sites E-Commerce avec Sylius depuis maintenant 5 ans. Nous avons vu grandir la technologie, depuis l’alpha jusqu'à aujourd'hui. A notre actif, on retrouve des plateformes E-Commerce automatisées avec différents ERP (Gestimum, Wavesoft, Business Central, Sage X3, Divalto, ...) mais aussi des rendus 3D, ou des configurateurs de produits !
Dans cet article, nous souhaitons vous proposer une méthode exhaustive afin de développer un mode Catalogue complètement dynamique dans votre projet Sylius. Le mode catalogue désigne le fait d’avoir la fonctionnalité de consultation des produits, sans les fonctions de tunnel de commande et d’achat.
Nous commencerons par la préparation de l’activation du mode catalogue dans l’interface d’administration. Ensuite nous expliquerons comment restreindre l’accès au tunnel de commande. Et enfin nous terminerons par l’affichage conditionnel des différentes informations liées à l’achat en ligne.
Ce guide de développement requiert un minimum de connaissances du framework Symfony ainsi que de Sylius. Le code présenté dans ce guide est tiré d’un projet exemple basé sur Sylius v1.11.7 et Symfony v5.4.12.
Ajout du mode catalogue à l’interface d’administration des données
La première chose à déterminer est l’endroit ou l’on veut faire apparaître l’option “Mode Catalogue” dans notre interface d’administration.
Sylius introduit le concept de canaux (Channel), qui permet de gérer plusieurs boutiques avec une seule interface d’administration, c’est justement ici que l’on va venir se brancher ! Ainsi vous pourrez activer ou désactiver le mode catalogue pour chaque canal de votre application.
Pour ajouter un nouveau paramètre à nos canaux, il nous faut ajouter un champ dans l’entité Channel de Sylius.
Pour plus de détails sur la customisation des modèles : https://docs.sylius.com/en/1.12/customization/model.html
Ajouter catalogMode dans l’entité Channel
D’abord, on vient ajouter l’attribut booléen catalogMode dans l’entité Channel.
//src/Entity/Channel/Channel.php
class Channel extends BaseChannel
{
/**
* @ORM\Column(name="catalog_mode", type="boolean", nullable=false, options={"default" : false})
*/
private bool $catalogMode = false;
/**
* @return bool
*/
public function getCatalogMode(): bool
{
return $this->catalogMode;
}
/**
* @param bool $catalogMode
* @return Channel
*/
public function setCatalogMode(bool $catalogMode): Channel
{
$this->catalogMode = $catalogMode;
return $this;
}
}
Ensuite on génère la migration associée.
$ php bin/console doctrine:migration:diff
Puis on l’exécute.
$ php bin/console doctrine:migration:migrate
//src/Migrations/VersionXXXXXXXXX.php
final class Version20220802073139 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE sylius_channel ADD catalog_mode TINYINT(1) DEFAULT \'0\' NOT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE sylius_channel DROP catalog_mode');
}
}
Ça y est ! On a ajouté notre attribut, maintenant il faut mettre à jour le formulaire des Channel pour pouvoir modifier sa valeur dans l’interface d’administration.
Ajouter le champ dans le formulaire Channel
Symfony introduit le principe d’extension de formulaire et Sylius le reprend afin de permettre aux développeurs de modifier librement les formulaires du framework !
Pour plus de détails sur la customisation des formulaires : https://docs.sylius.com/en/1.12/customization/form.html
D’abord il faut créer l’extension de formulaire qui va nous permettre d’afficher notre champ, fraîchement ajouté.
//src/Form/Extension/ChannelTypeExtension.php
final class ChannelTypeExtension extends AbstractTypeExtension
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('catalogueMode', CheckboxType::class, [
'label' => 'channel.form.label.catalog_mode',
'required' => false,
]);
}
public static function getExtendedTypes(): iterable
{
return [ChannelType::class];
}
}
Une fois le fichier créé, il faut le déclarer dans la configuration des services de l’application.
#config/services.yaml
app.form.extension.type.channel:
class: App\Form\Extension\ChannelTypeExtension
tags:
- { name: form.type_extension, extended_type: Sylius\Bundle\ChannelBundle\Form\Type\ChannelType }
Enfin, il ne reste plus qu'à modifier le template d’affichage du formulaire pour y inclure notre champ custom.
{# templates/bundles/SyliusAdminBundle/Channel/Form/_lookAndFeel.html.twig #}
<div class="ui hidden divider"></div>
<h4 class="ui top attached large header">{{ 'sylius.ui.look_and_feel'|trans }}</h4>
<div class="ui attached segment">
{{ form_row(form.themeName) }}
</div>
<div class="ui attached segment">
{{ form_row(form.locales) }}
{{ form_row(form.defaultLocale) }}
</div>
<div class="ui attached segment">
{{ form_row(form.menuTaxon) }}
</div>
<div class="ui hidden divider"></div>
<div class="ui attached segment">
{{ form_row(form.catalogueMode) }}
{{ form_row(form.skippingShippingStepAllowed) }}
{{ form_row(form.skippingPaymentStepAllowed) }}
{{ form_row(form.accountVerificationRequired) }}
</div>
Et voilà ! nous avons un formulaire avec un champ custom prêt à l’emploi qui nous permet d’activer ou de désactiver le mode catalogue.
Restriction de l’accès au tunnel de commande
L’intérêt d’un mode catalogue est de masquer les fonctionnalités d’achat par défaut de Sylius pour ne proposer qu’un catalogue vitrine en ligne. Il nous faut donc restreindre l’accès au tunnel de commande de Sylius ainsi qu’à la page “Panier”. Il faut cependant être vigilant sur l’accès via API des différentes fonctionnalités mentionnées. Pour cela nous proposerons un blocage métier (avec le Processor) ainsi qu’un blocage d’accès aux pages avec un contrôleur agissant comme une sorte de pare-feu.
Extension de l’OrderProcessor
Sylius introduit le concept d’OrderProcessor. Il s’agit d’un composant qui est appelé à plusieurs étapes du processus de commande et qui va servir à appliquer la logique de calcul liée aux commandes (les différentes taxes, promotions, sélection des méthodes de livraison…).
Schéma de l’OrderProcessor par défaut de Sylius Source : https://docs.sylius.com/en/1.12/book/orders/orders.html
Dans le schéma ci-dessus, on peut voir qu’en réalité l’OrderProcessor est composé de plusieurs Processors, exécutés dans l’ordre décroissant de leur priorité. L’idée pour notre mode catalogue va être de créer un nouveau Processor avec la plus haute priorité afin de vérifier si le canal courant est en mode catalogue ou non. Si oui, alors nous lèverons une exception de type NotFound pour générer une erreur 404.
//src/Services/OrderProcessor/OrderCatalogProcessor.php
final class OrderCatalogProcessor implements OrderProcessorInterface
{
private ChannelContextInterface $channelContext;
public function __construct(ChannelContextInterface $channelContext)
{
$this->channelContext = $channelContext;
}
public function process(OrderInterface $order): void
{
if ($this->channelContext->getChannel()->getCatalogMode()) {
throw new NotFoundHttpException();
}
}
}
Ensuite, il suffit de configurer le Processor pour l’activer.
#config/services.yaml
app.service.order_processor.order_catalog_processor:
class: App\Service\OrderProcessor\OrderCatalogProcessor
tags:
- { name: sylius.order_processor, priority: 70 }
La surcharge de la logique de l'OrderProcessor est donc terminée.
Création d’un contrôleur pour vérifier et rediriger les requêtes vers les pages interdites
Désormais nous avons restreint les logiques métier liées au tunnel de commande de notre application Sylius. Néanmoins, les utilisateurs pourront toujours accéder aux pages panier via une requête HTTP classique (avec l’url de la page par exemple).
L’idée va être de créer un contrôleur personnalisé qui va vérifier si le canal courant est en mode catalogue ou non, puis laisser passer les requêtes ou renvoyer une erreur.
//src/Controller/CatalogController.php
class CatalogController extends AbstractController
{
private ChannelContextInterface $channelContext;
public function __construct(ChannelContextInterface $channelContext)
{
$this->channelContext = $channelContext;
}
public function checkCatalogMode(Request $request): Response
{
if ($this->channelContext->getChannel()->getCatalogMode()) {
throw new NotFoundHttpException();
}
$redirectController = $request->get('_redirect_controller');
if (empty($redirectController)) {
throw new MissingMandatoryParametersException('request is missing _redirect_controller parameter');
}
return $this->forward($redirectController, $request->attributes->all());
}
}
Enfin, il nous suffit de surcharger les routes définies par Sylius pour l’accès au tunnel de commande ainsi qu’à la page panier.
#config/routes/catalog_mode.yaml
sylius_shop_cart_summary:
path: /{_locale}/cart
methods: [GET]
defaults:
_controller: App\Controller\CatalogController:checkCatalogMode
_redirect_controller: sylius.controller.order:summaryAction
_sylius:
template: "@SyliusShop/Cart/summary.html.twig"
form: Sylius\Bundle\OrderBundle\Form\Type\CartType
sylius_shop_checkout_address:
path: /{_locale}/checkout/address
methods: [GET, PUT]
defaults:
_controller: App\Controller\CatalogController:checkCatalogMode
_redirect_controller: sylius.controller.order:updateAction
_sylius:
event: address
flash: false
template: "@SyliusShop/Checkout/address.html.twig"
form:
type: Sylius\Bundle\CoreBundle\Form\Type\Checkout\AddressType
options:
customer: expr:service('sylius.context.customer').getCustomer()
repository:
method: findCartForAddressing
arguments:
- "expr:service('sylius.context.cart').getCart().getId()"
state_machine:
graph: sylius_order_checkout
transition: address
sylius_shop_checkout_select_shipping:
path: /{_locale}/checkout/select-shipping
methods: [GET, PUT]
defaults:
_controller: App\Controller\CatalogController:checkCatalogMode
_redirect_controller: sylius.controller.order:updateAction
_sylius:
event: select_shipping
flash: false
template: "@SyliusShop/Checkout/selectShipping.html.twig"
form: Sylius\Bundle\CoreBundle\Form\Type\Checkout\SelectShippingType
repository:
method: findCartForSelectingShipping
arguments:
- "expr:service('sylius.context.cart').getCart().getId()"
state_machine:
graph: sylius_order_checkout
transition: select_shipping
sylius_shop_checkout_select_payment:
path: /{_locale}/checkout/select-payment
methods: [GET, PUT]
defaults:
_controller: App\Controller\CatalogController:checkCatalogMode
_redirect_controller: sylius.controller.order:updateAction
_sylius:
event: payment
flash: false
template: "@SyliusShop/Checkout/selectPayment.html.twig"
form: Sylius\Bundle\CoreBundle\Form\Type\Checkout\SelectPaymentType
repository:
method: findCartForSelectingPayment
arguments:
- "expr:service('sylius.context.cart').getCart().getId()"
state_machine:
graph: sylius_order_checkout
transition: select_payment
sylius_shop_checkout_complete:
path: /{_locale}/checkout/complete
methods: [GET, PUT]
defaults:
_controller: App\Controller\CatalogController:checkCatalogMode
_redirect_controller: sylius.controller.order:updateAction
_sylius:
event: complete
flash: false
template: "@SyliusShop/Checkout/complete.html.twig"
repository:
method: findCartForSummary
arguments:
- "expr:service('sylius.context.cart').getCart().getId()"
state_machine:
graph: sylius_order_checkout
transition: complete
redirect:
route: sylius_shop_order_pay
parameters:
tokenValue: resource.tokenValue
form:
type: Sylius\Bundle\CoreBundle\Form\Type\Checkout\CompleteType
options:
validation_groups: 'sylius_checkout_complete'
#src/routes.yaml
sylius_catalog_mode:
resource: "./routes/catalog_mode.yml"
Désormais l’accès aux pages et aux fonctionnalités du processus de commande est totalement restreint. Cependant, dans un projet Sylius, lorsqu’un utilisateur se connecte à son compte, le panier en cours de la session est fusionné avec celui enregistré pour cet utilisateur. Lorsque le mode catalogue est activé, nous ne voulons pas effectuer ce traitement. Il faut alors surcharger l’évènement correspondant afin de conditionner cette fonctionnalité.
//src/EventListener/UserCartRecalculationListenerDecorator.php
final class UserCartRecalculationListenerDecorator
{
public function __construct(
private CartContextInterface $cartContext,
private OrderProcessorInterface $orderProcessor,
private SectionProviderInterface $uriBasedSectionContext,
private ChannelContextInterface $channelContext
) {}
/**
* @param InteractiveLoginEvent|UserEvent $event
*/
public function recalculateCartWhileLogin(object $event): void
{
if ($this->channelContext->getChannel()->getCatalogMode()) {
return;
}
if (!$this->uriBasedSectionContext->getSection() instanceof ShopSection) {
return;
}
/** @psalm-suppress DocblockTypeContradiction */
if (!$event instanceof InteractiveLoginEvent && !$event instanceof UserEvent) {
throw new \TypeError(sprintf(
'$event needs to be an instance of "%s" or "%s"',
InteractiveLoginEvent::class,
UserEvent::class
));
}
try {
$cart = $this->cartContext->getCart();
} catch (CartNotFoundException) {
return;
}
Assert::isInstanceOf($cart, OrderInterface::class);
$this->orderProcessor->process($cart);
}
}
Affichage conditionnel des prix et autres informations d’achat
Toutes les fonctionnalités et pages ayant été bloquées, il ne nous reste désormais plus qu’à conditionner l’affichage des boutons d’ajouts au panier et de toute autre information que vous ne souhaitez pas afficher en mode catalogue. L’objet channel est injecté automatiquement par Sylius dans les différentes pages publiques du site (dans le Shop). Il est donc très simple de conditionner un affichage avec Twig.
{# theme/MonTheme/bundles/SyliusShopBundle/Product/Show/_priceWidget.html.twig #}
…
{% if not sylius.channel.catalogMode and not product.variants.empty() %}
{% include '@SyliusShop/Product/Show/_price.html.twig' %}
{% endif %}
…
Néanmoins, pour l’interface d’administration de Sylius, les différents formulaires liés aux canaux peuvent être optimisés pour obtenir une meilleure UX. Pour cela, nous allons créer une extension twig qui nous permettra de savoir si un canal est en mode catalogue via son code.
//src/Twig/CatalogExtension.php
class CatalogExtension extends AbstractExtension
{
private ChannelRepositoryInterface $channelRepository;
public function __construct(ChannelRepositoryInterface $channelRepository)
{
$this->channelRepository = $channelRepository;
}
public function getFunctions(): array
{
return [
new TwigFunction('isChannelCatalog', [$this, 'isCatalog'])
];
}
public function isCatalog($channelCode): bool
{
$channel = $this->channelRepository->findOneByCode($channelCode);
if (empty($channel)) {
throw new ChannelNotFoundException();
}
return $channel->getCatalogMode();
}
}
Conclusion
Finalement, le mode catalogue à été ajouté de manière totalement dynamique à notre projet Sylius et nous avons le contrôle total sur les différents rendus d’affichage que nous voulons obtenir.
L’idéal pour une fonctionnalité de ce type est de la développer dans un plugin Sylius afin de la réutiliser très simplement par la suite, et pourquoi pas la mettre à disposition de la communauté ! Peut-être qu’un article sur la création de plugins Sylius pourrait arriver prochainement…
N’hésitez pas à nous faire vos retours sur nos différents réseaux sociaux :
LinkedIn : https://fr.linkedin.com/company/integral-service-web
Twitter : https://twitter.com/IntegralWeb69
Instagram : https://www.instagram.com/integralweb69/
Facebook : https://www.facebook.com/integralweb69