Important

You are browsing the documentation for version 3.1 of OroCommerce, OroCRM and OroPlatform, which is no longer maintained. Read version 5.1 (the latest LTS version) of the Oro documentation to get up-to-date information.

See our Release Process documentation for more information on the currently supported and upcoming releases.

Shipping Methods

This topic describes how to add a custom shipping method to your OroCommerce-based store.

It is recommended to manage shipping methods through integrations. Therefore, to create a new shipping method:

  • Implement an integration for the shipping method

  • Implement the shipping method itself

Usually, a shipping method has several services to provide a flexible choice of price and delivery time. As an example, we will implement the “Fast Shipping” method — a simple method that requires just the minimum set of options to operate. It will have two services (types): “With present” and “Without present”. Thus, at the end of the topic, you will have the understanding of what steps are necessary to add a workable shipment method and the basic template that you can further extend when the need arises.

Create a Bundle

First, create and enable the FastShippingBundle bundle for your shipping method as described in the How to create a new bundle topic:

  1. In the /src/ACME/Bundle/FastShippingBundle/ directory of your application, create class ACMEFastShippingBundle.php:

 1<?php
 2
 3namespace ACME\Bundle\FastShippingBundle;
 4
 5use ACME\Bundle\FastShippingBundle\DependencyInjection\FastShippingExtension;
 6use Symfony\Component\HttpKernel\Bundle\Bundle;
 7
 8/**
 9 * The FastShipping bundle class.
10 */
11class ACMEFastShippingBundle extends Bundle
12{
13    /**
14     * {@inheritDoc}
15     */
16    public function getContainerExtension()
17    {
18        if (!$this->extension) {
19            $this->extension = new FastShippingExtension();
20        }
21
22        return $this->extension;
23    }
24}

Tip

The body of your class can be empty if you use regular case in the name of your organization (i.e. Acme or ACME in our example). getExtension() is necessary when you use uppercase, as Symfony treats uppercase letters in the organization prefix as separate words when creating aliases.

  1. To enable the bundle, create Resources/config/oro/bundles.yml in the same directory, with the following content:

1bundles:
2    # Set a priority higher than the priority of the ShippingBundle (i.e. 200) to ensure that all dependencies
3    # from the ShippingBundle are loaded before the dependent methods of your bundle.
4    - { name: ACME\Bundle\FastShippingBundle\ACMEFastShippingBundle, priority: 200 }

Hint

To fully enable a bundle, you need to regenerate the application cache. However, to save time, you can do it after creation of the shipping integration.

Tip

All the files and subdirectories mentioned in the following sections are to be added to the /src/ACME/Bundle/FastShippingBundle/ directory of your application (referred to as <bundle_root>).

Create a Shipping Integration

Create an Entity to Store the Shipping Method Settings

Define an entity that to store the configuration settings of the shipping method in the database. To do this, create <bundle_root>/Entity/FastShippingSettings.php:

 1<?php
 2
 3namespace ACME\Bundle\FastShippingBundle\Entity;
 4
 5use Doctrine\Common\Collections\ArrayCollection;
 6use Doctrine\Common\Collections\Collection;
 7use Doctrine\ORM\Mapping as ORM;
 8use Oro\Bundle\IntegrationBundle\Entity\Transport;
 9use Oro\Bundle\LocaleBundle\Entity\LocalizedFallbackValue;
10use Symfony\Component\HttpFoundation\ParameterBag;
11
12/**
13 * Entity with settings for Fast Shipping integration
14 *
15 * @ORM\Entity
16 */
17class FastShippingSettings extends Transport
18{
19    /**
20     * @var Collection|LocalizedFallbackValue[]
21     *
22     * @ORM\ManyToMany(
23     *      targetEntity="Oro\Bundle\LocaleBundle\Entity\LocalizedFallbackValue",
24     *      cascade={"ALL"},
25     *      orphanRemoval=true
26     * )
27     * @ORM\JoinTable(
28     *      name="acme_fast_ship_transport_label",
29     *      joinColumns={
30     *          @ORM\JoinColumn(name="transport_id", referencedColumnName="id", onDelete="CASCADE")
31     *      },
32     *      inverseJoinColumns={
33     *          @ORM\JoinColumn(name="localized_value_id", referencedColumnName="id", onDelete="CASCADE", unique=true)
34     *      }
35     * )
36     */
37    private $labels;
38
39    /**
40     * @var ParameterBag
41     */
42    private $settings;
43
44    public function __construct()
45    {
46        $this->labels = new ArrayCollection();
47    }
48
49    /**
50     * @return Collection|LocalizedFallbackValue[]
51     */
52    public function getLabels(): Collection
53    {
54        return $this->labels;
55    }
56
57    /**
58     * @param LocalizedFallbackValue $label
59     *
60     * @return FastShippingSettings
61     */
62    public function addLabel(LocalizedFallbackValue $label): FastShippingSettings
63    {
64        if (!$this->labels->contains($label)) {
65            $this->labels->add($label);
66        }
67
68        return $this;
69    }
70
71    /**
72     * @param LocalizedFallbackValue $label
73     *
74     * @return FastShippingSettings
75     */
76    public function removeLabel(LocalizedFallbackValue $label): FastShippingSettings
77    {
78        if ($this->labels->contains($label)) {
79            $this->labels->removeElement($label);
80        }
81
82        return $this;
83    }
84
85    /**
86     * @return ParameterBag
87     */
88    public function getSettingsBag(): ParameterBag
89    {
90        if (null === $this->settings) {
91            $this->settings = new ParameterBag([]);
92        }
93
94        return $this->settings;
95    }
96}

As you can see from the code above, the only necessary parameter defined for the FastShipping shipping method is the label parameter.

Important

