Sylius, comment mettre en place un mode catalogue

12/09/2022 ·  web

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.

catalog form screen

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…).

order processor

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