Nicolas Dabene
Retour au blog
08 September 2025 Nicolas Dabene 4 min

Doctrine PrestaShop : gérer automatiquement le préfixe DB

Gérer automatiquement le préfixe DB dans Doctrine pour PrestaShop

PrestaShop développement PHP prestashop-ecommerce
Doctrine PrestaShop : gérer automatiquement le préfixe DB

Gérer automatiquement le préfixe DB dans Doctrine pour PrestaShop

Gérer automatiquement le préfixe DB dans Doctrine pour PrestaShop

Vous développez un module PrestaShop avec Doctrine et vous tombez sur cette erreur frustrante : Base table or view not found… alors que votre table existe bel et bien en base ? Le problème vient probablement du préfixe de table dynamique que PrestaShop ajoute automatiquement, mais que Doctrine ignore royalement.

Dans ma pratique de développement PrestaShop depuis plus de 15 ans, j’ai rencontré ce piège sur de nombreux projets. Aujourd’hui, je vais vous montrer comment résoudre élégamment ce problème avec un subscriber Doctrine personnalisé.

Le symptôme qui vous fait perdre des heures

Imaginez : vous venez de créer votre entité Doctrine parfaitement annotée, vous lancez votre première requête et… boom :

<span class="k">SQLSTATE</span><span class="p">[</span><span class="mi">42</span><span class="n">S02</span><span class="p">]:</span> <span class="n">Base</span> <span class="k">table</span> <span class="k">or</span> <span class="k">view</span> <span class="k">not</span> <span class="k">found</span><span class="p">:</span> <span class="mi">1146</span> <span class="k">Table</span> <span class="s1">'boutique.trade_in_request'</span> <span class="n">doesn</span><span class="s1">'t exist
</span>

Pourtant, en vérifiant votre base de données, la table existe bien… mais elle s’appelle ps_trade_in_request ou shop_trade_in_request selon le préfixe configuré lors de l’installation.

Pourquoi Doctrine ne trouve pas vos tables

Le problème est fondamental dans l’architecture PrestaShop :

PrestaShop utilise des préfixes dynamiques

Dans PrestaShop, le préfixe de table est stocké dans la constante _DB_PREFIX_ et peut varier selon l’installation :

  • ps_ (installation standard)
  • shop_ (installation personnalisée)
  • abc123_ (pour la sécurité)
  • Et bien d’autres possibilités…

Doctrine lit les annotations littéralement

Quand vous déclarez votre entité comme ceci :

<span class="cd">/**
 * @ORM\Table(name="trade_in_request")
 * @ORM\Entity()
 */</span>
<span class="kd">class</span> <span class="nc">TradeInRequest</span>
<span class="p">{</span>
    <span class="c1">// Vos propriétés...</span>
<span class="p">}</span>

Doctrine cherchera exactement la table trade_in_request, sans jamais ajouter le préfixe PrestaShop.

L’erreur classique : préfixer en dur

La tentation est grande de faire ça :

<span class="cd">/**
 * @ORM\Table(name="ps_trade_in_request") // ❌ JAMAIS !
 * @ORM\Entity()
 */</span>
<span class="kd">class</span> <span class="nc">TradeInRequest</span> <span class="p">{</span> <span class="p">}</span>

Mais c’est une très mauvaise idée :

  • Ça ne marchera que sur les installations avec le préfixe ps_
  • Impossible de déployer sur plusieurs environnements
  • Violation des bonnes pratiques PrestaShop

La solution élégante : un subscriber Doctrine

La meilleure approche consiste à intercepter le chargement des métadonnées Doctrine pour ajouter automatiquement le bon préfixe au runtime.

Étape 1 : Créer le subscriber

Créez le fichier src/Doctrine/TablePrefixSubscriber.php dans votre module :

<span class="cp"><?php</span>

<span class="kn">namespace</span> <span class="nn">Vendor\YourModule\Doctrine</span><span class="p">;</span>

<span class="kn">use</span> <span class="nc">Doctrine\Common\EventSubscriber</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Doctrine\ORM\Events</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Doctrine\ORM\Event\LoadClassMetadataEventArgs</span><span class="p">;</span>