When naming DB columns, make sure that the name does not exceed 31 symbols. Pay attention to the acme_fast_ship_transport_label name in the following extract:

1     * @ORM\JoinTable(
2     *      name="acme_fast_ship_transport_label",
3     *      joinColumns={
4     *          @ORM\JoinColumn(name="transport_id", referencedColumnName="id", onDelete="CASCADE")
5     *      },
6     *      inverseJoinColumns={
7     *          @ORM\JoinColumn(name="localized_value_id", referencedColumnName="id", onDelete="CASCADE", unique=true)
8     *      }
9     * )

Create a User Interface Form for the Shipping Method Integration

When you add an integration via the user interface of the back-office, a form that contains the integration settings appears. In this step, implement the form. To do this, create <bundle_root>/Form/Type/FastShippingTransportSettingsType.php:

 1<?php
 2
 3namespace ACME\Bundle\FastShippingBundle\Form\Type;
 4
 5use ACME\Bundle\FastShippingBundle\Entity\FastShippingSettings;
 6use Oro\Bundle\LocaleBundle\Form\Type\LocalizedFallbackValueCollectionType;
 7use Symfony\Component\Form\AbstractType;
 8use Symfony\Component\Form\FormBuilderInterface;
 9use Symfony\Component\OptionsResolver\OptionsResolver;
10use Symfony\Component\Validator\Constraints\NotBlank;
11
12/**
13 * Form type for Fast Shipping integration settings
14 */
15class FastShippingTransportSettingsType extends AbstractType
16{
17    private const BLOCK_PREFIX = 'acme_fast_shipping_settings';
18
19    /**
20     * {@inheritDoc}
21     */
22    public function buildForm(FormBuilderInterface $builder, array $options)
23    {
24        $builder
25            ->add(
26                'labels',
27                LocalizedFallbackValueCollectionType::class,
28                [
29                    'label'    => 'acme.fast_shipping.settings.labels.label',
30                    'required' => true,
31                    'entry_options'  => ['constraints' => [new NotBlank()]],
32                ]
33            );
34    }
35
36    /**
37     * {@inheritDoc}
38     */
39    public function configureOptions(OptionsResolver $resolver)
40    {
41        $resolver->setDefaults([
42            'data_class' => FastShippingSettings::class
43        ]);
44    }
45
46    /**
47     * {@inheritDoc}
48     */
49    public function getBlockPrefix()
50    {
51        return self::BLOCK_PREFIX;
52    }
53}

Add Translations for the Form Texts

To present the information on the user interface in a user-friendly way, add translations for the shipping method settings’ names. To do this, create <bundle_root>/Resources/translations/messages.en.yml:

1acme:
2    fast_shipping:
3        settings:
4            labels:
5                label: 'Label'

This defines the name of the field that contains the label.

Create the Integration Channel Type

When you select the type of the integration on the user interface, you will see the integration name and the icon that you define in this step.

To implement a channel type, create <bundle_root>/Integration/FastShippingChannelType.php:

 1<?php
 2
 3namespace ACME\Bundle\FastShippingBundle\Integration;
 4
 5use Oro\Bundle\IntegrationBundle\Provider\ChannelInterface;
 6use Oro\Bundle\IntegrationBundle\Provider\IconAwareIntegrationInterface;
 7
 8/**
 9 * Integration channel type for Fast Shipping integration
10 */
11class FastShippingChannelType implements ChannelInterface, IconAwareIntegrationInterface
12{
13    /**
14     * {@inheritDoc}
15     */
16    public function getLabel(): string
17    {
18        return 'acme.fast_shipping.channel_type.label';
19    }
20
21    /**
22     * {@inheritDoc}
23     */
24    public function getIcon(): string
25    {
26        return 'bundles/acmefastshipping/img/fast-shipping-logo.png';
27    }
28}

Add an Icon for the Integration

To add an icon:

  1. Save the file to the <bundle_root>/Resources/public/img directory.

  2. Install assets:

    1bin/console assets:install --symlink
    

To make sure that the icon is accessible for the web interface, check if it appears (as a copy or a symlink depending on the settings selected during the application installation) in the /public/bundles/acmefastshipping/img directory of your application.

Create the Integration Transport

Transport is generally responsible for how the data is obtained from the external system. While the Fast Shipping method does not interact with external systems, you still need to define transport and implement all methods of the TransportInterface for the integration to work properly. To add transport, create <bundle_root>/Integration/FastShippingTransport.php:

 1<?php
 2
 3namespace ACME\Bundle\FastShippingBundle\Integration;
 4
 5use ACME\Bundle\FastShippingBundle\Entity\FastShippingSettings;
 6use ACME\Bundle\FastShippingBundle\Form\Type\FastShippingTransportSettingsType;
 7use Oro\Bundle\IntegrationBundle\Entity\Transport;
 8use Oro\Bundle\IntegrationBundle\Provider\TransportInterface;
 9use Symfony\Component\HttpFoundation\ParameterBag;
10
11/**
12 * Transport for Fast Shipping integration
13 */
14class FastShippingTransport implements TransportInterface
15{
16    /** @var ParameterBag */
17    protected $settings;
18
19    /**
20     * @param Transport $transportEntity
21     */
22    public function init(Transport $transportEntity)
23    {
24        $this->settings = $transportEntity->getSettingsBag();
25    }
26
27    /**
28     * {@inheritDoc}
29     */
30    public function getSettingsFormType(): string
31    {
32        return FastShippingTransportSettingsType::class;
33    }
34
35    /**
36     * {@inheritDoc}
37     */
38    public function getSettingsEntityFQCN(): string
39    {
40        return FastShippingSettings::class;
41    }
42
43    /**
44     * {@inheritDoc}
45     */
46    public function getLabel(): string
47    {
48        return 'acme.fast_shipping.transport.label';
49    }
50}

