Back to Top

How to Use Symfony HTTP Client in PrestaShop 9

Updated 27 February 2026

PrestaShop 9 marks a shift in architecture by officially removing Guzzle from its core dependencies.

For years, Guzzle was the standard for HTTP communication in the PHP ecosystem. However, as PrestaShop moves on to Symfony 6.4 LTS, the platform now standardizes on the native Symfony HTTP Client.

While shifting away from a familiar tool might feel disruptive at first.

It is actually a strategic opportunity to modernize your module codebase, making it leaner, faster, and deeply integrated with Symfony’s service container.

In this blog, we will build a complete, installable module from scratch that demonstrates exactly how to integrate external APIs the “PrestaShop 9 way” using Dependency Injection and Services.

Searching for an experienced
Prestashop Company ?
Find out More

Architecture Shift: Procedural → Service-Oriented

The most important conceptual change in PrestaShop 9 is moving from tightly coupled, procedural code inside hooks to clean service-oriented architecture powered by Dependency Injection.

Before PrestaShop 9:

In previous versions, it was common to instantiate HTTP clients directly inside hook methods or controllers. This made the code hard to test, reuse, or maintain:

// Old approach - Business logic, HTTP calls, and error handling all mixed together
public function hookActionValidateOrder(array $params): void
{
    $client = new GuzzleHttp\Client([
        'base_uri' => 'https://api.shipping.com',
        'timeout'  => 10,
    ]);

    $response = $client->post('/shipments', [
        'json' => ['order_id' => $params['id_order']],
    ]);

    $data = json_decode($response->getBody(), true);
    // ... process $data
}

PrestaShop 9:

PrestaShop 9 embraces the Service Container pattern. Your hook method or controllers should do one thing: delegate to a service. HTTP logic lives in a dedicated, injectable, testable service class.

// Good: The hook delegates to a clean service
public function hookActionValidateOrder(array $params): void
{
    $container      = SymfonyContainer::getInstance();
    $shippingService = $container->get(ShippingApiService::class);

    try {
        $shipment = $shippingService->createShipment($params['id_order']);
        // Clean business logic only
    } catch (ShippingException $e) {
        $this->handleShippingError($e);
    }
}
servicearchitectureflow

Building the Module Step by Step

Let’s build wkdailyquote a complete, installable module that fetches a random quote from an external API and displays it on the PrestaShop dashboard.

Following the full PS 9 architecture: DI, services, DTOs, caching, and error handling.

Module Location: Create a folder named wkdailyquote inside your PrestaShop /modules/ directory before starting.

Step 1: Define Composer Autoloading

{
    "name": "wk/wkdailyquote",
    "type": "prestashop-module",
    "require": {
        "php": ">=8.1"
    },
    "autoload": {
        "psr-4": {
            "WkDailyQuote\\": "src/"
        }
    },
    "config": {
        "prepend-autoloader": false
    }
}

Now, navigate to /modules/wkdailyquote/ in your terminal and run:

composer dump-autoload

Step 2: Create the Data Transfer Object (DTO)

A DTO provides a type-safe, validated data structure for the API response. Instead of working with raw arrays throughout your codebase, you pass strongly-typed objects.

File: /modules/wkdailyquote/src/DTO/Quote.php

<?php

namespace WkDailyQuote\DTO;

class Quote
{
    public function __construct(
        private int    $id,
        private string $quote,
        private string $author
    ) {
        if (empty($quote)) {
            throw new \InvalidArgumentException('Quote cannot be empty');
        }
        if (empty($author)) {
            throw new \InvalidArgumentException('Author cannot be empty');
        }
    }
    public static function fromArray(array $data): self
    {
        if (!isset($data['id'], $data['quote'], $data['author'])) {
            throw new \InvalidArgumentException('Invalid API response structure');
        }
        return new self((int)$data['id'], $data['quote'], $data['author']);
    }
    public function getId(): int
    {
        return $this->id;
    }
    public function getQuote(): string
    {
        return $this->quote;
    }
    public function getAuthor(): string
    {
        return $this->author;
    }
    public function toArray(): array
    {
        return ['id' => $this->id, 'quote' => $this->quote, 'author' => $this->author];
    }
}

Step 3: Create Custom Exception Classes

Instead of catching a generic \Exception everywhere, create your own API-specific exception class (or small exception hierarchy).

Using named factory methods (like ApiException::networkError() or ApiException::invalidResponse()) also makes your throw statements clearer and easier to understand.

File: /modules/wkdailyquote/src/Exception/ApiException.php

