Skip to content

Extending Modules

This guide explains how to customize and extend the Innosend Magento 2 modules.

Overview

The Innosend modules are designed with extensibility in mind. You can:

  • Replace core implementations via preferences
  • Add functionality via plugins
  • Customize templates and layouts
  • Extend the JavaScript components
  • Add custom event observers

Replacing Core Components

Custom API Client

Create a custom API client implementation:

<?php
// app/code/Your/Module/Model/Api/CustomClient.php

namespace Your\Module\Model\Api;

use Innosend\Integration\Api\ClientInterface;
use Innosend\Integration\Model\Config;
use Psr\Log\LoggerInterface;

class CustomClient implements ClientInterface
{
    private Config $config;
    private LoggerInterface $logger;

    public function __construct(
        Config $config,
        LoggerInterface $logger
    ) {
        $this->config = $config;
        $this->logger = $logger;
    }

    public function get(string $endpoint, array $params = []): array
    {
        // Your custom implementation
        $this->logger->debug('Custom API GET: ' . $endpoint);

        // Add custom headers, authentication, caching, etc.
        return $this->makeRequest('GET', $endpoint, $params);
    }

    public function post(string $endpoint, array $data = []): array
    {
        return $this->makeRequest('POST', $endpoint, $data);
    }

    public function put(string $endpoint, array $data = []): array
    {
        return $this->makeRequest('PUT', $endpoint, $data);
    }

    public function delete(string $endpoint): array
    {
        return $this->makeRequest('DELETE', $endpoint);
    }

    public function isEnabled(): bool
    {
        return $this->config->isEnabled();
    }

    private function makeRequest(string $method, string $endpoint, array $data = []): array
    {
        // Custom request logic
    }
}

Register the preference:

<!-- app/code/Your/Module/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <preference for="Innosend\Integration\Api\ClientInterface"
                type="Your\Module\Model\Api\CustomClient"/>
</config>

Custom Order Mapper

Extend or replace the order mapping logic:

<?php
// app/code/Your/Module/Model/CustomOrderMapper.php

namespace Your\Module\Model;

use Innosend\OrderConnector\Model\OrderMapper;
use Magento\Sales\Api\Data\OrderInterface;

class CustomOrderMapper extends OrderMapper
{
    /**
     * Override to add custom fields
     */
    public function map(OrderInterface $order): array
    {
        // Get default mapping
        $data = parent::map($order);

        // Add custom fields
        $data['custom_field'] = $this->getCustomField($order);
        $data['store_code'] = $order->getStore()->getCode();

        // Modify existing fields
        if (isset($data['items'])) {
            $data['items'] = $this->enrichItems($data['items'], $order);
        }

        return $data;
    }

    private function getCustomField(OrderInterface $order): string
    {
        // Your custom logic
        return 'custom_value';
    }

    private function enrichItems(array $items, OrderInterface $order): array
    {
        // Add additional item data
        return $items;
    }
}

Register the preference:

<!-- app/code/Your/Module/etc/di.xml -->
<preference for="Innosend\OrderConnector\Model\OrderMapper"
            type="Your\Module\Model\CustomOrderMapper"/>

Using Plugins

Modify API Requests

Add data to API requests before they're sent:

<?php
// app/code/Your/Module/Plugin/Api/ClientPlugin.php

namespace Your\Module\Plugin\Api;

use Innosend\Integration\Model\Api\Client;

class ClientPlugin
{
    /**
     * Add custom headers to all requests
     */
    public function beforePost(Client $subject, string $endpoint, array $data = []): array
    {
        // Modify data before sending
        $data['_meta'] = [
            'source' => 'magento',
            'timestamp' => time()
        ];

        return [$endpoint, $data];
    }

    /**
     * Log all responses
     */
    public function afterGet(Client $subject, array $result, string $endpoint): array
    {
        // Log or modify response
        return $result;
    }
}

Register the plugin:

<!-- app/code/Your/Module/etc/di.xml -->
<type name="Innosend\Integration\Model\Api\Client">
    <plugin name="your_module_api_client_plugin"
            type="Your\Module\Plugin\Api\ClientPlugin"/>
</type>

Extend Pickup Point Selection

Add validation or processing when a pickup point is selected:

<?php
// app/code/Your/Module/Plugin/PickupPoints/SavePlugin.php

namespace Your\Module\Plugin\PickupPoints;

use Innosend\PickupPoints\Model\PickupPointRepository;
use Innosend\PickupPoints\Api\Data\QuotePickupPointInterface;
use Magento\Framework\Exception\LocalizedException;

class SavePlugin
{
    /**
     * Validate pickup point before saving
     */
    public function beforeSave(
        PickupPointRepository $subject,
        QuotePickupPointInterface $pickupPoint
    ): array {
        // Add validation
        if ($pickupPoint->getCarrierCode() === 'restricted_carrier') {
            throw new LocalizedException(__('This carrier is not available.'));
        }

        return [$pickupPoint];
    }

    /**
     * Additional processing after save
     */
    public function afterSave(
        PickupPointRepository $subject,
        QuotePickupPointInterface $result
    ): QuotePickupPointInterface {
        // Trigger custom event, logging, etc.
        return $result;
    }
}