Create a Configuration File for the Service Container

To start using a service container for your bundle, first create the bundle configuration file <bundle_root>/Resources/config/services.yml.

Add the Channel Type and Transport to the Services Container

To register the channel type and transport, append the following key-values to <bundle_root>/Resources/config/services.yml:

 1parameters:
 2    acme_fast_shipping.integration.type: 'fast_shipping'
 3
 4services:
 5    acme_fast_shipping.integration.channel:
 6        class: 'ACME\Bundle\FastShippingBundle\Integration\FastShippingChannelType'
 7        public: false
 8        tags:
 9            - { name: oro_integration.channel, type: '%acme_fast_shipping.integration.type%' }
10
11    acme_fast_shipping.integration.transport:
12        class: 'ACME\Bundle\FastShippingBundle\Integration\FastShippingTransport'
13        public: false
14        tags:
15            - { name: oro_integration.transport, type: '%acme_fast_shipping.integration.type%', channel_type: '%acme_fast_shipping.integration.type%' }

Set up Services with DependencyInjection

To set up services, load your configuration file (services.yml) using the DependencyInjection component. For this, create <bundle_root>/DependencyInjection/FastShippingExtension.php with the following content:

 1<?php
 2
 3namespace ACME\Bundle\FastShippingBundle\DependencyInjection;
 4
 5use Symfony\Component\Config\FileLocator;
 6use Symfony\Component\DependencyInjection\ContainerBuilder;
 7use Symfony\Component\DependencyInjection\Loader;
 8use Symfony\Component\HttpKernel\DependencyInjection\Extension;
 9
10class FastShippingExtension extends Extension
11{
12    /**
13     * @param array $configs
14     * @param ContainerBuilder $container
15     *
16     * @throws \Exception
17     */
18    public function load(array $configs, ContainerBuilder $container)
19    {
20        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
21        $loader->load('services.yml');
22    }
23
24    /**
25     * @return string
26     */
27    public function getAlias()
28    {
29        return 'acme_fast_shipping';
30    }
31}

Add Translations for the Channel Type and Transport

The channel type and, in general, transport labels also appear on the user interface (you will not see the transport label for Fast Shipping). Provide translations for them by appending the <bundle_root>/Resources/translations/messages.en.yml. Now, the messages.en.yml content must look as follows:

1acme:
2    fast_shipping:
3        settings:
4            labels:
5                label: 'Label'
6        transport:
7            label: 'Fast Shipping'
8        channel_type:
9            label: 'Fast Shipping'

Add an Installer

An installer ensures that upon the application installation, the database will contain the entity that you defined within your bundle.

Follow the instructions provided in the How to generate an installer topic to apply the changes without migration and generate an installer file based on the current schema of the DB.

Note

If you have not performed the steps mentioned in How to generate an installer, because you already have the installer file, then make sure to run the php bin/console oro:migration:load --force command to apply the changes from the file.

After you complete the process, you will have the <bundle_root>/Migrations/Schema/FastShippingBundleInstaller.php class with the following content:

 1<?php
 2
 3namespace ACME\Bundle\FastShippingBundle\Migrations\Schema;
 4
 5use Doctrine\DBAL\Schema\Schema;
 6use Oro\Bundle\MigrationBundle\Migration\Installation;
 7use Oro\Bundle\MigrationBundle\Migration\QueryBag;
 8
 9/**
10 * @SuppressWarnings(PHPMD.TooManyMethods)
11 * @SuppressWarnings(PHPMD.ExcessiveClassLength)
12 */
13class FastShippingBundleInstaller implements Installation
14{
15    /**
16     * {@inheritDoc}
17     */
18    public function getMigrationVersion()
19    {
20        return 'v1_0';
21    }
22
23    /**
24     * {@inheritDoc}
25     */
26    public function up(Schema $schema, QueryBag $queries)
27    {
28        /** Tables generation **/
29        $this->createAcmeFastShipTransportLabelTable($schema);
30
31        /** Foreign keys generation **/
32        $this->addAcmeFastShipTransportLabelForeignKeys($schema);
33    }
34
35    /**
36     * Create acme_fast_ship_transport_label table
37     *
38     * @param Schema $schema
39     */
40    protected function createAcmeFastShipTransportLabelTable(Schema $schema)
41    {
42        $table = $schema->createTable('acme_fast_ship_transport_label');
43        $table->addColumn('transport_id', 'integer', []);
44        $table->addColumn('localized_value_id', 'integer', []);
45        $table->setPrimaryKey(['transport_id', 'localized_value_id']);
46        $table->addUniqueIndex(['localized_value_id'], 'UNIQ_15E6E6F3EB576E89');
47        $table->addIndex(['transport_id'], 'IDX_15E6E6F39909C13F', []);
48    }
49
50    /**
51     * Add acme_fast_ship_transport_label foreign keys.
52     *
53     * @param Schema $schema
54     */
55    protected function addAcmeFastShipTransportLabelForeignKeys(Schema $schema)
56    {
57        $table = $schema->getTable('acme_fast_ship_transport_label');
58        $table->addForeignKeyConstraint(
59            $schema->getTable('oro_integration_transport'),
60            ['transport_id'],
61            ['id'],
62            ['onDelete' => 'CASCADE', 'onUpdate' => null]
63        );
64        $table->addForeignKeyConstraint(
65            $schema->getTable('oro_fallback_localization_val'),
66            ['localized_value_id'],
67            ['id'],
68            ['onDelete' => 'CASCADE', 'onUpdate' => null]
69        );
70    }
71}