<?php

namespace WkDailyQuote\Exception;

class ApiException extends \Exception
{
    public static function networkError(string $message, ?\Throwable $prev = null): self
    {
        return new self("Network error: $message", 0, $prev);
    }

    public static function httpError(int $statusCode, string $url): self
    {
        return new self("HTTP $statusCode error when accessing $url");
    }

    public static function invalidResponse(string $message): self
    {
        return new self("Invalid API response: $message");
    }
}

Step 4: Build the Cache Manager Service

Calling an external API on every page load is slow and wastes rate limit quota. The QuoteCacheManager wraps PrestaShop’s built-in cache system with a clean interface.

File: /modules/wkdailyquote/src/Service/QuoteCacheManager.php

<?php

namespace WkDailyQuote\Service;

use WkDailyQuote\DTO\Quote;

class QuoteCacheManager
{
    private const CACHE_KEY = 'wkdailyquote_current';
    private const CACHE_TTL = 3600;

    public function get(): ?Quote
    {
        $cached = \Cache::getInstance()->get(self::CACHE_KEY);

        return $cached ? Quote::fromArray($cached) : null;
    }

    public function set(Quote $quote): void
    {
        \Cache::getInstance()->set(self::CACHE_KEY, $quote->toArray(), self::CACHE_TTL);
    }

    public function clear(): void
    {
        \Cache::getInstance()->delete(self::CACHE_KEY);
    }
}

Step 5: Build the API Service (The Heart of the Module)

Here Symfony HTTP Client is actually used. The QuoteFetcher service receives the client via constructor injection, it never instantiates it directly. This is the key difference from the old approach.

Notice how Symfony’s $response->toArray() handles JSON parsing automatically, and how each error type maps cleanly to a specific exception class via Symfony’s exception hierarchy.

File: /modules/wkdailyquote/src/Service/QuoteFetcher.php

<?php

namespace WkDailyQuote\Service;

use WkDailyQuote\DTO\Quote;
use WkDailyQuote\Exception\ApiException;
use WkDailyQuote\Service\QuoteCacheManager;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;

class QuoteFetcher
{
    private const API_URL = 'https://dummyjson.com/quotes/random';
    private const TIMEOUT = 5;
    private const MAX_RETRIES = 2;

    public function __construct(
        private readonly HttpClientInterface $httpClient,
        private readonly QuoteCacheManager   $cacheManager
    ) {
    }

    public function getRandomQuote(): ?Quote
    {
        if ($cached = $this->cacheManager->get()) {
            return $cached;
        }

        try {
            $quote = $this->fetchWithRetry();
            $this->cacheManager->set($quote);
            return $quote;
        } catch (ApiException $e) {
            \PrestaShopLogger::addLog("[WkDailyQuote] {$e->getMessage()}", 3);
            return null;
        }
    }

    private function fetchWithRetry(): Quote
    {
        for ($attempt = 1; $attempt <= self::MAX_RETRIES; $attempt++) {
            try {
                return $this->makeRequest();
            } catch (ApiException $e) {
                if ($attempt < self::MAX_RETRIES) {
                    usleep(pow(2, $attempt) * 100000);
                }
                $last = $e;
            }
        }
        throw $last;
    }

    private function makeRequest(): Quote
    {
        try {
            $response = $this->httpClient->request('GET', self::API_URL, [
                'timeout' => self::TIMEOUT,
                'headers' => ['Accept' => 'application/json'],
            ]);

            if ($response->getStatusCode() !== 200) {
                throw ApiException::httpError($response->getStatusCode(), self::API_URL);
            }

            return Quote::fromArray($response->toArray());
        } catch (TransportExceptionInterface $e) {
            throw ApiException::networkError($e->getMessage(), $e);
        } catch (\InvalidArgumentException $e) {
            throw ApiException::invalidResponse($e->getMessage());
        }
    }
}

Step 6: Configure Dependency Injection

This YAML file is where the magic happens. It tells Symfony’s container how to wire everything together.

The container sees that QuoteFetcher needs an HttpClientInterface, so it automatically injects the built-in @http_client service — you never instantiate it yourself.

File: /modules/wkdailyquote/config/services.yml

services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

  WkDailyQuote\:
    resource: '../src/*'
    exclude:  '../src/{index.php,DTO}'

  WkDailyQuote\Service\QuoteCacheManager:
    public: true

  WkDailyQuote\Service\QuoteFetcher:
    public: true
    arguments:
      $httpClient:   '@http_client'
      $cacheManager: '@WkDailyQuote\Service\QuoteCacheManager'