<span class="kd">class</span> <span class="nc">TablePrefixSubscriber</span> <span class="kd">implements</span> <span class="nc">EventSubscriber</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">__construct</span><span class="p">(</span>
        <span class="k">private</span> <span class="k">readonly</span> <span class="kt">string</span> <span class="nv">$dbPrefix</span>
    <span class="p">)</span> <span class="p">{}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="n">getSubscribedEvents</span><span class="p">():</span> <span class="kt">array</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="p">[</span><span class="nc">Events</span><span class="o">::</span><span class="n">loadClassMetadata</span><span class="p">];</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="n">loadClassMetadata</span><span class="p">(</span><span class="kt">LoadClassMetadataEventArgs</span> <span class="nv">$args</span><span class="p">):</span> <span class="kt">void</span>
    <span class="p">{</span>
        <span class="nv">$classMetadata</span> <span class="o">=</span> <span class="nv">$args</span><span class="o">-></span><span class="nf">getClassMetadata</span><span class="p">();</span>
        
        <span class="c1">// Limiter aux entités de notre module uniquement</span>
        <span class="nv">$moduleNamespace</span> <span class="o">=</span> <span class="s1">'Vendor\\YourModule\\Entity\\'</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nf">str_starts_with</span><span class="p">(</span><span class="nv">$classMetadata</span><span class="o">-></span><span class="nf">getName</span><span class="p">(),</span> <span class="nv">$moduleNamespace</span><span class="p">))</span> <span class="p">{</span>
            <span class="k">return</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="nv">$this</span><span class="o">-></span><span class="nf">prefixTableName</span><span class="p">(</span><span class="nv">$classMetadata</span><span class="p">);</span>
        <span class="nv">$this</span><span class="o">-></span><span class="nf">prefixJoinTables</span><span class="p">(</span><span class="nv">$classMetadata</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">function</span> <span class="n">prefixTableName</span><span class="p">(</span><span class="nv">$classMetadata</span><span class="p">):</span> <span class="kt">void</span>
    <span class="p">{</span>
        <span class="nv">$tableName</span> <span class="o">=</span> <span class="nv">$classMetadata</span><span class="o">-></span><span class="nf">getTableName</span><span class="p">();</span>
        
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nf">str_starts_with</span><span class="p">(</span><span class="nv">$tableName</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-></span><span class="n">dbPrefix</span><span class="p">))</span> <span class="p">{</span>
            <span class="nv">$classMetadata</span><span class="o">-></span><span class="nf">setPrimaryTable</span><span class="p">([</span>
                <span class="s1">'name'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="n">dbPrefix</span> <span class="mf">.</span> <span class="nv">$tableName</span>
            <span class="p">]);</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">function</span> <span class="n">prefixJoinTables</span><span class="p">(</span><span class="nv">$classMetadata</span><span class="p">):</span> <span class="kt">void</span>
    <span class="p">{</span>
        <span class="k">foreach</span> <span class="p">(</span><span class="nv">$classMetadata</span><span class="o">-></span><span class="nf">getAssociationMappings</span><span class="p">()</span> <span class="k">as</span> <span class="o">&</span><span class="nv">$mapping</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="k">isset</span><span class="p">(</span><span class="nv">$mapping</span><span class="p">[</span><span class="s1">'joinTable'</span><span class="p">][</span><span class="s1">'name'</span><span class="p">]))</span> <span class="p">{</span>
                <span class="nv">$joinTableName</span> <span class="o">=</span> <span class="nv">$mapping</span><span class="p">[</span><span class="s1">'joinTable'</span><span class="p">][</span><span class="s1">'name'</span><span class="p">];</span>
                
                <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nf">str_starts_with</span><span class="p">(</span><span class="nv">$joinTableName</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-></span><span class="n">dbPrefix</span><span class="p">))</span> <span class="p">{</span>
                    <span class="nv">$mapping</span><span class="p">[</span><span class="s1">'joinTable'</span><span class="p">][</span><span class="s1">'name'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="n">dbPrefix</span> <span class="mf">.</span> <span class="nv">$joinTableName</span><span class="p">;</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

Étape 2 : Déclarer le service

Dans votre fichier config/services.yml :

<span class="na">services</span><span class="pi">:</span>
  <span class="na">Vendor\YourModule\Doctrine\TablePrefixSubscriber</span><span class="pi">:</span>
    <span class="na">arguments</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">%database_prefix%'</span>
    <span class="na">tags</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">doctrine.event_subscriber</span> <span class="pi">}</span>

Étape 3 : Garder vos entités propres

Vos entités restent sans préfixe :

<span class="cp"><?php</span>

<span class="kn">namespace</span> <span class="nn">Vendor\YourModule\Entity</span><span class="p">;</span>

<span class="kn">use</span> <span class="nc">Doctrine\ORM\Mapping</span> <span class="k">as</span> <span class="no">ORM</span><span class="p">;</span>

<span class="cd">/**
 * @ORM\Table(name="trade_in_request")
 * @ORM\Entity(repositoryClass="Vendor\YourModule\Repository\TradeInRequestRepository")
 */</span>
<span class="kd">class</span> <span class="nc">TradeInRequest</span>
<span class="p">{</span>
    <span class="cd">/**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */</span>
    <span class="k">private</span> <span class="kt">int</span> <span class="nv">$id</span><span class="p">;</span>

    <span class="cd">/**
     * @ORM\Column(type="string", length=255)
     */</span>
    <span class="k">private</span> <span class="kt">string</span> <span class="nv">$customerEmail</span><span class="p">;</span>

    <span class="cd">/**
     * @ORM\Column(type="datetime")
     */</span>
    <span class="k">private</span> <span class="err">\</span><span class="nc">DateTime</span> <span class="nv">$createdAt</span><span class="p">;</span>

    <span class="c1">// Getters et setters...</span>
<span class="p">}</span>

Étape 4 : Adapter votre SQL d’installation

Dans votre fichier sql/install.sql, utilisez toujours la variable de préfixe :

<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">IF</span> <span class="k">NOT</span> <span class="k">EXISTS</span> <span class="nv">`{$prefix}trade_in_request`</span> <span class="p">(</span>
    <span class="nv">`id`</span> <span class="nb">int</span><span class="p">(</span><span class="mi">11</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
    <span class="nv">`customer_email`</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">255</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
    <span class="nv">`created_at`</span> <span class="nb">datetime</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
    <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="p">(</span><span class="nv">`id`</span><span class="p">)</span>
<span class="p">)</span> <span class="n">ENGINE</span><span class="o">=</span><span class="n">InnoDB</span> <span class="k">DEFAULT</span> <span class="n">CHARSET</span><span class="o">=</span><span class="n">utf8mb4</span> <span class="k">COLLATE</span><span class="o">=</span><span class="n">utf8mb4_unicode_ci</span><span class="p">;</span>

Déployer la solution

Vider le cache Symfony

bin/console cache:clear <span class="nt">--no-warmup</span>

Réinitialiser le module

bin/console prestashop:module reset yourmodule <span class="nt">--no-interaction</span>

Ou depuis le back-office : désinstaller puis réinstaller le module.

Gestion des relations complexes

Le subscriber gère aussi les tables de jointure automatiquement. Pour une relation ManyToMany :

<span class="cd">/**
 * @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")}
 * )
 */</span>
<span class="k">private</span> <span class="kt">Collection</span> <span class="nv">$categories</span><span class="p">;</span>

La table trade_in_request_category sera automatiquement préfixée en {prefix}trade_in_request_category.

Tester votre implémentation

Créez un test simple pour vérifier que tout fonctionne :

<span class="cp"><?php</span>

<span class="kn">namespace</span> <span class="nn">Vendor\YourModule\Tests</span><span class="p">;</span>

<span class="kn">use</span> <span class="nc">Vendor\YourModule\Entity\CustomerReview</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Symfony\Bundle\FrameworkBundle\Test\KernelTestCase</span><span class="p">;</span>

<span class="kd">class</span> <span class="nc">TablePrefixTest</span> <span class="kd">extends</span> <span class="nc">KernelTestCase</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">testTablePrefixIsApplied</span><span class="p">():</span> <span class="kt">void</span>
    <span class="p">{</span>
        <span class="k">self</span><span class="o">::</span><span class="nf">bootKernel</span><span class="p">();</span>
        
        <span class="nv">$entityManager</span> <span class="o">=</span> <span class="k">self</span><span class="o">::</span><span class="nf">getContainer</span><span class="p">()</span><span class="o">-></span><span class="nf">get</span><span class="p">(</span><span class="s1">'doctrine.orm.entity_manager'</span><span class="p">);</span>
        <span class="nv">$metadata</span> <span class="o">=</span> <span class="nv">$entityManager</span><span class="o">-></span><span class="nf">getClassMetadata</span><span class="p">(</span><span class="nc">CustomerReview</span><span class="o">::</span><span class="n">class</span><span class="p">);</span>
        
        <span class="c1">// Vérifier que le préfixe est bien appliqué</span>
        <span class="nv">$expectedTableName</span> <span class="o">=</span> <span class="n">_DB_PREFIX_</span> <span class="mf">.</span> <span class="s1">'customer_review'</span><span class="p">;</span>
        <span class="nv">$this</span><span class="o">-></span><span class="nf">assertEquals</span><span class="p">(</span><span class="nv">$expectedTableName</span><span class="p">,</span> <span class="nv">$metadata</span><span class="o">-></span><span class="nf">getTableName</span><span class="p">());</span>
    <span class="p">}</span>
<span class="p">}</span>

Avantages de cette approche

Cette solution présente de nombreux avantages dans ma pratique quotidienne :

Compatibilité universelle

  • Fonctionne avec tous les préfixes de base de données
  • Aucun code spécifique à un environnement
  • Déploiement simplifié sur différentes instances

Maintenance facilitée

  • Centralisation de la logique de préfixage
  • Pas de duplication de code
  • Évolutivité garantie

Conformité aux standards

  • Respect des bonnes pratiques PrestaShop
  • Code métier propre et lisible
  • Séparation des responsabilités

Points d’attention importants

Limitation du scope

Toujours limiter le subscriber aux entités de votre module :

<span class="nv">$moduleNamespace</span> <span class="o">=</span> <span class="s1">'Vendor\\YourModule\\Entity\\'</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nf">str_starts_with</span><span class="p">(</span><span class="nv">$classMetadata</span><span class="o">-></span><span class="nf">getName</span><span class="p">(),</span> <span class="nv">$moduleNamespace</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">return</span><span class="p">;</span> <span class="c1">// Ne pas toucher aux autres entités</span>
<span class="p">}</span>

Cette précaution évite les conflits avec d’autres modules ou le core PrestaShop.

Cohérence SQL/Doctrine

Assurez-vous que vos scripts SQL utilisent le même nom de base que vos entités :

  • Entité : @ORM\Table(name="my_table")
  • SQL : CREATE TABLE {$prefix}my_table

Test en conditions réelles

Testez avec différents préfixes pour valider votre implémentation :

<span class="c1">// Dans votre environnement de test</span>
<span class="nb">define</span><span class="p">(</span><span class="s1">'_DB_PREFIX_'</span><span class="p">,</span> <span class="s1">'test_'</span><span class="p">);</span>

Conclusion

La gestion automatique des préfixes de tables avec Doctrine dans PrestaShop n’est pas complexe une fois qu’on connaît la technique. Cette approche avec un subscriber événementiel offre une solution robuste et maintenable qui respecte les standards de la plateforme.

La prochaine fois que vous développez un module avec Doctrine, pensez à implémenter ce subscriber dès le démarrage. Votre futur vous-même (et vos collègues) vous remercieront !


Article publié le 8 septembre 2025 par Nicolas Dabène - Expert PHP & PrestaShop avec 15+ ans d’expérience

Cet article est également disponible en anglais sur CoderLegion : Master Doctrine in PrestaShop — The Clean Way to Handle Dynamic DB Prefixes.

LinkedIn

Suivez mes analyses IA et e-commerce

Je partage des retours terrain sur les agents IA, PrestaShop, MCP et l automatisation pour les equipes e-commerce.

Me suivre sur LinkedIn