Check That the Integration is Created Successfully

  1. Clear the application cache:

    1bin/console cache:clear
    

    Note

    If you are working in a production environment, you have to use the --env=prod parameter with the command.

  2. Open the user interface and check that the changes have applied and you can add an integration of the Fast Shipping type. Note that at this point you are not yet able to add this shipping method to a shipping rule.

    View the Fast Shipping integration details.

Implement a Shipping Method

Now implement the shipping method itself using the following steps:

Implement the Main Method

To implement the main method, create the <bundle_root>/Method/FastShippingMethod.php class that implements two standard interfaces \Oro\Bundle\ShippingBundle\Method\ShippingMethodInterface and \Oro\Bundle\ShippingBundle\Method\ShippingMethodIconAwareInterface:

  1<?php
  2
  3namespace ACME\Bundle\FastShippingBundle\Method;
  4
  5use Oro\Bundle\ShippingBundle\Method\ShippingMethodIconAwareInterface;
  6use Oro\Bundle\ShippingBundle\Method\ShippingMethodInterface;
  7use Oro\Bundle\ShippingBundle\Method\ShippingMethodTypeInterface;
  8use Symfony\Component\Form\Extension\Core\Type\HiddenType;
  9
 10/**
 11 * Fast Shipping method class
 12 */
 13class FastShippingMethod implements ShippingMethodInterface, ShippingMethodIconAwareInterface
 14{
 15    /**
 16     * @var array
 17     */
 18    private $types;
 19
 20    /**
 21     * @var string
 22     */
 23    private $label;
 24
 25    /**
 26     * @var string|null
 27     */
 28    private $icon;
 29
 30    /**
 31     * @var string
 32     */
 33    private $identifier;
 34
 35    /**
 36     * @var bool
 37     */
 38    private $enabled;
 39
 40    /**
 41     * @param string $identifier
 42     * @param string $label
 43     * @param string|null $icon
 44     * @param bool $enabled
 45     * @param array $types
 46     */
 47    public function __construct(string $identifier, string $label, ?string $icon, bool $enabled, array $types)
 48    {
 49        $this->identifier = $identifier;
 50        $this->label = $label;
 51        $this->icon = $icon;
 52        $this->enabled = $enabled;
 53        $this->types = $types;
 54    }
 55
 56    /**
 57     * {@inheritDoc}
 58     */
 59    public function getIdentifier(): string
 60    {
 61        return $this->identifier;
 62    }
 63
 64    /**
 65     * {@inheritDoc}
 66     */
 67    public function isGrouped(): bool
 68    {
 69        return true;
 70    }
 71
 72    /**
 73     * {@inheritDoc}
 74     */
 75    public function isEnabled(): bool
 76    {
 77        return $this->enabled;
 78    }
 79
 80    /**
 81     * {@inheritDoc}
 82     */
 83    public function getLabel(): string
 84    {
 85        return $this->label;
 86    }
 87
 88    /**
 89     * {@inheritDoc}
 90     */
 91    public function getIcon(): ?string
 92    {
 93        return $this->icon;
 94    }
 95
 96    /**
 97     * {@inheritDoc}
 98     */
 99    public function getTypes(): array
100    {
101        return $this->types;
102    }
103
104    /**
105     * {@inheritDoc}
106     */
107    public function getType($type): ?ShippingMethodTypeInterface
108    {
109        if (array_key_exists($type, $this->types)) {
110            return $this->types[$type];
111        }
112
113        return null;
114    }
115
116    /**
117     * {@inheritDoc}
118     */
119    public function getOptionsConfigurationFormType(): string
120    {
121        return HiddenType::class;
122    }
123
124    /**
125     * {@inheritDoc}
126     */
127    public function getSortOrder(): int
128    {
129        return 150;
130    }
131}

The methods are the following:

  • getIdentifier — Provides a unique identifier of the shipping method in the scope of the Oro application.

  • getLabel — Returns the shipping method’s label that appears on the shipping rule edit page. It can also be a Symfony translated message.

  • getIcon — Returns the icon that appears on the shipping rule edit page.

  • isEnabled — Defines, whether the integration of the shipping method is enabled by default.

  • isGrouped — Defines how shipping method’s types appear in the shipping method configuration on the user interface. If set to true, the types appear in the table where each line contains the Active check box that enables users to enable individual shipping method types for a particular shipping method configuration.

  • getSortOrder — Defines the order in which shipping methods appear on the user interface. For example, in the following screenshot, the Flat rate sort order is lower than the UPS sort order:

    ../../../_images/shipping_methods_frontend.png
  • getType — Returns the selected shipping method type based on the type identifier.

  • getTypes — Returns a set of the shipping method types.

  • getOptionsConfigurationFormType — Returns the user interface form with the configuration options. The form appears on the shipping rule edit page. If the method returns HiddenType::class, the form does not appear.

Add the Shipping Method Identifier Generator to the Services Container

Append the following lines to <bundle_root>/Resources/config/services.yml:

1    acme_fast_shipping.method.identifier_generator.method:
2        parent: oro_integration.generator.prefixed_identifier_generator
3        arguments:
4            - '%acme_fast_shipping.integration.type%'

Create a Factory for the Shipping Method

This factory generates an individual configuration set for each instance of the integration of the Fast Shipping type. In our case, it also contains the method createTypes() that generates the services (types) of the fast shipping type and assigns them labels.