Event Observers

Listen to Order Sync

<?php
// app/code/Your/Module/Observer/OrderSyncObserver.php

namespace Your\Module\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Psr\Log\LoggerInterface;

class OrderSyncObserver implements ObserverInterface
{
    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function execute(Observer $observer): void
    {
        $order = $observer->getEvent()->getOrder();

        // Check if order has pickup point
        $pickupPoint = $order->getExtensionAttributes()->getPickupPoint();

        if ($pickupPoint) {
            $this->logger->info(
                'Order with pickup point placed',
                [
                    'order_id' => $order->getIncrementId(),
                    'pickup_point' => $pickupPoint->getName()
                ]
            );

            // Additional processing
            $this->notifyWarehouse($order, $pickupPoint);
        }
    }

    private function notifyWarehouse($order, $pickupPoint): void
    {
        // Your custom logic
    }
}

Register the observer:

<!-- app/code/Your/Module/etc/events.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">

    <event name="sales_order_place_after">
        <observer name="your_module_order_sync"
                  instance="Your\Module\Observer\OrderSyncObserver"/>
    </event>
</config>

Template Customization

Override Templates in Your Theme

Copy templates to your theme:

app/design/frontend/Your/Theme/
└── Innosend_PickupPoints/
    └── templates/
        └── pickup-points/
            ├── checkout.phtml
            ├── modal.phtml
            └── selected.phtml

Custom Pickup Point Display

<!-- app/design/frontend/Your/Theme/Innosend_PickupPoints/templates/pickup-points/selected.phtml -->
<?php
/** @var \Innosend\PickupPoints\Block\Checkout\PickupPoint $block */
?>

<div class="pickup-point-selected custom-style">
    <div class="pickup-point-icon">
        <img src="<?= $block->getCarrierLogo() ?>" alt="Carrier" />
    </div>
    <div class="pickup-point-info">
        <strong><?= $escaper->escapeHtml($block->getPickupPointName()) ?></strong>
        <address>
            <?= $escaper->escapeHtml($block->getPickupPointAddress()) ?>
        </address>
        <!-- Add custom content -->
        <div class="custom-badge">
            <?= __('Ready for pickup in 24h') ?>
        </div>
    </div>
</div>

Override Knockout Templates

Create your own template in your theme:

<!-- app/design/frontend/Your/Theme/Innosend_PickupPoints/web/template/pickup-points/modal.html -->
<div class="modal-popup innosend-modal custom-modal" data-bind="visible: isModalOpen">
    <div class="modal-header">
        <h2 data-bind="i18n: 'Choose Pickup Point'"></h2>
        <button class="action-close" data-bind="click: closeModal">
            <span data-bind="i18n: 'Close'"></span>
        </button>
    </div>

    <div class="modal-body">
        <!-- Custom search bar -->
        <div class="search-container">
            <input type="text"
                   data-bind="value: searchQuery, valueUpdate: 'keyup'"
                   placeholder="Search by city or postal code" />
        </div>

        <!-- Custom carrier filter -->
        <div class="carrier-filter">
            <!-- ko foreach: availableCarriers -->
            <button data-bind="click: $parent.filterByCarrier,
                              css: { active: $parent.selectedCarrier() === $data }">
                <img data-bind="attr: { src: logoUrl, alt: name }" />
            </button>
            <!-- /ko -->
        </div>

        <!-- Pickup points list -->
        <div class="pickup-points-list" data-bind="foreach: filteredPickupPoints">
            <!-- Your custom item template -->
        </div>
    </div>
</div>

JavaScript Customization

Extend the Pickup Points Component

// app/design/frontend/Your/Theme/Innosend_PickupPoints/web/js/custom-pickup-points.js

define([
    'Innosend_PickupPoints/js/pickup-points',
    'ko',
    'jquery'
], function (PickupPoints, ko, $) {
    'use strict';

    return PickupPoints.extend({
        defaults: {
            // Add custom defaults
            customOption: true,
            analyticsEnabled: true
        },

        /**
         * Override initialize
         */
        initialize: function () {
            this._super();

            // Add custom initialization
            this.initAnalytics();

            return this;
        },

        /**
         * Override pickup point selection
         */
        selectPickupPoint: function (pickupPoint) {
            // Custom pre-selection logic
            if (this.analyticsEnabled) {
                this.trackSelection(pickupPoint);
            }

            // Call parent method
            this._super(pickupPoint);

            // Custom post-selection logic
            this.showConfirmation(pickupPoint);
        },

        /**
         * Custom method: Track selection
         */
        trackSelection: function (pickupPoint) {
            // Analytics tracking
            if (typeof gtag !== 'undefined') {
                gtag('event', 'pickup_point_selected', {
                    'carrier': pickupPoint.carrier,
                    'city': pickupPoint.city
                });
            }
        },

        /**
         * Custom method: Show confirmation
         */
        showConfirmation: function (pickupPoint) {
            // Show toast notification
            require(['Magento_Ui/js/model/messageList'], function (messageList) {
                messageList.addSuccessMessage({
                    message: 'Pickup point selected: ' + pickupPoint.name
                });
            });
        },

        /**
         * Custom method: Initialize analytics
         */
        initAnalytics: function () {
            // Setup analytics
        }
    });
});