Step 7: Create the Main Module File

The main wkdailyquote.php file registers hooks, resolves services from the container, and delegates all real work to them.

Notice there is no HTTP code here whatsoever, that separation of concerns is exactly the goal.

File: /modules/wkdailyquote/wkdailyquote.php

<?php

require_once __DIR__ . '/vendor/autoload.php';

use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
use WkDailyQuote\Service\QuoteFetcher;
use WkDailyQuote\Service\QuoteCacheManager;

class WkDailyQuote extends Module
{
    public function __construct()
    {
        $this->name = 'wkdailyquote';
        $this->tab = 'administration';
        $this->version = '1.0.0';
        $this->author = 'wk';
        $this->need_instance = 0;
        $this->ps_versions_compliancy = [
            'min' => '9.0.0',
            'max' => _PS_VERSION_
        ];
        $this->bootstrap = true;

        parent::__construct();

        $this->displayName = $this->l('Daily Quote');
        $this->description = $this->l('Displays inspirational quotes on your dashboard using Symfony HTTP Client with caching.');
        $this->confirmUninstall = $this->l('Are you sure you want to uninstall this module?');
    }

    public function install(): bool
    {
        return parent::install()  && $this->registerHook('displayDashboardTop');
    }

    public function uninstall(): bool
    {
        $this->clearModuleCache();

        return parent::uninstall();
    }

    public function hookDisplayDashboardTop(array $params)
    {
        if ($this->context->controller->controller_name != 'AdminDashboard') return;

        $container = SymfonyContainer::getInstance();

        if (!$container->has(QuoteFetcher::class)) {
            return '';
        }

        $quote = $container->get(QuoteFetcher::class)->getRandomQuote();

        if (!$quote) {
            return $this->renderErrorMessage();
        }

        $this->context->smarty->assign([
            'quote_text'   => $quote->getQuote(),
            'quote_author' => $quote->getAuthor(),
            'quote_id'     => $quote->getId(),
        ]);

        return $this->display(__FILE__, 'views/templates/hook/dashboard_quote.tpl');
    }

    private function clearModuleCache(): void
    {
        $container = SymfonyContainer::getInstance();

        if ($container->has(QuoteCacheManager::class)) {
            /** @var QuoteCacheManager $cacheManager */
            $cacheManager = $container->get(QuoteCacheManager::class);
            $cacheManager->clear();
        }
    }
}

Step 8: Create the View Templates

File: /modules/wkdailyquote/views/templates/hook/dashboard_quote.tpl

<section id="wkdailyquote" class="panel widget wkdailyquote-container">
  <div class="panel-heading">
    <i class="icon-quote-left"></i>
    {l s='Quote of the Moment' mod='wkdailyquote'}
  </div>
  <div class="panel-body">
    <blockquote class="wkdailyquote-quote">
      <p>{$quote_text|escape:'html':'UTF-8'}</p>
      <footer><cite>{$quote_author|escape:'html':'UTF-8'}</cite></footer>
    </blockquote>
  </div>
</section>

In this example, we are displaying a quote on dashboard using the PrestaShop hook.

While simple, this module will shows the exact architecture required for complex integrations like ERPs, shipping carriers, or payment gateways.

Here is the result below:

wkdailyquote

The architecture you’ve built is designed to scale. Here are four powerful ways to extend its capabilities once the foundation is in place:

  • Authentication for API Requests: Secure your connections by implementing Bearer tokens, API keys, or OAuth2 headers directly within a scoped client configuration.
  • Handling POST Requests with JSON Payloads: Move beyond simple GET requests. The client makes it easy to send data; simply use the json key in your request options to automatically encode arrays and set the Content-Type header.
  • Implementing Rate Limiting: Prevent API bans by using the Rate Limiter component. This ensures your module stays within the provider’s usage limits, even during high-traffic periods.
  • Asynchronous and Parallel Requests: Drastically improve performance when fetching data from multiple sources. The Symfony HTTP Client is non-blocking, allowing you to trigger multiple requests simultaneously and only wait when you actually need the data.

That’s all about this blog.

If any issues or doubts, please feel free to mention them in the comment section.

Or contact us at [email protected]

I would be happy to help.

Also, you can explore our PrestaShop Development Services and a large range of quality PrestaShop Modules.

. . .

Leave a Comment

Your email address will not be published. Required fields are marked*


Be the first to comment.

Back to Top

Message Sent!

If you have more details or questions, you can reply to the received confirmation email.

Back to Home