Create the <bundle_root>/Factory/FastShippingMethodFromChannelFactory.php class with the following content:

  1<?php
  2
  3namespace ACME\Bundle\FastShippingBundle\Factory;
  4
  5use ACME\Bundle\FastShippingBundle\Entity\FastShippingSettings;
  6use ACME\Bundle\FastShippingBundle\Method\FastShippingMethod;
  7use ACME\Bundle\FastShippingBundle\Method\FastShippingMethodType;
  8use Oro\Bundle\IntegrationBundle\Entity\Channel;
  9use Oro\Bundle\IntegrationBundle\Generator\IntegrationIdentifierGeneratorInterface;
 10use Oro\Bundle\IntegrationBundle\Provider\IntegrationIconProviderInterface;
 11use Oro\Bundle\LocaleBundle\Helper\LocalizationHelper;
 12use Oro\Bundle\ShippingBundle\Method\Factory\IntegrationShippingMethodFactoryInterface;
 13use Symfony\Component\Translation\TranslatorInterface;
 14
 15/**
 16 * Factory that creates shipping method from the channel
 17 */
 18class FastShippingMethodFromChannelFactory implements IntegrationShippingMethodFactoryInterface
 19{
 20    /**
 21     * @var IntegrationIdentifierGeneratorInterface
 22     */
 23    private $identifierGenerator;
 24
 25    /**
 26     * @var LocalizationHelper
 27     */
 28    private $localizationHelper;
 29
 30    /**
 31     * @var TranslatorInterface
 32     */
 33    private $translator;
 34
 35    /**
 36     * @var IntegrationIconProviderInterface
 37     */
 38    private $integrationIconProvider;
 39
 40    /**
 41     * FastShippingMethodFromChannelFactory constructor.
 42     * @param IntegrationIdentifierGeneratorInterface $identifierGenerator
 43     * @param LocalizationHelper $localizationHelper
 44     * @param TranslatorInterface $translator
 45     * @param IntegrationIconProviderInterface $integrationIconProvider
 46     */
 47    public function __construct(
 48        IntegrationIdentifierGeneratorInterface $identifierGenerator,
 49        LocalizationHelper $localizationHelper,
 50        TranslatorInterface $translator,
 51        IntegrationIconProviderInterface $integrationIconProvider
 52    ) {
 53        $this->identifierGenerator = $identifierGenerator;
 54        $this->localizationHelper = $localizationHelper;
 55        $this->translator = $translator;
 56        $this->integrationIconProvider = $integrationIconProvider;
 57    }
 58
 59    /**
 60     * @param Channel $channel
 61     *
 62     * @return FastShippingMethod
 63     */
 64    public function create(Channel $channel): FastShippingMethod
 65    {
 66        $id = $this->identifierGenerator->generateIdentifier($channel);
 67        $label = $this->getChannelLabel($channel);
 68        $icon = $this->getIcon($channel);
 69        $types = $this->createTypes();
 70
 71        return new FastShippingMethod($id, $label, $icon, $channel->isEnabled(), $types);
 72    }
 73
 74    /**
 75     * @param Channel $channel
 76     *
 77     * @return string
 78     */
 79    private function getChannelLabel(Channel $channel): string
 80    {
 81        /** @var FastShippingSettings $transport */
 82        $transport = $channel->getTransport();
 83
 84        return (string) $this->localizationHelper->getLocalizedValue($transport->getLabels());
 85    }
 86
 87    /**
 88     * @param Channel $channel
 89     *
 90     * @return string|null
 91     */
 92    private function getIcon(Channel $channel): ?string
 93    {
 94        return $this->integrationIconProvider->getIcon($channel);
 95    }
 96
 97    /**
 98     * @return array
 99     */
100    private function createTypes(): array
101    {
102        $withoutPresentLabel = $this->translator
103            ->trans('acme.fast_shipping.method.processing_type.without_present.label');
104        $withPresentLabel = $this->translator
105            ->trans('acme.fast_shipping.method.processing_type.with_present.label');
106
107        $withoutPresent = new FastShippingMethodType($withoutPresentLabel, false);
108        $withPresent = new FastShippingMethodType($withPresentLabel, true);
109
110        return [
111            $withoutPresent->getIdentifier() => $withoutPresent,
112            $withPresent->getIdentifier() => $withPresent,
113        ];
114    }
115}

Add the Shipping Method Factory to the Services Container

To register the shipping method factory, append the following key-values to <bundle_root>/Resources/config/services.yml under the services section:

1    acme_fast_shipping.factory.method:
2        class: 'ACME\Bundle\FastShippingBundle\Factory\FastShippingMethodFromChannelFactory'
3        public: false
4        arguments:
5            - '@acme_fast_shipping.method.identifier_generator.method'
6            - '@oro_locale.helper.localization'
7            - '@translator'
8            - '@oro_integration.provider.integration_icon'

Create a Shipping Method Provider

For this, add the <bundle_root>/Method/FastShippingMethodProvider.php class with the following content:

 1<?php
 2
 3namespace ACME\Bundle\FastShippingBundle\Method;
 4
 5use Oro\Bundle\EntityBundle\ORM\DoctrineHelper;
 6use Oro\Bundle\ShippingBundle\Method\Factory\IntegrationShippingMethodFactoryInterface;
 7use Oro\Bundle\ShippingBundle\Method\Provider\Integration\ChannelShippingMethodProvider;
 8
 9/**
10 * Provides methods for getting shipping method object by name
11 */
12class FastShippingMethodProvider extends ChannelShippingMethodProvider
13{
14    /**
15     * {@inheritDoc}
16     */
17    public function __construct(
18        $channelType,
19        DoctrineHelper $doctrineHelper,
20        IntegrationShippingMethodFactoryInterface $methodFactory
21    ) {
22        parent::__construct($channelType, $doctrineHelper, $methodFactory);
23    }
24}

Add the Shipping Method Provider to the Services Container

Append the following lines to <bundle_root>/Resources/config/services.yml under the services section:

1    acme_fast_shipping.method.provider:
2        class: 'ACME\Bundle\FastShippingBundle\Method\FastShippingMethodProvider'
3        arguments:
4            - '%acme_fast_shipping.integration.type%'
5            - '@oro_entity.doctrine_helper'
6            - '@acme_fast_shipping.factory.method'
7        tags:
8            - { name: oro_shipping_method_provider }
9            - { name: doctrine.orm.entity_listener, entity: 'Oro\Bundle\IntegrationBundle\Entity\Channel', event: postLoad }