Register the custom component:

// app/design/frontend/Your/Theme/Innosend_PickupPoints/requirejs-config.js
var config = {
    map: {
        '*': {
            'Innosend_PickupPoints/js/pickup-points': 'Innosend_PickupPoints/js/custom-pickup-points'
        }
    }
};

Add Custom Mixins

// app/design/frontend/Your/Theme/Innosend_PickupPoints/web/js/pickup-points-mixin.js

define([
    'jquery'
], function ($) {
    'use strict';

    return function (PickupPoints) {
        return PickupPoints.extend({
            /**
             * Add method to filter by distance
             */
            filterByDistance: function (maxDistance) {
                var filtered = this.pickupPoints().filter(function (point) {
                    return point.distance <= maxDistance;
                });

                this.filteredPickupPoints(filtered);
            },

            /**
             * Add method to sort by carrier
             */
            sortByCarrier: function () {
                var sorted = this.pickupPoints().sort(function (a, b) {
                    return a.carrier.localeCompare(b.carrier);
                });

                this.pickupPoints(sorted);
            }
        });
    };
});

Register the mixin:

// app/design/frontend/Your/Theme/requirejs-config.js
var config = {
    config: {
        mixins: {
            'Innosend_PickupPoints/js/pickup-points': {
                'Innosend_PickupPoints/js/pickup-points-mixin': true
            }
        }
    }
};

Layout XML Customization

Add Custom Block to Checkout

<!-- app/design/frontend/Your/Theme/Innosend_PickupPoints/layout/checkout_index_index.xml -->
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="checkout.root">
            <block class="Your\Module\Block\Checkout\CustomInfo"
                   name="innosend.pickup.custom.info"
                   template="Your_Module::checkout/pickup-info.phtml"
                   before="innosend.pickup.point"/>
        </referenceContainer>
    </body>
</page>

Modify Admin Order View

<!-- app/code/Your/Module/view/adminhtml/layout/sales_order_view.xml -->
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="order_additional_info">
            <block class="Your\Module\Block\Adminhtml\Order\PickupPointActions"
                   name="pickup.point.actions"
                   template="Your_Module::order/pickup-point-actions.phtml"/>
        </referenceContainer>
    </body>
</page>

Best Practices

Do's

  • ✅ Use dependency injection instead of ObjectManager
  • ✅ Extend existing classes rather than replacing them entirely
  • ✅ Use plugins for targeted modifications
  • ✅ Keep customizations in a separate module
  • ✅ Write unit tests for custom code
  • ✅ Document your customizations

Don'ts

  • ❌ Don't modify core Innosend module files directly
  • ❌ Don't use class rewrites (preferences) unless necessary
  • ❌ Don't break existing functionality
  • ❌ Don't hardcode values—use configuration
  • ❌ Don't ignore Magento coding standards

Module Structure Example

app/code/Your/InnosendCustomizations/
├── Block/
│   ├── Adminhtml/
│   │   └── Order/
│   │       └── PickupPointActions.php
│   └── Checkout/
│       └── CustomInfo.php
├── Model/
│   └── CustomOrderMapper.php
├── Observer/
│   └── OrderSyncObserver.php
├── Plugin/
│   └── Api/
│       └── ClientPlugin.php
├── etc/
│   ├── di.xml
│   ├── events.xml
│   ├── module.xml
│   └── frontend/
│       └── routes.xml
├── registration.php
└── view/
    ├── adminhtml/
    │   └── layout/
    │       └── sales_order_view.xml
    └── frontend/
        ├── layout/
        │   └── checkout_index_index.xml
        ├── templates/
        │   └── checkout/
        │       └── pickup-info.phtml
        └── web/
            └── js/
                └── custom-pickup-points.js

Testing Your Extensions

Unit Tests

<?php
// Test/Unit/Model/CustomOrderMapperTest.php

namespace Your\Module\Test\Unit\Model;

use PHPUnit\Framework\TestCase;
use Your\Module\Model\CustomOrderMapper;

class CustomOrderMapperTest extends TestCase
{
    private CustomOrderMapper $mapper;

    protected function setUp(): void
    {
        // Setup mocks and instantiate
    }

    public function testMapIncludesCustomFields(): void
    {
        $order = $this->createMock(\Magento\Sales\Api\Data\OrderInterface::class);

        $result = $this->mapper->map($order);

        $this->assertArrayHasKey('custom_field', $result);
        $this->assertArrayHasKey('store_code', $result);
    }
}

Integration Tests

<?php
// Test/Integration/PickupPointCustomizationTest.php

namespace Your\Module\Test\Integration;

use Magento\TestFramework\TestCase\AbstractController;

class PickupPointCustomizationTest extends AbstractController
{
    public function testCustomPickupPointValidation(): void
    {
        // Test your customizations in integration context
    }
}