🎁 Perplexity PRO offert
Automatically Manage DB Prefix in Doctrine for PrestaShop
You’re developing a PrestaShop module with Doctrine and encounter this frustrating error: Base table or view not found… even though your table definitely exists in the database? The problem likely comes from the dynamic table prefix that PrestaShop adds automatically, but which Doctrine royally ignores.
In my PrestaShop development practice for over 15 years, I’ve encountered this trap on many projects. Today, I’ll show you how to elegantly solve this problem with a custom Doctrine subscriber.
The Symptom That Costs You Hours
Imagine: you’ve just created your perfectly annotated Doctrine entity, you launch your first query and… boom:
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'shop.trade_in_request' doesn't exist
Yet, checking your database, the table exists… but it’s called ps_trade_in_request or shop_trade_in_request depending on the prefix configured during installation.
Why Doctrine Doesn’t Find Your Tables
The problem is fundamental in PrestaShop architecture:
PrestaShop Uses Dynamic Prefixes
In PrestaShop, the table prefix is stored in the _DB_PREFIX_ constant and can vary by installation:
ps_(standard installation)shop_(custom installation)abc123_(for security)- And many other possibilities…
Doctrine Reads Annotations Literally
When you declare your entity like this:
/**
* @ORM\Table(name="trade_in_request")
* @ORM\Entity()
*/
class TradeInRequest
{
// Your properties...
}
Doctrine will search for exactly the trade_in_request table, never adding the PrestaShop prefix.
The Classic Mistake: Hardcoding the Prefix
The temptation is great to do this:
/**
* @ORM\Table(name="ps_trade_in_request") // ❌ NEVER!
* @ORM\Entity()
*/
class TradeInRequest { }
But it’s a very bad idea:
- It will only work on installations with
ps_prefix - Impossible to deploy on multiple environments
- Violation of PrestaShop best practices
The Elegant Solution: A Doctrine Subscriber
The best approach is to intercept Doctrine metadata loading to automatically add the correct prefix at runtime.
Step 1: Create the Subscriber
Create the file src/Doctrine/TablePrefixSubscriber.php in your module:
<?php
namespace Vendor\YourModule\Doctrine;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Events;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
class TablePrefixSubscriber implements EventSubscriber
{
public function __construct(
private readonly string $dbPrefix
) {}
public function getSubscribedEvents(): array
{
return [Events::loadClassMetadata];
}
public function loadClassMetadata(LoadClassMetadataEventArgs $args): void
{
$classMetadata = $args->getClassMetadata();
// Limit to our module entities only
$moduleNamespace = 'Vendor\\YourModule\\Entity\\';
if (!str_starts_with($classMetadata->getName(), $moduleNamespace)) {
return;
}
$this->prefixTableName($classMetadata);
$this->prefixJoinTables($classMetadata);
}
private function prefixTableName($classMetadata): void
{
$tableName = $classMetadata->getTableName();
if (!str_starts_with($tableName, $this->dbPrefix)) {
$classMetadata->setPrimaryTable([
'name' => $this->dbPrefix . $tableName
]);
}
}
private function prefixJoinTables($classMetadata): void
{
foreach ($classMetadata->getAssociationMappings() as &$mapping) {
if (isset($mapping['joinTable']['name'])) {
$joinTableName = $mapping['joinTable']['name'];
if (!str_starts_with($joinTableName, $this->dbPrefix)) {
$mapping['joinTable']['name'] = $this->dbPrefix . $joinTableName;
}
}
}
}
}
Step 2: Declare the Service
In your config/services.yml file:
services:
Vendor\YourModule\Doctrine\TablePrefixSubscriber:
arguments:
- '%database_prefix%'
tags:
- { name: doctrine.event_subscriber }
Step 3: Keep Your Entities Clean
Your entities remain without prefix:
<?php
namespace Vendor\YourModule\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="trade_in_request")
* @ORM\Entity(repositoryClass="Vendor\YourModule\Repository\TradeInRequestRepository")
*/
class TradeInRequest
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private int $id;
/**
* @ORM\Column(type="string", length=255)
*/
private string $customerEmail;
/**
* @ORM\Column(type="datetime")
*/
private \DateTime $createdAt;
// Getters and setters...
}
Step 4: Adapt Your Installation SQL
In your sql/install.sql file, always use the prefix variable:
CREATE TABLE IF NOT EXISTS `{$prefix}trade_in_request` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`customer_email` varchar(255) NOT NULL,
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Deploying the Solution
Clear Symfony Cache
bin/console cache:clear --no-warmup
Reset the Module
bin/console prestashop:module reset yourmodule --no-interaction
Or from the back office: uninstall then reinstall the module.
Handling Complex Relationships
The subscriber also handles join tables automatically. For a ManyToMany relationship:
/**
* @ORM\ManyToMany(targetEntity="Category")
* @ORM\JoinTable(name="trade_in_request_category",
* joinColumns={@ORM\JoinColumn(name="request_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="category_id", referencedColumnName="id")}
* )
*/
private Collection $categories;
The trade_in_request_category table will automatically be prefixed to {prefix}trade_in_request_category.
Testing Your Implementation
Create a simple test to verify everything works:
<?php
namespace Vendor\YourModule\Tests;
use Vendor\YourModule\Entity\CustomerReview;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class TablePrefixTest extends KernelTestCase
{
public function testTablePrefixIsApplied(): void
{
self::bootKernel();
$entityManager = self::getContainer()->get('doctrine.orm.entity_manager');
$metadata = $entityManager->getClassMetadata(CustomerReview::class);
// Verify prefix is properly applied
$expectedTableName = _DB_PREFIX_ . 'customer_review';
$this->assertEquals($expectedTableName, $metadata->getTableName());
}
}
Advantages of This Approach
This solution offers many advantages in my daily practice:
Universal Compatibility
- Works with all database prefixes
- No environment-specific code
- Simplified deployment on different instances
Facilitated Maintenance
- Centralization of prefixing logic
- No code duplication
- Guaranteed scalability
Standards Compliance
- Respect for PrestaShop best practices
- Clean and readable business code
- Separation of concerns
Important Points of Attention
Scope Limitation
Always limit the subscriber to your module entities:
$moduleNamespace = 'Vendor\\YourModule\\Entity\\';
if (!str_starts_with($classMetadata->getName(), $moduleNamespace)) {
return; // Don't touch other entities
}
This precaution avoids conflicts with other modules or PrestaShop core.
SQL/Doctrine Consistency
Ensure your SQL scripts use the same base name as your entities:
- Entity:
@ORM\Table(name="my_table") - SQL:
CREATE TABLE {$prefix}my_table
Testing in Real Conditions
Test with different prefixes to validate your implementation:
// In your test environment
define('_DB_PREFIX_', 'test_');
Conclusion
Automatic table prefix management with Doctrine in PrestaShop isn’t complex once you know the technique. This event subscriber approach offers a robust and maintainable solution that respects platform standards.
Next time you develop a module with Doctrine, remember to implement this subscriber from the start. Your future self (and colleagues) will thank you!
Article published on September 8, 2025 by Nicolas Dabène - PHP & PrestaShop Expert with 15+ years of experience
Questions Fréquentes
Why doesn't Doctrine find my PrestaShop tables?
Doctrine reads annotations literally and searches for exactly the name specified in @ORM\Table(name=”trade_in_request”) without ever adding the PrestaShop prefix (DB_PREFIX). Your table is called ps_trade_in_request but Doctrine searches for trade_in_request, hence the ‘Base table or view not found’ error.
How to elegantly solve the DB prefix problem with Doctrine?
Create a TablePrefixSubscriber that intercepts Doctrine’s loadClassMetadata event to automatically add the correct prefix at runtime. Declare it as a Doctrine service with the doctrine.event_subscriber tag and inject %database_prefix%. This centralized solution is maintainable and compatible with all environments.
Should I hardcode the prefix in Doctrine annotations?
No, never hardcode the prefix like @ORM\Table(name=”ps_trade_in_request”). This will only work on installations with ps_ prefix, make multi-environment deployment impossible, and violate PrestaShop best practices. Instead, use an event subscriber that dynamically manages the prefix.
Does the subscriber also handle ManyToMany join tables?
Yes, the TablePrefixSubscriber automatically handles join tables via the prefixJoinTables() method that traverses associations and prefixes joinTable. For a @ManyToMany relationship with joinTable(name=”trade_in_request_category”), it will automatically be transformed to {prefix}trade_in_request_category.
How to limit the subscriber to my module entities only?
In the loadClassMetadata() method, check the entity namespace with str_starts_with($classMetadata->getName(), ‘Vendor\YourModule\Entity\’). If the entity doesn’t match your module, return immediately. This precaution avoids conflicts with other modules or PrestaShop core.
Articles Liés
Préparer votre boutique PrestaShop pour le Black Friday 2025 avec le module Advanced Search Pro
Découvrez comment le module Advanced Search Pro peut transformer votre boutique PrestaShop en machine à ventes pour l...
Google Shopping & Black Friday 2025 : Guide Complet pour PrestaShop
Découvrez comment optimiser votre présence Google Shopping pour le Black Friday 2025 en Europe et maximiser vos reven...
🧩 Énigme PrestaShop : Saurez-vous trouver les 5 erreurs ?
Développeurs PrestaShop, je vous lance un défi ! Découvrez les 5 erreurs courantes dans ce module de best-sellers et ...
Pourquoi les FAQ sont devenues essentielles pour votre visibilité sur les IA
Les assistants IA changent radicalement la façon dont vos clients vous trouvent. Découvrez pourquoi les FAQ bien stru...
Optimiser vos modules PrestaShop avec le lazy loading des services Symfony
Découvrez comment améliorer significativement les performances de vos modules PrestaShop grâce au lazy loading des se...
L'Évolution de Google Shopping : De Froogle aux Innovations IA (2002-2025)
De simple comparateur gratuit à plateforme IA sophistiquée : découvrez 23 ans d'évolution révolutionnaire de Google S...
Découvrez mes autres articles
Guides e-commerce, tutoriels PrestaShop et bonnes pratiques pour développeurs
Voir tous les articles