Create a Shipping Method Type

Shipping method types define different specifics of the same shipping services. For example, for Flat Rate, the type defines whether to calculate shipping price per order or per item. The Fast Shipping will have two types: “With Present” and “Without Present”.

To create a shipping method type, add the <bundle_root>/Method/FastShippingMethodType.php class with the following content:

 1<?php
 2
 3namespace ACME\Bundle\FastShippingBundle\Method;
 4
 5use ACME\Bundle\FastShippingBundle\Form\Type\FastShippingMethodOptionsType;
 6use Oro\Bundle\CurrencyBundle\Entity\Price;
 7use Oro\Bundle\ShippingBundle\Context\ShippingContextInterface;
 8use Oro\Bundle\ShippingBundle\Method\ShippingMethodTypeInterface;
 9
10/**
11 * Fast Shipping method type
12 * Contains logic of calculating shipping price
13 */
14class FastShippingMethodType implements ShippingMethodTypeInterface
15{
16    public const PRICE_OPTION = 'price';
17
18    private const WITHOUT_PRESENT_TYPE = 'without_present';
19    private const WITH_PRESENT_TYPE = 'with_present';
20
21    /**
22     * @var string
23     */
24    private $label;
25
26    /**
27     * @var bool
28     */
29    private $isWithPresent;
30
31    /**
32     * @param string $label
33     * @param bool $isWithPresent
34     */
35    public function __construct(string $label, bool $isWithPresent)
36    {
37        $this->label = $label;
38        $this->isWithPresent = $isWithPresent;
39    }
40
41    /**
42     * {@inheritDoc}
43     */
44    public function getIdentifier()
45    {
46        return $this->isWithPresent ? self::WITH_PRESENT_TYPE : self::WITHOUT_PRESENT_TYPE;
47    }
48
49    /**
50     * {@inheritDoc}
51     */
52    public function getLabel(): string
53    {
54        return $this->label;
55    }
56
57    /**
58     * {@inheritDoc}
59     */
60    public function getSortOrder(): int
61    {
62        return 0;
63    }
64
65    /**
66     * {@inheritDoc}
67     */
68    public function getOptionsConfigurationFormType(): string
69    {
70        return FastShippingMethodOptionsType::class;
71    }
72
73    /**
74     * {@inheritDoc}
75     */
76    public function calculatePrice(ShippingContextInterface $context, array $methodOptions, array $typeOptions): ?Price
77    {
78        $price = $typeOptions[static::PRICE_OPTION];
79
80        // Provide additional price calculation logic here if required.
81
82        return Price::create((float)$price, $context->getCurrency());
83    }
84}
  • getIdentifier — Returns a unique identifier of a shipping method type in the scope of the shipping method.

  • getLabel — Returns the label of the shipping method type. The label appears on the shipping rule edit page in the back-office and on the storefront.

  • getSortOrder — Defines the order in which shipping method types appear on the user interface. For example, see the UPS shipping types below. The number that defines the sort order of the UPS Ground is lower than that of the UPS 2nd Day Air (i.e. the lower the number, the higher up the list the method type appears):

    ../../../_images/shipping_methods_frontend.png
  • getOptionsConfigurationFormType — Returns the user interface form with the configuration options. The form appears on the shipping rule edit page. If the method returns HiddenType::class, the form does not appear.

  • calculatePrice– Contains the main logic and returns the shipping price for the given $context.

Note

If you implement a more complicated shipping method, see OroBundleShippingBundleContextShippingContextInterface for attributes that can affect a shipping price (e.g., shipping address information or line items).

Define Translation for the Shipping Method Type

Provide translations by appending the <bundle_root>/Resources/translations/messages.en.yml. Now, the messages.en.yml content must look as follows:

 1acme:
 2    fast_shipping:
 3        settings:
 4            labels:
 5                label: 'Label'
 6        transport:
 7            label: 'Fast Shipping'
 8        channel_type:
 9            label: 'Fast Shipping'
10        method:
11            price.label: 'Price'
12            processing_type:
13                without_present.label: 'Fast Shipping Rate Without Present'
14                with_present.label: 'Fast Shipping Rate With Present'

Create a Shipping Method Options Form

This form with options for a shipping method appears on the user interface of the back-office when you add the shipping method to a shipping rule. Add FastShippingMethodOptionsType.php to the <bundle_root>/Form/Type/ directory:

 1<?php
 2
 3namespace ACME\Bundle\FastShippingBundle\Form\Type;
 4
 5use ACME\Bundle\FastShippingBundle\Method\FastShippingMethodType;
 6use Oro\Bundle\CurrencyBundle\Rounding\RoundingServiceInterface;
 7use Symfony\Component\Form\AbstractType;
 8use Symfony\Component\Form\Extension\Core\Type\NumberType;
 9use Symfony\Component\Form\FormBuilderInterface;
