Custom business logic¶

Templates customization is just the beginning of the broad spectrum of customization possibilities in Sylius. There are very few things in Sylius you’re not able to customize or override. Let’s take a look at one of the typical example of customizing Sylius default logic, in this case, logic related to shipments and their cost. It’s time for a custom shipping calculator.

Custom shipping calculator¶

Each shipping calculator is able to calculate a shipping cost for the provided order. This calculation is usually based on bought products and some configuration done by Administrator. By default Sylius provides FlatRateCalculator and PerUnitRateCalculator (their names are quite self-explaining), but it’s sometimes not enough. So let’s say your store packs ordered products in parcels and you need to charge a customer for each of them.

You should start with the implementation of your custom shipping calculator service. Remember, that it must implement the CalculatorInterface from Shipping Component. Let’s name it ParcelCalculator and place it in src/ShippingCalculator directory.

<?php
# src/ShippingCalculator/ParcelCalculator.php

declare(strict_types=1);

namespace App\ShippingCalculator;

use Sylius\Component\Shipping\Calculator\CalculatorInterface;
use Sylius\Component\Shipping\Model\ShipmentInterface;

final class ParcelCalculator implements CalculatorInterface
{
    public function calculate(ShipmentInterface $subject, array $configuration): int
    {
        $parcelSize = $configuration['size'];
        $parcelPrice = $configuration['price'];

        $numberOfPackages = ceil($subject->getUnits()->count() / $parcelSize);

        return (int) ($numberOfPackages * $parcelPrice);
    }

    public function getType(): string
    {
        return 'parcel';
    }
}

Two more things are needed to make it work. A form type, that would be used to pass some data to the $configuration array in the calculator service, and a proper service registration in the services.yaml file.

<?php
# src/Form/Type/ParcelShippingCalculatorType.php

declare(strict_types=1);

namespace App\Form\Type;

use Sylius\Bundle\MoneyBundle\Form\Type\MoneyType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;

final class ParcelShippingCalculatorType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('size', NumberType::class)
            ->add('price', MoneyType::class, [
                'currency' => 'USD',
            ])
        ;
    }
}

Attention

The currency needed for MoneyType in the proposed implementation hardcoded just for testing reasons. In a real application, you should get the proper currency code from the repository, context or some configuration file.

# config/services.yml

services:
    # ...

    App\ShippingCalculator\ParcelCalculator:
        tags:
            -
                {
                    name: sylius.shipping_calculator,
                    calculator: "parcel",
                    label: "Parcel",
                    form_type: App\Form\Type\ParcelShippingCalculatorType
                }

That’s it! You should now be able to select your shipping calculator during the creation or edition of a shipping method.

../_images/shipping-calculator.png

You can also see the results of your customization on checkout shipping step, how the shipping fee changes depending on how many products you have in the cart.

For 1 product:

../_images/shipping-cost-1.png

For 4 products:

../_images/shipping-cost-2.png

This customization will also work when using unified API without any extra steps:

First, you need to pick up a new cart:

curl -X POST "https://master.demo.sylius.com/api/v2/shop/orders" -H "accept: application/ld+json"

With body:

{
    "localeCode": "string"
}

Note

The localeCode value is optional in the body of cart pickup. This means that if you won’t provide it, the default locale from the channel will be used.

This should return a response with tokenValue which we would need for the next API calls:

{
    //...
    "shippingState": "string",
    "tokenValue": "CART_TOKEN",
    "id": 123,
    //...
}

Then we need to add a product to the cart but first, it would be good to have any. You can use this call to retrieve some products:

curl -X GET "https://master.demo.sylius.com/api/v2/shop/products?page=1&itemsPerPage=30" -H  "accept: application/ld+json"

And choose any product variant IRI that you would like to add to your cart:

curl --location --request PATCH 'https://master.demo.sylius.com/api/v2/shop/orders/CART_TOKEN/items' -H 'Content-Type: application/merge-patch+json'

With a chosen product variant in the body:

{
    "productVariant": "/api/v2/shop/product-variants/PRODUCT_VARIANT_CODE",
    "quantity": 1
}

This should return a response with the cart that should contain a shippingTotal:

{
    //...
    "taxTotal": 0,
    "shippingTotal": 500,
    "orderPromotionTotal": 0,
    //...
}

Attention

API returns costs in decimal numbers that’s why in response it is 500 currency unit shipping total (which stands for 5 USD in this case).

Now let’s change the quantity of our product variant. We can do it by calling the endpoint above once again, or by utilizing the changeQuantity endpoint:

curl --location --request PATCH 'https://master.demo.sylius.com/api/v2/shop/orders/CART_TOKEN/items/ORDER_ITEM_ID' -H 'Content-Type: application/merge-patch+json'

With new quantity in the body:

{
  "quantity": 4
}

Which should return a response with the cart:

{
    //...
    "taxTotal": 0,
    "shippingTotal": 1000,
    "orderPromotionTotal": 0,
    //...
}

Amazing job! You’ve just provided your own logic into a Sylius-based system. Therefore, your store can provide a unique experience for your Customers. Basing on this knowledge, you’re ready to customize your shop even more and make it as suitable to your business needs as possible.