DEV Community

Nicolas Dabene
Nicolas Dabene

Posted on • Originally published at nicolas-dabene.fr

PrestaShop Doctrine: Automatically Manage the DB Prefix

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:

<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">'shop.trade_in_request'</span> <span class="n">doesn</span><span class="s1">'t exist
</span>
Enter fullscreen mode Exit fullscreen mode

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:

<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">// Your properties...</span>
<span class="p">}</span>

Enter fullscreen mode Exit fullscreen mode

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:

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

Enter fullscreen mode Exit fullscreen mode

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:

<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">// Limit to our module entities only</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>

Enter fullscreen mode Exit fullscreen mode

Step 2: Declare the Service

In your config/services.yml file:

<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>

Enter fullscreen mode Exit fullscreen mode

Step 3: Keep Your Entities Clean

Your entities remain without prefix:

<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 and setters...</span>
<span class="p">}</span>

Enter fullscreen mode Exit fullscreen mode

Step 4: Adapt Your Installation SQL

In your sql/install.sql file, always use the prefix variable:

<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>

Enter fullscreen mode Exit fullscreen mode

Deploying the Solution

Clear Symfony Cache

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

Enter fullscreen mode Exit fullscreen mode

Reset the Module

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

Enter fullscreen mode Exit fullscreen mode

Or from the back office: uninstall then reinstall the module.

Handling Complex Relationships

The subscriber also handles join tables automatically. For a ManyToMany relationship:

<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>

Enter fullscreen mode Exit fullscreen mode

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:

<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">// Verify prefix is properly applied</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>

Enter fullscreen mode Exit fullscreen mode

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:

<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">// Don't touch other entities</span>
<span class="p">}</span>

Enter fullscreen mode Exit fullscreen mode

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:

<span class="c1">// In your test environment</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>

Enter fullscreen mode Exit fullscreen mode

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

This article was also published on CoderLegion: Master Doctrine in PrestaShop — The Clean Way to Handle Dynamic DB Prefixes.

Top comments (0)