10use Symfony\Component\OptionsResolver\Exception\AccessException;
11use Symfony\Component\OptionsResolver\OptionsResolver;
12use Symfony\Component\Validator\Constraints\NotBlank;
13use Symfony\Component\Validator\Constraints\Type;
14
15/**
16 * Form type for fast shipping method options which are displayed on shipping rules page
17 */
18class FastShippingMethodOptionsType extends AbstractType
19{
20    private const BLOCK_PREFIX = 'acme_fast_shipping_options_type';
21
22    /**
23     * @var RoundingServiceInterface
24     */
25    protected $roundingService;
26
27    /**
28     * @param RoundingServiceInterface $roundingService
29     */
30    public function __construct(RoundingServiceInterface $roundingService)
31    {
32        $this->roundingService = $roundingService;
33    }
34
35    /**
36     * {@inheritDoc}
37     */
38    public function buildForm(FormBuilderInterface $builder, array $options)
39    {
40        $priceOptions = [
41            'scale' => $this->roundingService->getPrecision(),
42            'rounding_mode' => $this->roundingService->getRoundType(),
43            'attr' => ['data-scale' => $this->roundingService->getPrecision()],
44        ];
45
46        $builder
47            ->add(FastShippingMethodType::PRICE_OPTION, NumberType::class, array_merge([
48                'label' => 'acme.fast_shipping.method.price.label',
49                'constraints' => [new NotBlank(), new Type(['type' => 'numeric'])],
50            ], $priceOptions));
51    }
52
53    /**
54     * @param OptionsResolver $resolver
55     *
56     * @throws AccessException
57     */
58    public function configureOptions(OptionsResolver $resolver)
59    {
60        $resolver->setDefaults([
61            'label' => 'acme.fast_shipping.form.acme_fast_shipping_options_type.label',
62        ]);
63    }
64
65    /**
66     * {@inheritDoc}
67     */
68    public function getBlockPrefix()
69    {
70        return self::BLOCK_PREFIX;
71    }
72}

Add the Shipping Method Options Form to the Services Container

Append the following lines to <bundle_root>/Resources/config/services.yml under the services section:

1    acme_fast_shipping.form.type.fast_shipping_options:
2        class: 'ACME\Bundle\FastShippingBundle\Form\Type\FastShippingMethodOptionsType'
3        arguments:
4            - '@oro_currency.rounding.price_rounding_service'
5        tags:
6            - { name: form.type }

Define Translation for the Shipping Method Form Options

Provide translations by appending the <bundle_root>/Resources/translations/messages.en.yml. Now, the messages.en.yml content must look as follows:

 1acme:
 2    fast_shipping:
 3        settings:
 4            labels:
 5                label: 'Label'
 6        transport:
 7            label: 'Fast Shipping'
 8        channel_type:
 9            label: 'Fast Shipping'
10        method:
11            price.label: 'Price'
12            processing_type:
13                without_present.label: 'Fast Shipping Rate Without Present'
14                with_present.label: 'Fast Shipping Rate With Present'
15        form:
16            acme_fast_shipping_options_type.label: 'Fast Shipping Rate'

Add a Template

In the shipping rules, this template is used to display the configured settings of the Fast Shipping integration.

Create the /Resources/views/method/fastShippingMethodWithOptions.html.twig file with the following content:

 1{% import 'OroShippingBundle:ShippingMethodsConfigsRule:macros.html.twig' as ShipRuleMacro %}
 2
 3   {% spaceless %}
 4       {% set methodLabel = get_shipping_method_label(methodData.identifier)|trans %}
 5       {% if methodLabel|length > 0 %}
 6           <li>{{ methodLabel }}
 7           <ul>
 8       {% endif %}
 9       {% for type in methodData.types %}
10           {%- if type.enabled -%}
11               <li>{{ get_shipping_method_type_label(methodData.identifier, type.identifier)|trans }} ({{ 'acme.fast_shipping.method.price.label'|trans }}: {{ type.options['price']|oro_format_currency({'currency': currency}) }}
12                   ) {{ ShipRuleMacro.renderShippingMethodDisabledFlag(methodData.identifier) }}</li>
13           {%- endif -%}
14       {% endfor %}
15       {% if methodLabel|length > 0 %}
16           </ul>
17           </li>
18       {% endif %}
19   {% endspaceless %}

Add a Check for When Users Disable Used Shipping Method Types

To show a notification when a user disables or removes the integration currently used in shipping rules, use the event listeners to catch the corresponding event and the event handlers.

Add Event Listeners to the System Container

Append the following lines to <bundle_root>/Resources/config/services.yml under the parameters and services sections:

 1parameters:
 2    acme_fast_shipping.admin_view.method_template: 'ACMEFastShippingBundle::method/fastShippingMethodWithOptions.html.twig'
 3
 4services:
 5    acme_fast_shipping.event_listener.shipping_method_config_data:
 6        parent: oro_shipping.admin_view.method_template.listener
 7        arguments:
 8            - '%acme_fast_shipping.admin_view.method_template%'
 9            - '@acme_fast_shipping.method.provider'
10        tags:
11            - { name: kernel.event_listener, event: oro_shipping_method.config_data, method: onGetConfigData }
12
13    acme_fast_shipping.remove_integration_listener:
14        parent: oro_shipping.remove_integration_listener
15        arguments:
16            - '%acme_fast_shipping.integration.type%'
17            - '@acme_fast_shipping.method.identifier_generator.method'
18            - '@oro_shipping.method.event.dispatcher.method_removal'
19        tags:
20            - { name: kernel.event_listener, event: oro_integration.channel_delete, method: onRemove }
21
22    acme_fast_shipping.disable_integration_listener:
23        parent: oro_shipping.disable_integration_listener
24        arguments:
25            - '%acme_fast_shipping.integration.type%'
26            - '@acme_fast_shipping.method.identifier_generator.method'
27            - '@oro_shipping.method_disable_handler.decorator'
28        tags:
29            - { name: kernel.event_listener, event: oro_integration.channel_disable, method: onIntegrationDisable }

Add Actions

