PrestaShop 9 marks an architectural change by officially removing Guzzle from its core dependencies.
Over the years, Guzzle became the standard for HTTP communications in the PHP ecosystem. However, along with the move to PrestaShop Symfony 6.4 LTSthe platform is now standardizing on the original version Symfony HTTP Client.
Although switching from familiar tools may feel annoying at first.
This is actually a strategic opportunity to modernize your module codebase, making it leaner, faster, and deeply integrated with the Symfony service container.
In this blog, we will create a complete module that can be installed from scratch that shows you exactly how to integrate external APIs into your “PrestaShop 9 ways” use Dependency Injection And Service.
Architecture Shift: Procedural → Service Oriented
The most important conceptual change in PrestaShop 9 is the move from closely related, procedural code inner hook to clean service oriented architecture powered by Dependency Injection.
Before PrestaShop 9:
In previous versions, it was common to instantiate an HTTP client directly within a hook or controller method. This makes the code difficult 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' => '
'timeout' => 10,
]);
$response = $client->post('/shipments', [
'json' => ['order_id' => $params['id_order']],
]);
$data = json_decode($response->getBody(), true);
// ... process $data
}
Presta 9 Store:
PrestaShop 9 includes Service Container pattern. Your hook or controller method should do one thing: delegate to a service. HTTP logic is in a special, injectable, and 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);
}
}
Building a Module Step by Step
Let’s build wkdailyquote a complete installable module that takes random quotes from external APIs and displays them in the PrestaShop dashboard.
Follows the complete architecture of PS 9: DI, services, DTO, caching and error handling.
Module Location: Create a folder named wkdailyquote inside your PrestaShop /module/ 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 a Data Transfer Object (DTO)
DTO provides a type-safe and validated data structures for API responses. Rather than working with raw arrays across the codebase, you pass strongly typed objects.
Submit: /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 a Custom Exception Class
Rather than catching a generic one \Exception wherever, create your own API-specific exception classes (or small exception hierarchies).
Using named factory methods (like ApiException::networkError() or ApiException::invalidResponse()) also create your own throw statements are clearer and easier to understand.
Submit: /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 consumes the speed limit quota. QuoteCacheManager wraps PrestaShop’s built-in cache system in a clean interface.
Submit: /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: Building the API Service (Core of the Module)
Here Symfony HTTP Client actually used. QuoteFetcher service accepts clients via constructor injectionit never instantiates directly. This is the main difference from the old approach.
Notice how Symfony’s $response->toArray() handles JSON parsing automatically, and how each error type is neatly mapped to a specific exception class via Symfony’s exception hierarchy.
Submit: /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 = '
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. This notifies the Symfony container how to put it all together.
The container sees that QuoteFetcher requires an HttpClientInterface, so it automatically includes the built-in @http_client service — you never instantiate it yourself.
Submit: /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 Main Module File
The main thing wkdailyquote.php files register hooks, resolve services from containers, and delegate all the real work to them.
Note that there is no HTTP code whatsoever here, that separation of concerns is the goal.
Submit: /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 a View Template
Submit: /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 display quotes in the dashboard using a PrestaShop hook.
Although simple, this module will demonstrate the exact architecture required for complex integrations such as ERP, shipping carriers, or payment gateways.
Here are the results below:

The architecture you build is designed to scale. Here are four powerful ways to expand your capabilities once the foundation is in place:
- Authentication for API Requests: Secure your connections by implementing Bearer tokens, API keys, or OAuth2 headers directly in scoped client configurations.
- Handling POST Requests with JSON Payloads: Goes beyond a simple GET request. Clients make it easy to send data; just use it
jsonenter your query options to auto-encode the array and set itContent-Typeheader. - Applying Rate Limiting: Prevent API bans by using the Rate Limiter component. This ensures your modules stay within your provider’s usage limits, even during high traffic periods.
- Asynchronous and Parallel Requests: Drastically improves performance when retrieving data from multiple sources. The Symfony HTTP client is non-blocking, allowing you to trigger multiple requests simultaneously and only wait when you really need the data.
That’s all about this blog.
If you have any issues or doubts, please mention them in the comments section.
Or call us at [email protected]
I will be happy to help.
Additionally, you can explore our PrestaShop Development Services and a wide range of quality PrestaShop Modules.
Berita Terkini
Berita Terbaru
Daftar Terbaru
News
Jasa Impor China
Berita Terbaru
Flash News
RuangJP
Pemilu
Berita Terkini
Prediksi Bola
Technology
Otomotif
Berita Terbaru
Teknologi
Berita terkini
Berita Pemilu
Berita Teknologi
Hiburan
master Slote
Berita Terkini
Pendidikan
Resep
Jasa Backlink
Slot gacor terpercaya
Anime Batch
