🔧 ProgrammierungWhy client SDK generation belongs in the open(17.09.2026 um 21:18 Uhr)
🔧 ProgrammierungAn AI System Grows Upward, Not Sideways(17.09.2026 um 20:39 Uhr)
🔧 ProgrammierungHow I Implemented Memory in my AI Agent REXA(17.09.2026 um 20:41 Uhr)
🔧 ProgrammierungBuilding Privacy-First Web Utilities with Utilvo(17.09.2026 um 20:43 Uhr)
🔧 ProgrammierungHow to Build an AI Assistant in Termux With Python(17.09.2026 um 20:46 Uhr)
🔧 Programmierungorbitdesk-modern-workplace-operations-lab-webrtc-team-calls(17.09.2026 um 20:50 Uhr)
🔧 ProgrammierungTool Poisoning on MCP Servers: The Attack Vector Nobody's Patching(17.09.2026 um 20:57 Uhr)
🔧 ProgrammierungData Preparation for AI Agents(17.09.2026 um 20:59 Uhr)
⚠️ Malware / Trojaner / VirenRansomware Operators Are Using AI Coding Agents Now(17.09.2026 um 20:59 Uhr)
🔧 ProgrammierungWhy client SDK generation belongs in the open(17.09.2026 um 21:18 Uhr)
🔧 ProgrammierungAn AI System Grows Upward, Not Sideways(17.09.2026 um 20:39 Uhr)
🔧 ProgrammierungHow I Implemented Memory in my AI Agent REXA(17.09.2026 um 20:41 Uhr)
🔧 ProgrammierungBuilding Privacy-First Web Utilities with Utilvo(17.09.2026 um 20:43 Uhr)
🔧 ProgrammierungHow to Build an AI Assistant in Termux With Python(17.09.2026 um 20:46 Uhr)
🔧 Programmierungorbitdesk-modern-workplace-operations-lab-webrtc-team-calls(17.09.2026 um 20:50 Uhr)
🔧 ProgrammierungTool Poisoning on MCP Servers: The Attack Vector Nobody's Patching(17.09.2026 um 20:57 Uhr)
🔧 ProgrammierungData Preparation for AI Agents(17.09.2026 um 20:59 Uhr)
⚠️ Malware / Trojaner / VirenRansomware Operators Are Using AI Coding Agents Now(17.09.2026 um 20:59 Uhr)
🔧 Programmierung 🕛 vor 3 Monaten 22 Min Lesezeit
0

PrestaShop Doctrine: Automatically Manage the DB Prefix

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht




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:




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






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:




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







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:




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







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:




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










Step 2: Declare the Service



In your config/services.yml file:




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










Step 3: Keep Your Entities Clean



Your entities remain without prefix:




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










Step 4: Adapt Your Installation SQL



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




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










Deploying the Solution






Clear Symfony Cache






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










Reset the Module






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







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






Handling Complex Relationships



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




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







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:




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










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:




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







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:




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










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.

Vollständiger Original-Artikel
Den kompletten Beitrag mit allen Details direkt auf dev.to lesen.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
I wore flagship earbuds from Sony and Sennheiser – it’s a tough call, but one gets it right
1 Quelle
This sleep-tracking alarm clock doesn’t include a ‘score’, and that’s why I love it
1 Quelle
Why client SDK generation belongs in the open
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten PrestaShop Doctrine: Automatically Manage the DB Prefix

Thematisch verwandte Begriffe: PrestaShop, Doctrine, Automatically, Manage · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...