Create actions.yml in the <bundle_root>/Resources/config/oro/ directory:

  1operations:
  2    # Disable the default deactivate method for the Fast Shipping integration.
  3    oro_integration_deactivate:
  4        preconditions:
  5            '@and':
  6                - '@not_equal': [$type, '%acme_fast_shipping.integration.type%']
  7
  8    # Disable the default delete method for the Fast Shipping integration.
  9    oro_integration_delete:
 10        preconditions:
 11            '@and':
 12                - '@not_equal': [$type, '%acme_fast_shipping.integration.type%']
 13
 14    # Use the deactivate method that:
 15    #    a. first checks whether there are shipping rules that use the Fast Shipping method, and
 16    #    b. if yes, displays to a user the confirmation dialog with the notification message and the link to the list of the corresponding rules.
 17    acme_fast_shipping_integration_deactivate:
 18        extends: oro_integration_deactivate
 19        for_all_entities: false
 20        for_all_datagrids: false
 21        replace:
 22            - preactions
 23            - preconditions
 24            - frontend_options
 25
 26        # Filter the grid with the active shipping rules that use the Fast Shipping method and generate the link to it.
 27        preactions:
 28            - '@call_service_method':
 29                  attribute: $.actionAllowed
 30                  service: oro_integration.utils.edit_mode
 31                  method: isSwitchEnableAllowed
 32                  method_parameters: [$.data.editMode]
 33            - '@call_service_method':
 34                  attribute: $.methodIdentifier
 35                  service: acme_fast_shipping.method.identifier_generator.method
 36                  method: generateIdentifier
 37                  method_parameters: [$.data]
 38            - '@call_service_method':
 39                  attribute: $.linkGrid
 40                  service: oro_shipping.helper.filtered_datagrid_route
 41                  method: generate
 42                  method_parameters:  [{'methodConfigs': $.methodIdentifier}]
 43
 44        # Check that the method is used in the shipping rules.
 45        preconditions:
 46            '@and':
 47                - '@shipping_method_has_enabled_shipping_rules':
 48                      parameters:
 49                          method_identifier: $.methodIdentifier
 50                - '@equal': [$type, '%acme_fast_shipping.integration.type%']
 51                - '@equal': [$.actionAllowed, true]
 52                - '@equal': [$.data.enabled, true]
 53
 54        # Show the confirmation dialog with the notification message.
 55        frontend_options:
 56            confirmation:
 57                title: oro.shipping.integration.deactivate.title
 58                okText: oro.shipping.integration.deactivate.button.okText
 59                message: oro.shipping.integration.deactivate.message
 60                message_parameters:
 61                    linkGrid: $.linkGrid
 62                component: oroui/js/standart-confirmation
 63
 64
 65    # If there are no shipping rules that use this method, deactivate without displaying to a user the confirmation dialog.
 66    acme_fast_shipping_integration_deactivate_without_rules:
 67        extends: acme_fast_shipping_integration_deactivate
 68        for_all_entities: false
 69        for_all_datagrids: false
 70        replace:
 71            - preconditions
 72            - frontend_options
 73        preconditions:
 74            '@and':
 75                - '@not':
 76                      - '@shipping_method_has_enabled_shipping_rules':
 77                            parameters:
 78                                method_identifier: $.methodIdentifier
 79                - '@equal': [$type, '%acme_fast_shipping.integration.type%']
 80                - '@equal': [$.actionAllowed, true]
 81                - '@equal': [$.data.enabled, true]
 82        frontend_options: ~
 83
 84    # Use the delete method that:
 85    #    a. first checks whether there are shipping rules that use the Fast Shipping method, and
 86    #    b. if yes, displays to a user the confirmation dialog with the notification message and the link to the list of the corresponding rules.
 87    acme_fast_shipping_integration_delete:
 88        extends: oro_integration_delete
 89        for_all_entities: false
 90        for_all_datagrids: false
 91        replace:
 92            - preactions
 93            - preconditions
 94            - frontend_options
 95        preactions:
 96            - '@call_service_method':
 97                  service: oro_integration.utils.edit_mode
 98                  method: isEditAllowed
 99                  method_parameters: [$.data.editMode]
100                  attribute: $.actionAllowed
101            - '@call_service_method':
102                  attribute: $.methodIdentifier
103                  service: acme_fast_shipping.method.identifier_generator.method
104                  method: generateIdentifier
105                  method_parameters: [$.data]
106            - '@call_service_method':
107                  attribute: $.linkGrid
108                  service: oro_shipping.helper.filtered_datagrid_route
109                  method: generate
110                  method_parameters:  [{'methodConfigs': $.methodIdentifier}]
111        preconditions:
112            '@and':
113                - '@shipping_method_has_shipping_rules':
114                      parameters:
115                          method_identifier: $.methodIdentifier
116                - '@equal': [$type, '%acme_fast_shipping.integration.type%']
117                - '@equal': [$.actionAllowed, true]
118        frontend_options:
119            confirmation:
120                title: oro.shipping.integration.delete.title
121                okText: oro.shipping.integration.delete.button.okText
122                message: oro.shipping.integration.delete.message
123                message_parameters:
124                    linkGrid: $.linkGrid
125                component: oroui/js/standart-confirmation
126
127    acme_fast_shipping_integration_delete_without_rules:
128        extends: acme_fast_shipping_integration_delete
129        for_all_entities: false
130        for_all_datagrids: false
131        replace:
132            - preconditions
133            - frontend_options
134        preconditions:
135            '@and':
136                - '@not':
137                      - '@shipping_method_has_shipping_rules':
138                            parameters:
139                                method_identifier: $.methodIdentifier
140                - '@equal': [$type, '%acme_fast_shipping.integration.type%']
141                - '@equal': [$.actionAllowed, true]
142        frontend_options:
143            title: oro.action.delete_entity
144            confirmation:
145                title: oro.action.delete_entity
146                message: oro.action.delete_confirm
147                message_parameters:
148                    entityLabel: $name
149                component: oroui/js/delete-confirmation

Important

To enable this shipping method, you need to set up a corresponding shipping rule. Follow the Shipping Rules Configuration topic for more details.