Nicolas Dabene
Back to blog
24 February 2026 Nicolas Dabene 8 min

Vibe Coding in e-commerce: why 80% of AI-generated modules will never make it to production

Vibe Coding in e-commerce: why 80% of AI-generated modules will never make it to production

PrestaShop Vibe Coding Artificial Intelligence e-commerce security performance multi-shop technical debt hooks modules Development Security prestashop-ecommerce
Vibe Coding in e-commerce: why 80% of AI-generated modules will never make it to production

Vibe Coding in e-commerce: why 80% of AI-generated modules will never make it to production

Vibe Coding in e-commerce: why 80% of AI-generated modules will never make it to production

Reading time: 15 min****Last updated: February 2026

The dream they’re selling vs. the reality on the ground

“Describe what you want, AI codes it for you.”

Since Gene Kim popularized the concept of Vibe Coding and tools like Cursor, Claude Code, and GitHub Copilot exploded onto the scene, an enticing narrative has taken hold: anyone can now create software. No need to understand the code — just “give the vibe.”

And honestly? For a prototype, a demo, a side project… it works. It’s even impressive.

But I’ve been developing PrestaShop modules for over 10 years. Modules running on stores processing 50,000 orders per month. Modules installed on multi-shop architectures with 12 stores, 4 languages, 3 currencies, business rules specific to each customer group, and an ERP plugged in behind it all.

And what I’ve been seeing over the past 6 months in my audits, code takeovers, and support requests is a wave of “vibe-coded” modules that all share the same fate:

They work in demo. They break in production.

This article isn’t an anti-AI manifesto. I use AI every day in my workflow. But I’m going to show you, with concrete examples and real code, why Vibe Coding applied to e-commerce — and specifically to PrestaShop — is a minefield that only domain expertise can navigate.


1. Hooks: the #1 trap AI doesn’t understand

What AI generates

Ask an LLM to create a module that displays a reassurance block on the product page. You’ll get something like:

<span class="k">public</span> <span class="k">function</span> <span class="n">hookDisplayProductAdditionalInfo</span><span class="p">(</span><span class="nv">$params</span><span class="p">)</span>
<span class="p">{</span>
    <span class="nv">$product</span> <span class="o">=</span> <span class="nv">$params</span><span class="p">[</span><span class="s1">'product'</span><span class="p">];</span>

    <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">smarty</span><span class="o">-></span><span class="nf">assign</span><span class="p">([</span>
        <span class="s1">'product_name'</span> <span class="o">=></span> <span class="nv">$product</span><span class="o">-></span><span class="n">name</span><span class="p">,</span>
        <span class="s1">'reassurance_text'</span> <span class="o">=></span> <span class="s1">'Free shipping over €49'</span><span class="p">,</span>
    <span class="p">]);</span>

    <span class="k">return</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">display</span><span class="p">(</span><span class="k">__FILE__</span><span class="p">,</span> <span class="s1">'views/templates/hook/reassurance.tpl'</span><span class="p">);</span>
<span class="p">}</span>

Looks clean. Works on a fresh PrestaShop install. The client is happy for 48 hours.

What blows up in production

Problem 1: $params['product'] isn’t what you think it is.

Depending on the PrestaShop version (1.7.6 vs 1.7.8 vs 8.1), whether you’re on the standard product page or in a quick-view module, depending on the theme used… $params['product'] can be:

  • A Product object
  • An associative array (from ProductPresenter)
  • null (if the hook is called in an unexpected context)
  • An array with different keys depending on the version

Robust code looks like this:

<span class="k">public</span> <span class="k">function</span> <span class="n">hookDisplayProductAdditionalInfo</span><span class="p">(</span><span class="nv">$params</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// Defensive handling of the product parameter</span>
    <span class="nv">$product</span> <span class="o">=</span> <span class="kc">null</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">$params</span><span class="p">[</span><span class="s1">'product'</span><span class="p">]))</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nb">is_array</span><span class="p">(</span><span class="nv">$params</span><span class="p">[</span><span class="s1">'product'</span><span class="p">]))</span> <span class="p">{</span>
            <span class="c1">// PrestaShop 1.7.7+ with ProductPresenter</span>
            <span class="nv">$productId</span> <span class="o">=</span> <span class="p">(</span><span class="n">int</span><span class="p">)</span> <span class="p">(</span><span class="nv">$params</span><span class="p">[</span><span class="s1">'product'</span><span class="p">][</span><span class="s1">'id_product'</span><span class="p">]</span> <span class="o">??</span> <span class="nv">$params</span><span class="p">[</span><span class="s1">'product'</span><span class="p">][</span><span class="s1">'id'</span><span class="p">]</span> <span class="o">??</span> <span class="mi">0</span><span class="p">);</span>
            <span class="k">if</span> <span class="p">(</span><span class="nv">$productId</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
                <span class="nv">$product</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Product</span><span class="p">(</span><span class="nv">$productId</span><span class="p">,</span> <span class="kc">false</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">language</span><span class="o">-></span><span class="n">id</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">shop</span><span class="o">-></span><span class="n">id</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">}</span> <span class="k">elseif</span> <span class="p">(</span><span class="nv">$params</span><span class="p">[</span><span class="s1">'product'</span><span class="p">]</span> <span class="k">instanceof</span> <span class="nc">Product</span><span class="p">)</span> <span class="p">{</span>
            <span class="nv">$product</span> <span class="o">=</span> <span class="nv">$params</span><span class="p">[</span><span class="s1">'product'</span><span class="p">];</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="c1">// Fallback to context if the hook doesn't provide anything usable</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nc">Validate</span><span class="o">::</span><span class="nf">isLoadedObject</span><span class="p">(</span><span class="nv">$product</span><span class="p">))</span> <span class="p">{</span>
        <span class="nv">$productId</span> <span class="o">=</span> <span class="p">(</span><span class="n">int</span><span class="p">)</span> <span class="nc">Tools</span><span class="o">::</span><span class="nf">getValue</span><span class="p">(</span><span class="s1">'id_product'</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="nv">$productId</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
            <span class="nv">$product</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Product</span><span class="p">(</span><span class="nv">$productId</span><span class="p">,</span> <span class="kc">false</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">language</span><span class="o">-></span><span class="n">id</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">shop</span><span class="o">-></span><span class="n">id</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nc">Validate</span><span class="o">::</span><span class="nf">isLoadedObject</span><span class="p">(</span><span class="nv">$product</span><span class="p">)</span> <span class="o">||</span> <span class="o">!</span><span class="nv">$product</span><span class="o">-></span><span class="n">active</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="s1">''</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// ... rest of the processing</span>
<span class="p">}</span>

Sexy? No. Necessary? Absolutely.

Problem 2: the hook might not even exist.

AI will suggest hookDisplayProductAdditionalInfo because it’s in the official documentation. But on a real store with a custom theme (Flavor, Warehouse, etc.), this hook:

  • may have been removed from the template
  • may be called at a different position in the DOM
  • may be competing with 4 other modules registered on it

A senior developer knows to check the target theme, offer a widget as an alternative, and implement a fallback with hookDisplayFooterProduct or hookDisplayOverrideTemplate if needed.

Problem 3: forgotten action hooks.

AI generates display hooks very well. It systematically forgets critical action hooks. A vibe-coded stock management module I audited last month:

  • Handled hookActionProductUpdate to recalculate stock
  • Forgot hookActionObjectProductDeleteAfter → ghost products in the database
  • Forgot hookActionProductAttributeUpdate → product combinations never synchronized
  • Forgot hookActionObjectCombinationDeleteAfter → ERP crash
  • Didn’t handle hookActionObjectStockAvailableUpdateAfter → conflict with native stock

One forgotten hook = inconsistent data across hundreds of products.


2. Security: the gaping hole AI leaves wide open

Unprotected AJAX

This is the pattern I find in 90% of vibe-coded modules with an admin interface:

<span class="c1">// front/ajax.php — AI-generated</span>
<span class="k">include</span><span class="p">(</span><span class="s1">'../../config/config.inc.php'</span><span class="p">);</span>

<span class="nv">$action</span> <span class="o">=</span> <span class="nc">Tools</span><span class="o">::</span><span class="nf">getValue</span><span class="p">(</span><span class="s1">'action'</span><span class="p">);</span>

<span class="k">if</span> <span class="p">(</span><span class="nv">$action</span> <span class="o">===</span> <span class="s1">'updatePrice'</span><span class="p">)</span> <span class="p">{</span>
    <span class="nv">$id_product</span> <span class="o">=</span> <span class="nc">Tools</span><span class="o">::</span><span class="nf">getValue</span><span class="p">(</span><span class="s1">'id_product'</span><span class="p">);</span>
    <span class="nv">$new_price</span> <span class="o">=</span> <span class="nc">Tools</span><span class="o">::</span><span class="nf">getValue</span><span class="p">(</span><span class="s1">'price'</span><span class="p">);</span>

    <span class="nv">$product</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Product</span><span class="p">(</span><span class="nv">$id_product</span><span class="p">);</span>
    <span class="nv">$product</span><span class="o">-></span><span class="n">price</span> <span class="o">=</span> <span class="nv">$new_price</span><span class="p">;</span>
    <span class="nv">$product</span><span class="o">-></span><span class="nf">save</span><span class="p">();</span>

    <span class="k">die</span><span class="p">(</span><span class="nb">json_encode</span><span class="p">([</span><span class="s1">'success'</span> <span class="o">=></span> <span class="kc">true</span><span class="p">]));</span>
<span class="p">}</span>

This code is a wide-open door. Anyone can change the price of any product in the store with a simple curl call:

curl <span class="s2">"https://your-store.com/modules/mymodule/front/ajax.php?action=updatePrice&id_product=1&price=0.01"</span>

Congratulations: all your products are now 1 cent.

What secure code requires

<span class="c1">// controllers/front/ajax.php — secure version</span>
<span class="kd">class</span> <span class="nc">MyModuleAjaxModuleFrontController</span> <span class="kd">extends</span> <span class="nc">ModuleFrontController</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">initContent</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="c1">// Verify this is actually an AJAX request</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$this</span><span class="o">-></span><span class="n">ajax</span><span class="p">)</span> <span class="p">{</span>
            <span class="nv">$this</span><span class="o">-></span><span class="nf">ajaxRender</span><span class="p">(</span><span class="nb">json_encode</span><span class="p">([</span><span class="s1">'error'</span> <span class="o">=></span> <span class="s1">'Invalid request'</span><span class="p">]));</span>
            <span class="k">return</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">parent</span><span class="o">::</span><span class="nf">initContent</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="n">displayAjaxUpdatePrice</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="c1">// 1. CSRF token verification</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$this</span><span class="o">-></span><span class="nf">isTokenValid</span><span class="p">())</span> <span class="p">{</span>
            <span class="nb">header</span><span class="p">(</span><span class="s1">'HTTP/1.1 403 Forbidden'</span><span class="p">);</span>
            <span class="nv">$this</span><span class="o">-></span><span class="nf">ajaxRender</span><span class="p">(</span><span class="nb">json_encode</span><span class="p">([</span><span class="s1">'error'</span> <span class="o">=></span> <span class="s1">'Invalid token'</span><span class="p">]));</span>
            <span class="k">return</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// 2. Permission check (admin employee logged in)</span>
        <span class="nv">$cookie</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Cookie</span><span class="p">(</span><span class="s1">'psAdmin'</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$cookie</span><span class="o">-></span><span class="n">id_employee</span><span class="p">)</span> <span class="p">{</span>
            <span class="nb">header</span><span class="p">(</span><span class="s1">'HTTP/1.1 401 Unauthorized'</span><span class="p">);</span>
            <span class="nv">$this</span><span class="o">-></span><span class="nf">ajaxRender</span><span class="p">(</span><span class="nb">json_encode</span><span class="p">([</span><span class="s1">'error'</span> <span class="o">=></span> <span class="s1">'Unauthorized'</span><span class="p">]));</span>
            <span class="k">return</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="nv">$employee</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Employee</span><span class="p">((</span><span class="n">int</span><span class="p">)</span> <span class="nv">$cookie</span><span class="o">-></span><span class="n">id_employee</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nc">Validate</span><span class="o">::</span><span class="nf">isLoadedObject</span><span class="p">(</span><span class="nv">$employee</span><span class="p">)</span>
            <span class="o">||</span> <span class="o">!</span><span class="nv">$employee</span><span class="o">-></span><span class="nf">hasAuthOnShop</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">shop</span><span class="o">-></span><span class="n">id</span><span class="p">))</span> <span class="p">{</span>
            <span class="nb">header</span><span class="p">(</span><span class="s1">'HTTP/1.1 403 Forbidden'</span><span class="p">);</span>
            <span class="nv">$this</span><span class="o">-></span><span class="nf">ajaxRender</span><span class="p">(</span><span class="nb">json_encode</span><span class="p">([</span><span class="s1">'error'</span> <span class="o">=></span> <span class="s1">'Insufficient permissions'</span><span class="p">]));</span>
            <span class="k">return</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// 3. Strict input validation</span>
        <span class="nv">$idProduct</span> <span class="o">=</span> <span class="p">(</span><span class="n">int</span><span class="p">)</span> <span class="nc">Tools</span><span class="o">::</span><span class="nf">getValue</span><span class="p">(</span><span class="s1">'id_product'</span><span class="p">);</span>
        <span class="nv">$newPrice</span> <span class="o">=</span> <span class="p">(</span><span class="n">float</span><span class="p">)</span> <span class="nc">Tools</span><span class="o">::</span><span class="nf">getValue</span><span class="p">(</span><span class="s1">'price'</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="nv">$idProduct</span> <span class="o"><=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
            <span class="nv">$this</span><span class="o">-></span><span class="nf">ajaxRender</span><span class="p">(</span><span class="nb">json_encode</span><span class="p">([</span><span class="s1">'error'</span> <span class="o">=></span> <span class="s1">'Invalid product ID'</span><span class="p">]));</span>
            <span class="k">return</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">if</span> <span class="p">(</span><span class="nv">$newPrice</span> <span class="o"><</span> <span class="mi">0</span> <span class="o">||</span> <span class="nv">$newPrice</span> <span class="o">></span> <span class="mf">999999.99</span><span class="p">)</span> <span class="p">{</span>
            <span class="nv">$this</span><span class="o">-></span><span class="nf">ajaxRender</span><span class="p">(</span><span class="nb">json_encode</span><span class="p">([</span><span class="s1">'error'</span> <span class="o">=></span> <span class="s1">'Invalid price range'</span><span class="p">]));</span>
            <span class="k">return</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// 4. Verify the product belongs to the current shop context</span>
        <span class="nv">$product</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Product</span><span class="p">(</span><span class="nv">$idProduct</span><span class="p">,</span> <span class="kc">false</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">shop</span><span class="o">-></span><span class="n">id</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nc">Validate</span><span class="o">::</span><span class="nf">isLoadedObject</span><span class="p">(</span><span class="nv">$product</span><span class="p">))</span> <span class="p">{</span>
            <span class="nv">$this</span><span class="o">-></span><span class="nf">ajaxRender</span><span class="p">(</span><span class="nb">json_encode</span><span class="p">([</span><span class="s1">'error'</span> <span class="o">=></span> <span class="s1">'Product not found in current shop'</span><span class="p">]));</span>
            <span class="k">return</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// 5. Secure update with multi-shop handling</span>
        <span class="nv">$product</span><span class="o">-></span><span class="n">price</span> <span class="o">=</span> <span class="nv">$newPrice</span><span class="p">;</span>

        <span class="k">if</span> <span class="p">(</span><span class="nc">Shop</span><span class="o">::</span><span class="nf">getContext</span><span class="p">()</span> <span class="o">===</span> <span class="nc">Shop</span><span class="o">::</span><span class="no">CONTEXT_SHOP</span><span class="p">)</span> <span class="p">{</span>
            <span class="nv">$product</span><span class="o">-></span><span class="nf">save</span><span class="p">();</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="c1">// In multi-shop context, force current shop</span>
            <span class="nv">$product</span><span class="o">-></span><span class="n">id_shop_default</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">shop</span><span class="o">-></span><span class="n">id</span><span class="p">;</span>
            <span class="nv">$product</span><span class="o">-></span><span class="nf">save</span><span class="p">();</span>
        <span class="p">}</span>

        <span class="c1">// 6. Log the action for audit trail</span>
        <span class="nc">PrestaShopLogger</span><span class="o">::</span><span class="nf">addLog</span><span class="p">(</span>
            <span class="nb">sprintf</span><span class="p">(</span><span class="s1">'Product #%d price updated to %f by employee #%d via module MyModule'</span><span class="p">,</span>
                <span class="nv">$idProduct</span><span class="p">,</span> <span class="nv">$newPrice</span><span class="p">,</span> <span class="p">(</span><span class="n">int</span><span class="p">)</span> <span class="nv">$cookie</span><span class="o">-></span><span class="n">id_employee</span><span class="p">),</span>
            <span class="mi">1</span><span class="p">,</span>
            <span class="kc">null</span><span class="p">,</span>
            <span class="s1">'Product'</span><span class="p">,</span>
            <span class="nv">$idProduct</span><span class="p">,</span>
            <span class="kc">true</span><span class="p">,</span>
            <span class="p">(</span><span class="n">int</span><span class="p">)</span> <span class="nv">$cookie</span><span class="o">-></span><span class="n">id_employee</span>
        <span class="p">);</span>

        <span class="c1">// 7. Flush product price cache</span>
        <span class="nc">Product</span><span class="o">::</span><span class="nf">flushPriceCache</span><span class="p">();</span>

        <span class="nv">$this</span><span class="o">-></span><span class="nf">ajaxRender</span><span class="p">(</span><span class="nb">json_encode</span><span class="p">([</span>
            <span class="s1">'success'</span> <span class="o">=></span> <span class="kc">true</span><span class="p">,</span>
            <span class="s1">'product_id'</span> <span class="o">=></span> <span class="nv">$idProduct</span><span class="p">,</span>
            <span class="s1">'new_price'</span> <span class="o">=></span> <span class="nv">$newPrice</span><span class="p">,</span>
        <span class="p">]));</span>
    <span class="p">}</span>
<span class="p">}</span>

We went from 8 lines to 65 lines. And every additional line blocks a real attack vector.

SQL injections: still here in 2026

AI loves generating “readable” SQL queries:

<span class="c1">// AI-generated — SQL injection</span>
<span class="nv">$sql</span> <span class="o">=</span> <span class="s2">"SELECT * FROM "</span> <span class="mf">.</span> <span class="n">_DB_PREFIX_</span> <span class="mf">.</span> <span class="s2">"product
        WHERE reference = '"</span> <span class="mf">.</span> <span class="nv">$reference</span> <span class="mf">.</span> <span class="s2">"'"</span><span class="p">;</span>
<span class="nv">$results</span> <span class="o">=</span> <span class="nc">Db</span><span class="o">::</span><span class="nf">getInstance</span><span class="p">()</span><span class="o">-></span><span class="nf">executeS</span><span class="p">(</span><span class="nv">$sql</span><span class="p">);</span>

<span class="c1">// Secure version</span>
<span class="nv">$sql</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DbQuery</span><span class="p">();</span>
<span class="nv">$sql</span><span class="o">-></span><span class="nf">select</span><span class="p">(</span><span class="s1">'p.id_product, p.reference, p.price, pl.name'</span><span class="p">);</span>
<span class="nv">$sql</span><span class="o">-></span><span class="nf">from</span><span class="p">(</span><span class="s1">'product'</span><span class="p">,</span> <span class="s1">'p'</span><span class="p">);</span>
<span class="nv">$sql</span><span class="o">-></span><span class="nf">innerJoin</span><span class="p">(</span><span class="s1">'product_lang'</span><span class="p">,</span> <span class="s1">'pl'</span><span class="p">,</span> <span class="s1">'p.id_product = pl.id_product AND pl.id_lang = '</span> <span class="mf">.</span> <span class="p">(</span><span class="n">int</span><span class="p">)</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">language</span><span class="o">-></span><span class="n">id</span><span class="p">);</span>
<span class="nv">$sql</span><span class="o">-></span><span class="nf">innerJoin</span><span class="p">(</span><span class="s1">'product_shop'</span><span class="p">,</span> <span class="s1">'ps'</span><span class="p">,</span> <span class="s1">'p.id_product = ps.id_product AND ps.id_shop = '</span> <span class="mf">.</span> <span class="p">(</span><span class="n">int</span><span class="p">)</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">shop</span><span class="o">-></span><span class="n">id</span><span class="p">);</span>
<span class="nv">$sql</span><span class="o">-></span><span class="nf">where</span><span class="p">(</span><span class="s1">'p.reference = \''</span> <span class="mf">.</span> <span class="nf">pSQL</span><span class="p">(</span><span class="nv">$reference</span><span class="p">)</span> <span class="mf">.</span> <span class="s1">'\''</span><span class="p">);</span>
<span class="nv">$sql</span><span class="o">-></span><span class="nf">where</span><span class="p">(</span><span class="s1">'ps.active = 1'</span><span class="p">);</span>
<span class="nv">$results</span> <span class="o">=</span> <span class="nc">Db</span><span class="o">::</span><span class="nf">getInstance</span><span class="p">(</span><span class="n">_PS_USE_SQL_SLAVE_</span><span class="p">)</span><span class="o">-></span><span class="nf">executeS</span><span class="p">(</span><span class="nv">$sql</span><span class="p">);</span>

Notice the details AI systematically forgets:

  • pSQL() to escape the value
  • The product_shop join for multi-shop context
  • The product_lang join for the current language
  • Using _PS_USE_SQL_SLAVE_ for read queries (performance)
  • Filtering on active = 1

3. Performance: the silent serial killer

The N+1 problem: the classic AI reproduces endlessly

A vibe-coded cross-selling module I recently audited:

<span class="c1">// AI-generated code — N+1 queries</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">hookDisplayShoppingCartFooter</span><span class="p">(</span><span class="nv">$params</span><span class="p">)</span>
<span class="p">{</span>
    <span class="nv">$cart</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">cart</span><span class="p">;</span>
    <span class="nv">$products</span> <span class="o">=</span> <span class="nv">$cart</span><span class="o">-></span><span class="nf">getProducts</span><span class="p">();</span>
    <span class="nv">$recommendations</span> <span class="o">=</span> <span class="p">[];</span>

    <span class="k">foreach</span> <span class="p">(</span><span class="nv">$products</span> <span class="k">as</span> <span class="nv">$product</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// Query 1 per product: fetch the category</span>
        <span class="nv">$category</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Category</span><span class="p">(</span><span class="nv">$product</span><span class="p">[</span><span class="s1">'id_category_default'</span><span class="p">],</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">language</span><span class="o">-></span><span class="n">id</span><span class="p">);</span>

        <span class="c1">// Query 2 per product: fetch products from the same category</span>
        <span class="nv">$categoryProducts</span> <span class="o">=</span> <span class="nv">$category</span><span class="o">-></span><span class="nf">getProducts</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">language</span><span class="o">-></span><span class="n">id</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>

        <span class="k">foreach</span> <span class="p">(</span><span class="nv">$categoryProducts</span> <span class="k">as</span> <span class="nv">$catProduct</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// Query 3 per recommended product: check stock</span>
            <span class="nv">$stockAvailable</span> <span class="o">=</span> <span class="nc">StockAvailable</span><span class="o">::</span><span class="nf">getQuantityAvailableByProduct</span><span class="p">(</span><span class="nv">$catProduct</span><span class="p">[</span><span class="s1">'id_product'</span><span class="p">]);</span>

            <span class="k">if</span> <span class="p">(</span><span class="nv">$stockAvailable</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
                <span class="c1">// Query 4 per recommended product: fetch the image</span>
                <span class="nv">$image</span> <span class="o">=</span> <span class="nc">Image</span><span class="o">::</span><span class="nf">getCover</span><span class="p">(</span><span class="nv">$catProduct</span><span class="p">[</span><span class="s1">'id_product'</span><span class="p">]);</span>
                <span class="nv">$catProduct</span><span class="p">[</span><span class="s1">'image_url'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">link</span><span class="o">-></span><span class="nf">getImageLink</span><span class="p">(</span>
                    <span class="nv">$catProduct</span><span class="p">[</span><span class="s1">'link_rewrite'</span><span class="p">],</span>
                    <span class="nv">$image</span><span class="p">[</span><span class="s1">'id_image'</span><span class="p">],</span>
                    <span class="s1">'home_default'</span>
                <span class="p">);</span>
                <span class="nv">$recommendations</span><span class="p">[]</span> <span class="o">=</span> <span class="nv">$catProduct</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">smarty</span><span class="o">-></span><span class="nf">assign</span><span class="p">(</span><span class="s1">'recommendations'</span><span class="p">,</span> <span class="nb">array_slice</span><span class="p">(</span><span class="nv">$recommendations</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">4</span><span class="p">));</span>
    <span class="k">return</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">display</span><span class="p">(</span><span class="k">__FILE__</span><span class="p">,</span> <span class="s1">'views/templates/hook/recommendations.tpl'</span><span class="p">);</span>
<span class="p">}</span>

A cart with 5 products, in categories of 50 products each = potentially 1,000+ SQL queries on every cart page display.

On a store with traffic, this module brings the server to its knees within 24 hours.

The optimized version

<span class="k">public</span> <span class="k">function</span> <span class="n">hookDisplayShoppingCartFooter</span><span class="p">(</span><span class="nv">$params</span><span class="p">)</span>
<span class="p">{</span>
    <span class="nv">$cart</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">cart</span><span class="p">;</span>
    <span class="nv">$products</span> <span class="o">=</span> <span class="nv">$cart</span><span class="o">-></span><span class="nf">getProducts</span><span class="p">();</span>

    <span class="k">if</span> <span class="p">(</span><span class="k">empty</span><span class="p">(</span><span class="nv">$products</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">return</span> <span class="s1">''</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// Cache: don't recalculate on every page load</span>
    <span class="nv">$cacheKey</span> <span class="o">=</span> <span class="s1">'mymodule_reco_'</span> <span class="mf">.</span> <span class="nv">$cart</span><span class="o">-></span><span class="n">id</span> <span class="mf">.</span> <span class="s1">'_'</span> <span class="mf">.</span> <span class="nb">md5</span><span class="p">(</span><span class="nb">serialize</span><span class="p">(</span><span class="nb">array_column</span><span class="p">(</span><span class="nv">$products</span><span class="p">,</span> <span class="s1">'id_product'</span><span class="p">)));</span>

    <span class="k">if</span> <span class="p">(</span><span class="nv">$cachedOutput</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">getCachedRecommendations</span><span class="p">(</span><span class="nv">$cacheKey</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nv">$cachedOutput</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nv">$productIds</span> <span class="o">=</span> <span class="nb">array_column</span><span class="p">(</span><span class="nv">$products</span><span class="p">,</span> <span class="s1">'id_product'</span><span class="p">);</span>
    <span class="nv">$categoryIds</span> <span class="o">=</span> <span class="nb">array_unique</span><span class="p">(</span><span class="nb">array_column</span><span class="p">(</span><span class="nv">$products</span><span class="p">,</span> <span class="s1">'id_category_default'</span><span class="p">));</span>
    <span class="nv">$idLang</span> <span class="o">=</span> <span class="p">(</span><span class="n">int</span><span class="p">)</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">language</span><span class="o">-></span><span class="n">id</span><span class="p">;</span>
    <span class="nv">$idShop</span> <span class="o">=</span> <span class="p">(</span><span class="n">int</span><span class="p">)</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">shop</span><span class="o">-></span><span class="n">id</span><span class="p">;</span>

    <span class="c1">// A SINGLE query to fetch everything</span>
    <span class="nv">$sql</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DbQuery</span><span class="p">();</span>
    <span class="nv">$sql</span><span class="o">-></span><span class="nf">select</span><span class="p">(</span><span class="s1">'DISTINCT p.id_product, pl.name, pl.link_rewrite, p.price,
                   p.id_category_default, sa.quantity as stock,
                   IFNULL(img.id_image, 0) as id_image'</span><span class="p">);</span>
    <span class="nv">$sql</span><span class="o">-></span><span class="nf">from</span><span class="p">(</span><span class="s1">'product'</span><span class="p">,</span> <span class="s1">'p'</span><span class="p">);</span>
    <span class="nv">$sql</span><span class="o">-></span><span class="nf">innerJoin</span><span class="p">(</span><span class="s1">'product_lang'</span><span class="p">,</span> <span class="s1">'pl'</span><span class="p">,</span>
        <span class="s1">'p.id_product = pl.id_product AND pl.id_lang = '</span> <span class="mf">.</span> <span class="nv">$idLang</span> <span class="mf">.</span> <span class="s1">' AND pl.id_shop = '</span> <span class="mf">.</span> <span class="nv">$idShop</span><span class="p">);</span>
    <span class="nv">$sql</span><span class="o">-></span><span class="nf">innerJoin</span><span class="p">(</span><span class="s1">'product_shop'</span><span class="p">,</span> <span class="s1">'ps'</span><span class="p">,</span>
        <span class="s1">'p.id_product = ps.id_product AND ps.id_shop = '</span> <span class="mf">.</span> <span class="nv">$idShop</span><span class="p">);</span>
    <span class="nv">$sql</span><span class="o">-></span><span class="nf">innerJoin</span><span class="p">(</span><span class="s1">'stock_available'</span><span class="p">,</span> <span class="s1">'sa'</span><span class="p">,</span>
        <span class="s1">'p.id_product = sa.id_product AND sa.id_product_attribute = 0 AND sa.id_shop = '</span> <span class="mf">.</span> <span class="nv">$idShop</span><span class="p">);</span>
    <span class="nv">$sql</span><span class="o">-></span><span class="nf">leftJoin</span><span class="p">(</span><span class="s1">'image_shop'</span><span class="p">,</span> <span class="s1">'img'</span><span class="p">,</span>
        <span class="s1">'p.id_product = img.id_product AND img.id_shop = '</span> <span class="mf">.</span> <span class="nv">$idShop</span> <span class="mf">.</span> <span class="s1">' AND img.cover = 1'</span><span class="p">);</span>
    <span class="nv">$sql</span><span class="o">-></span><span class="nf">where</span><span class="p">(</span><span class="s1">'p.id_category_default IN ('</span> <span class="mf">.</span> <span class="nb">implode</span><span class="p">(</span><span class="s1">','</span><span class="p">,</span> <span class="nb">array_map</span><span class="p">(</span><span class="s1">'intval'</span><span class="p">,</span> <span class="nv">$categoryIds</span><span class="p">))</span> <span class="mf">.</span> <span class="s1">')'</span><span class="p">);</span>
    <span class="nv">$sql</span><span class="o">-></span><span class="nf">where</span><span class="p">(</span><span class="s1">'p.id_product NOT IN ('</span> <span class="mf">.</span> <span class="nb">implode</span><span class="p">(</span><span class="s1">','</span><span class="p">,</span> <span class="nb">array_map</span><span class="p">(</span><span class="s1">'intval'</span><span class="p">,</span> <span class="nv">$productIds</span><span class="p">))</span> <span class="mf">.</span> <span class="s1">')'</span><span class="p">);</span>
    <span class="nv">$sql</span><span class="o">-></span><span class="nf">where</span><span class="p">(</span><span class="s1">'ps.active = 1'</span><span class="p">);</span>
    <span class="nv">$sql</span><span class="o">-></span><span class="nf">where</span><span class="p">(</span><span class="s1">'sa.quantity > 0'</span><span class="p">);</span>
    <span class="nv">$sql</span><span class="o">-></span><span class="nf">orderBy</span><span class="p">(</span><span class="s1">'RAND()'</span><span class="p">);</span>
    <span class="nv">$sql</span><span class="o">-></span><span class="nf">limit</span><span class="p">(</span><span class="mi">4</span><span class="p">);</span>

    <span class="nv">$recommendations</span> <span class="o">=</span> <span class="nc">Db</span><span class="o">::</span><span class="nf">getInstance</span><span class="p">(</span><span class="n">_PS_USE_SQL_SLAVE_</span><span class="p">)</span><span class="o">-></span><span class="nf">executeS</span><span class="p">(</span><span class="nv">$sql</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="k">empty</span><span class="p">(</span><span class="nv">$recommendations</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">return</span> <span class="s1">''</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// Build image URLs in batch (no extra queries)</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="nv">$recommendations</span> <span class="k">as</span> <span class="o">&</span><span class="nv">$reco</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nv">$reco</span><span class="p">[</span><span class="s1">'id_image'</span><span class="p">]</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
            <span class="nv">$reco</span><span class="p">[</span><span class="s1">'image_url'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">link</span><span class="o">-></span><span class="nf">getImageLink</span><span class="p">(</span>
                <span class="nv">$reco</span><span class="p">[</span><span class="s1">'link_rewrite'</span><span class="p">],</span>
                <span class="nv">$reco</span><span class="p">[</span><span class="s1">'id_product'</span><span class="p">]</span> <span class="mf">.</span> <span class="s1">'-'</span> <span class="mf">.</span> <span class="nv">$reco</span><span class="p">[</span><span class="s1">'id_image'</span><span class="p">],</span>
                <span class="nc">ImageType</span><span class="o">::</span><span class="nf">getFormattedName</span><span class="p">(</span><span class="s1">'home'</span><span class="p">)</span>
            <span class="p">);</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="nv">$reco</span><span class="p">[</span><span class="s1">'image_url'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">link</span><span class="o">-></span><span class="nf">getImageLink</span><span class="p">(</span>
                <span class="nv">$reco</span><span class="p">[</span><span class="s1">'link_rewrite'</span><span class="p">],</span>
                <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">language</span><span class="o">-></span><span class="n">iso_code</span> <span class="mf">.</span> <span class="s1">'-default'</span><span class="p">,</span>
                <span class="nc">ImageType</span><span class="o">::</span><span class="nf">getFormattedName</span><span class="p">(</span><span class="s1">'home'</span><span class="p">)</span>
            <span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">smarty</span><span class="o">-></span><span class="nf">assign</span><span class="p">(</span><span class="s1">'recommendations'</span><span class="p">,</span> <span class="nv">$recommendations</span><span class="p">);</span>
    <span class="nv">$output</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">display</span><span class="p">(</span><span class="k">__FILE__</span><span class="p">,</span> <span class="s1">'views/templates/hook/recommendations.tpl'</span><span class="p">);</span>

    <span class="c1">// Cache for 30 minutes</span>
    <span class="nv">$this</span><span class="o">-></span><span class="nf">cacheRecommendations</span><span class="p">(</span><span class="nv">$cacheKey</span><span class="p">,</span> <span class="nv">$output</span><span class="p">,</span> <span class="mi">1800</span><span class="p">);</span>

    <span class="k">return</span> <span class="nv">$output</span><span class="p">;</span>
<span class="p">}</span>

Result: 1 query instead of 1,000. Caching included. Ready for traffic.


4. Multi-shop: AI’s total blind spot

This is where the vast majority of vibe-coded modules collapse, because AI simply has no idea about the complexity of PrestaShop multi-shop.

What AI doesn’t know

PrestaShop multi-shop means 3 possible contexts:

Shop::CONTEXT_SHOP    → a single store
Shop::CONTEXT_GROUP   → a group of stores
Shop::CONTEXT_ALL     → all stores

And every database table potentially has an associated _shop table. When a vibe-coded module does:

<span class="c1">// Only works in single-shop mode</span>
<span class="nc">Configuration</span><span class="o">::</span><span class="nf">updateValue</span><span class="p">(</span><span class="s1">'MY_MODULE_SETTING'</span><span class="p">,</span> <span class="nv">$value</span><span class="p">);</span>

In a multi-shop context, this line can:

  • Overwrite the configuration of ALL stores (if ALL context)
  • Only save for the group (if GROUP context)
  • Work correctly (if SHOP context, and even then…)

The correct approach

<span class="c1">// Explicit multi-shop handling</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">saveConfiguration</span><span class="p">()</span>
<span class="p">{</span>
    <span class="nv">$shops</span> <span class="o">=</span> <span class="nc">Shop</span><span class="o">::</span><span class="nf">getShops</span><span class="p">(</span><span class="kc">true</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="nc">Shop</span><span class="o">::</span><span class="nf">getContext</span><span class="p">()</span> <span class="o">===</span> <span class="nc">Shop</span><span class="o">::</span><span class="no">CONTEXT_SHOP</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// Save for the current store only</span>
        <span class="nc">Configuration</span><span class="o">::</span><span class="nf">updateValue</span><span class="p">(</span>
            <span class="s1">'MY_MODULE_SETTING'</span><span class="p">,</span>
            <span class="nc">Tools</span><span class="o">::</span><span class="nf">getValue</span><span class="p">(</span><span class="s1">'MY_MODULE_SETTING'</span><span class="p">),</span>
            <span class="kc">false</span><span class="p">,</span>
            <span class="kc">null</span><span class="p">,</span>
            <span class="p">(</span><span class="n">int</span><span class="p">)</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">shop</span><span class="o">-></span><span class="n">id</span>
        <span class="p">);</span>
    <span class="p">}</span> <span class="k">elseif</span> <span class="p">(</span><span class="nc">Shop</span><span class="o">::</span><span class="nf">getContext</span><span class="p">()</span> <span class="o">===</span> <span class="nc">Shop</span><span class="o">::</span><span class="no">CONTEXT_ALL</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// User wants to apply to all stores</span>
        <span class="k">foreach</span> <span class="p">(</span><span class="nv">$shops</span> <span class="k">as</span> <span class="nv">$idShop</span><span class="p">)</span> <span class="p">{</span>
            <span class="nc">Configuration</span><span class="o">::</span><span class="nf">updateValue</span><span class="p">(</span>
                <span class="s1">'MY_MODULE_SETTING'</span><span class="p">,</span>
                <span class="nc">Tools</span><span class="o">::</span><span class="nf">getValue</span><span class="p">(</span><span class="s1">'MY_MODULE_SETTING'</span><span class="p">),</span>
                <span class="kc">false</span><span class="p">,</span>
                <span class="kc">null</span><span class="p">,</span>
                <span class="p">(</span><span class="n">int</span><span class="p">)</span> <span class="nv">$idShop</span>
            <span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

Multi-shop installation: the real nightmare

The install() of a vibe-coded module:

<span class="c1">// Naive installation</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">install</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="k">parent</span><span class="o">::</span><span class="nf">install</span><span class="p">()</span>
        <span class="o">&&</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">registerHook</span><span class="p">(</span><span class="s1">'displayProductAdditionalInfo'</span><span class="p">)</span>
        <span class="o">&&</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">installDb</span><span class="p">();</span>
<span class="p">}</span>

<span class="k">private</span> <span class="k">function</span> <span class="n">installDb</span><span class="p">()</span>
<span class="p">{</span>
    <span class="nv">$sql</span> <span class="o">=</span> <span class="s1">'CREATE TABLE IF NOT EXISTS `'</span> <span class="mf">.</span> <span class="n">_DB_PREFIX_</span> <span class="mf">.</span> <span class="s1">'mymodule_data` (
        `id` INT AUTO_INCREMENT PRIMARY KEY,
        `id_product` INT NOT NULL,
        `custom_field` VARCHAR(255) NOT NULL
    )'</span><span class="p">;</span>

    <span class="k">return</span> <span class="nc">Db</span><span class="o">::</span><span class="nf">getInstance</span><span class="p">()</span><span class="o">-></span><span class="nf">execute</span><span class="p">(</span><span class="nv">$sql</span><span class="p">);</span>
<span class="p">}</span>

The robust install():

<span class="c1">// Multi-shop aware installation</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">install</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nc">Shop</span><span class="o">::</span><span class="nf">isFeatureActive</span><span class="p">())</span> <span class="p">{</span>
        <span class="nc">Shop</span><span class="o">::</span><span class="nf">setContext</span><span class="p">(</span><span class="nc">Shop</span><span class="o">::</span><span class="no">CONTEXT_ALL</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">parent</span><span class="o">::</span><span class="nf">install</span><span class="p">())</span> <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-></span><span class="n">_errors</span><span class="p">[]</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">l</span><span class="p">(</span><span class="s1">'Could not install module base'</span><span class="p">);</span>
        <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nv">$hooks</span> <span class="o">=</span> <span class="p">[</span>
        <span class="s1">'displayProductAdditionalInfo'</span><span class="p">,</span>
        <span class="s1">'displayBackOfficeHeader'</span><span class="p">,</span>
        <span class="s1">'actionProductUpdate'</span><span class="p">,</span>
        <span class="s1">'actionProductDelete'</span><span class="p">,</span>
        <span class="s1">'actionShopDataDuplication'</span><span class="p">,</span> <span class="c1">// ← Crucial for multi-shop!</span>
        <span class="s1">'actionObjectShopDeleteAfter'</span><span class="p">,</span>
    <span class="p">];</span>

    <span class="k">foreach</span> <span class="p">(</span><span class="nv">$hooks</span> <span class="k">as</span> <span class="nv">$hook</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$this</span><span class="o">-></span><span class="nf">registerHook</span><span class="p">(</span><span class="nv">$hook</span><span class="p">))</span> <span class="p">{</span>
            <span class="nv">$this</span><span class="o">-></span><span class="n">_errors</span><span class="p">[]</span> <span class="o">=</span> <span class="nb">sprintf</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="nf">l</span><span class="p">(</span><span class="s1">'Could not register hook: %s'</span><span class="p">),</span> <span class="nv">$hook</span><span class="p">);</span>
            <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$this</span><span class="o">-></span><span class="nf">installDb</span><span class="p">())</span> <span class="p">{</span>
        <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// Default configuration for all stores</span>
    <span class="nv">$this</span><span class="o">-></span><span class="nf">initializeDefaultConfig</span><span class="p">();</span>

    <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">private</span> <span class="k">function</span> <span class="n">installDb</span><span class="p">()</span>
<span class="p">{</span>
    <span class="nv">$sql</span> <span class="o">=</span> <span class="p">[];</span>

    <span class="nv">$sql</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'CREATE TABLE IF NOT EXISTS `'</span> <span class="mf">.</span> <span class="n">_DB_PREFIX_</span> <span class="mf">.</span> <span class="s1">'mymodule_data` (
        `id_mymodule_data` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        `id_product` INT UNSIGNED NOT NULL,
        `custom_field` VARCHAR(255) NOT NULL,
        `date_add` DATETIME NOT NULL,
        `date_upd` DATETIME NOT NULL,
        INDEX (`id_product`)
    ) ENGINE='</span> <span class="mf">.</span> <span class="n">_MYSQL_ENGINE_</span> <span class="mf">.</span> <span class="s1">' DEFAULT CHARSET=utf8mb4'</span><span class="p">;</span>

    <span class="c1">// _shop table for multi-shop</span>
    <span class="nv">$sql</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'CREATE TABLE IF NOT EXISTS `'</span> <span class="mf">.</span> <span class="n">_DB_PREFIX_</span> <span class="mf">.</span> <span class="s1">'mymodule_data_shop` (
        `id_mymodule_data` INT UNSIGNED NOT NULL,
        `id_shop` INT UNSIGNED NOT NULL,
        `active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1,
        PRIMARY KEY (`id_mymodule_data`, `id_shop`),
        INDEX (`id_shop`)
    ) ENGINE='</span> <span class="mf">.</span> <span class="n">_MYSQL_ENGINE_</span> <span class="mf">.</span> <span class="s1">' DEFAULT CHARSET=utf8mb4'</span><span class="p">;</span>

    <span class="c1">// _lang table for multilingual support</span>
    <span class="nv">$sql</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'CREATE TABLE IF NOT EXISTS `'</span> <span class="mf">.</span> <span class="n">_DB_PREFIX_</span> <span class="mf">.</span> <span class="s1">'mymodule_data_lang` (
        `id_mymodule_data` INT UNSIGNED NOT NULL,
        `id_lang` INT UNSIGNED NOT NULL,
        `id_shop` INT UNSIGNED NOT NULL DEFAULT 1,
        `custom_label` VARCHAR(255) NOT NULL,
        PRIMARY KEY (`id_mymodule_data`, `id_lang`, `id_shop`),
        INDEX (`id_lang`),
        INDEX (`id_shop`)
    ) ENGINE='</span> <span class="mf">.</span> <span class="n">_MYSQL_ENGINE_</span> <span class="mf">.</span> <span class="s1">' DEFAULT CHARSET=utf8mb4'</span><span class="p">;</span>

    <span class="k">foreach</span> <span class="p">(</span><span class="nv">$sql</span> <span class="k">as</span> <span class="nv">$query</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nc">Db</span><span class="o">::</span><span class="nf">getInstance</span><span class="p">()</span><span class="o">-></span><span class="nf">execute</span><span class="p">(</span><span class="nv">$query</span><span class="p">))</span> <span class="p">{</span>
            <span class="nv">$this</span><span class="o">-></span><span class="n">_errors</span><span class="p">[]</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">l</span><span class="p">(</span><span class="s1">'Database installation error'</span><span class="p">);</span>
            <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// Crucial hook: when a store is duplicated, data must follow</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">hookActionShopDataDuplication</span><span class="p">(</span><span class="nv">$params</span><span class="p">)</span>
<span class="p">{</span>
    <span class="nv">$oldShopId</span> <span class="o">=</span> <span class="p">(</span><span class="n">int</span><span class="p">)</span> <span class="nv">$params</span><span class="p">[</span><span class="s1">'old_id_shop'</span><span class="p">];</span>
    <span class="nv">$newShopId</span> <span class="o">=</span> <span class="p">(</span><span class="n">int</span><span class="p">)</span> <span class="nv">$params</span><span class="p">[</span><span class="s1">'new_id_shop'</span><span class="p">];</span>

    <span class="nc">Db</span><span class="o">::</span><span class="nf">getInstance</span><span class="p">()</span><span class="o">-></span><span class="nf">execute</span><span class="p">(</span><span class="s1">'
        INSERT INTO `'</span> <span class="mf">.</span> <span class="n">_DB_PREFIX_</span> <span class="mf">.</span> <span class="s1">'mymodule_data_shop` (`id_mymodule_data`, `id_shop`, `active`)
        SELECT `id_mymodule_data`, '</span> <span class="mf">.</span> <span class="nv">$newShopId</span> <span class="mf">.</span> <span class="s1">', `active`
        FROM `'</span> <span class="mf">.</span> <span class="n">_DB_PREFIX_</span> <span class="mf">.</span> <span class="s1">'mymodule_data_shop`
        WHERE `id_shop` = '</span> <span class="mf">.</span> <span class="nv">$oldShopId</span>
    <span class="p">);</span>
<span class="p">}</span>

AI NEVER generates hookActionShopDataDuplication. Never. And without it, store duplication breaks the module’s data.


5. Tests and validation: what simply doesn’t exist

A vibe-coded module has no tests. Zero. AI generates functional code, not quality infrastructure.

In a professional module, here’s what exists beyond the code:

mymodule/
├── mymodule.php
├── config/
│   └── services.yml          ← Dependency injection
├── src/
│   ├── Controller/
│   ├── Repository/            ← Database abstraction layer
│   ├── Service/
│   └── Exception/             ← Typed business exceptions
├── tests/
│   ├── Unit/
│   │   ├── Service/
│   │   └── Repository/
│   └── Integration/
│       ├── HookTest.php
│       ├── MultiShopTest.php
│       └── InstallTest.php
├── upgrade/
│   ├── upgrade-1.1.0.php      ← Database migration
│   ├── upgrade-1.2.0.php
│   └── upgrade-2.0.0.php
├── views/
├── translations/
└── .github/
    └── workflows/
        └── ci.yml             ← Automated CI/CD

Upgrade files: the great forgotten

When your module evolves, the database needs to migrate. AI never thinks to create upgrade files:

<span class="c1">// upgrade/upgrade-1.2.0.php</span>
<span class="k">function</span> <span class="n">upgrade_module_1_2_0</span><span class="p">(</span><span class="nv">$module</span><span class="p">)</span>
<span class="p">{</span>
    <span class="nv">$sql</span> <span class="o">=</span> <span class="p">[];</span>

    <span class="c1">// Add a column without breaking existing data</span>
    <span class="nv">$sql</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'ALTER TABLE `'</span> <span class="mf">.</span> <span class="n">_DB_PREFIX_</span> <span class="mf">.</span> <span class="s1">'mymodule_data`
              ADD COLUMN `priority` INT UNSIGNED NOT NULL DEFAULT 0 AFTER `custom_field`'</span><span class="p">;</span>

    <span class="c1">// Migrate existing data</span>
    <span class="nv">$sql</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'UPDATE `'</span> <span class="mf">.</span> <span class="n">_DB_PREFIX_</span> <span class="mf">.</span> <span class="s1">'mymodule_data` SET `priority` = 1 WHERE `active` = 1'</span><span class="p">;</span>

    <span class="c1">// New hook needed</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$module</span><span class="o">-></span><span class="nf">registerHook</span><span class="p">(</span><span class="s1">'displayAfterBodyOpeningTag'</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">foreach</span> <span class="p">(</span><span class="nv">$sql</span> <span class="k">as</span> <span class="nv">$query</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nc">Db</span><span class="o">::</span><span class="nf">getInstance</span><span class="p">()</span><span class="o">-></span><span class="nf">execute</span><span class="p">(</span><span class="nv">$query</span><span class="p">))</span> <span class="p">{</span>
            <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="c1">// Clear cache</span>
    <span class="k">if</span> <span class="p">(</span><span class="nb">method_exists</span><span class="p">(</span><span class="s1">'Cache'</span><span class="p">,</span> <span class="s1">'clean'</span><span class="p">))</span> <span class="p">{</span>
        <span class="nc">Cache</span><span class="o">::</span><span class="nf">clean</span><span class="p">(</span><span class="s1">'mymodule_*'</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">}</span>

Without upgrade files, updating the module breaks all existing installations. That’s the kind of thing you learn after receiving 200 support tickets in a single day.


6. Compatibility: the real craft

Compatibility with other modules

A module never lives alone. On a typical PrestaShop store, there are 30 to 80 installed modules. A vibe-coded module:

  • Overwrites overrides without checking if they already exist → crashes other modules
  • Loads jQuery twice or loads an incompatible version → JavaScript broken everywhere
  • Modifies ObjectModels without using hooks → modules observing these objects are never notified
  • Adds CSS/JS everywhere instead of targeting relevant pages → global slowdown
<span class="c1">// What AI generates</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">hookDisplayHeader</span><span class="p">()</span>
<span class="p">{</span>
    <span class="c1">// Loaded on ALL front pages</span>
    <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">controller</span><span class="o">-></span><span class="nf">addCSS</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="n">_path</span> <span class="mf">.</span> <span class="s1">'views/css/style.css'</span><span class="p">);</span>
    <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">controller</span><span class="o">-></span><span class="nf">addJS</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="n">_path</span> <span class="mf">.</span> <span class="s1">'views/js/script.js'</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// What you should do</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">hookDisplayHeader</span><span class="p">()</span>
<span class="p">{</span>
    <span class="nv">$controller</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">controller</span><span class="p">;</span>

    <span class="c1">// Only load on relevant pages</span>
    <span class="k">if</span> <span class="p">(</span><span class="nv">$controller</span> <span class="k">instanceof</span> <span class="nc">ProductController</span><span class="p">)</span> <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">controller</span><span class="o">-></span><span class="nf">registerStylesheet</span><span class="p">(</span>
            <span class="s1">'mymodule-product'</span><span class="p">,</span>
            <span class="s1">'modules/'</span> <span class="mf">.</span> <span class="nv">$this</span><span class="o">-></span><span class="n">name</span> <span class="mf">.</span> <span class="s1">'/views/css/product.css'</span><span class="p">,</span>
            <span class="p">[</span><span class="s1">'media'</span> <span class="o">=></span> <span class="s1">'all'</span><span class="p">,</span> <span class="s1">'priority'</span> <span class="o">=></span> <span class="mi">150</span><span class="p">]</span>
        <span class="p">);</span>
        <span class="nv">$this</span><span class="o">-></span><span class="n">context</span><span class="o">-></span><span class="n">controller</span><span class="o">-></span><span class="nf">registerJavascript</span><span class="p">(</span>
            <span class="s1">'mymodule-product'</span><span class="p">,</span>
            <span class="s1">'modules/'</span> <span class="mf">.</span> <span class="nv">$this</span><span class="o">-></span><span class="n">name</span> <span class="mf">.</span> <span class="s1">'/views/js/product.js'</span><span class="p">,</span>
            <span class="p">[</span><span class="s1">'position'</span> <span class="o">=></span> <span class="s1">'bottom'</span><span class="p">,</span> <span class="s1">'priority'</span> <span class="o">=></span> <span class="mi">150</span><span class="p">,</span> <span class="s1">'attribute'</span> <span class="o">=></span> <span class="s1">'defer'</span><span class="p">]</span>
        <span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

PrestaShop version compatibility

A professional module must handle this:

<span class="c1">// Adapt code based on version</span>
<span class="k">if</span> <span class="p">(</span><span class="nb">version_compare</span><span class="p">(</span><span class="n">_PS_VERSION_</span><span class="p">,</span> <span class="s1">'8.0.0'</span><span class="p">,</span> <span class="s1">'>='</span><span class="p">))</span> <span class="p">{</span>
    <span class="c1">// PrestaShop 8: uses Symfony and Doctrine</span>
    <span class="c1">// Legacy AdminControllers are deprecated</span>
    <span class="c1">// Theme system has evolved</span>
<span class="p">}</span> <span class="k">elseif</span> <span class="p">(</span><span class="nb">version_compare</span><span class="p">(</span><span class="n">_PS_VERSION_</span><span class="p">,</span> <span class="s1">'1.7.7'</span><span class="p">,</span> <span class="s1">'>='</span><span class="p">))</span> <span class="p">{</span>
    <span class="c1">// PrestaShop 1.7.7+: new hooks, new ProductPresenter</span>
    <span class="c1">// Symfony partially integrated</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="c1">// PrestaShop 1.7.x legacy</span>
    <span class="c1">// Still pre-Symfony code everywhere</span>
<span class="p">}</span>


7. The bottom line: what Vibe Coding does well, and where it stops

Let’s be honest. Here’s my assessment after 6 months of daily AI use in my PrestaShop development:

What AI does very well

Task Time saved Initial module scaffolding ~60% Basic Smarty template generation ~50% Writing simple SQL queries ~40% HelperForm form generation ~70% Code documentation ~80% Refactoring existing code ~40% ### What AI does poorly (or not at all)

Problem Production consequence Security (tokens, permissions, SQL injection) Store hacked, data stolen Multi-shop Corrupted data across stores Performance (N+1, cache) Server down during peak traffic Complete action hooks Inconsistent data, ERP/CRM desynchronized Upgrade files Module impossible to update Cross-version compatibility Module crashes on other versions Robust error handling White screens, 500 errors GDPR compliance Legal risk Accessibility (a11y) Legal non-compliance ---

8. My method: AI as an accelerator, not a replacement

I’m not against Vibe Coding. I’m against Vibe Coding without a safety net.

Here’s how I integrate AI into my daily workflow:

1. AI generates, I specify

I never ask “create me a wishlist module”. I ask:

“Generate the WishlistRepository class with add, remove, getByCustomer methods. Use DbQuery, handle multi-shop with shop joins, escape values with pSQL and (int). The getByCustomer method must use _PS_USE_SQL_SLAVE.”

2. AI codes, I review

Every generated line goes through my mental checklist:

  • Security: token, permissions, escaping
  • Multi-shop: context, _shop joins
  • Multi-language: _lang joins
  • Performance: number of queries, caching
  • Hooks: complete (action + display)
  • Compatibility: PS version, themes
  • Errors: try/catch, Validate, fallbacks

3. AI iterates, I validate

AI is excellent at iterating quickly on variations. But the final validation is human, with tests on a real store, with real data, in a real multi-shop context.

🔑#### Key Takeaways — Vibe Coding & PrestaShop Modules

What every developer (and every merchant ordering a module) must understand about Vibe Coding applied to PrestaShop:

  1. AI generates functional code — not production-ready code. Vibe-coded modules compile and work in demos. They break on edge cases, real load, and inter-module interactions. That’s precisely what makes them dangerous.
  2. Three unavoidable failure vectors: security (AJAX without CSRF token, SQL injections), multi-shop (missing _shop and _lang joins, hookActionShopDataDuplication never generated), and performance (N+1 queries that bring a server down within 24h of traffic).
  3. Critical action hooks are systematically forgotten. AI generates display hooks well, never the hookActionObject*Delete/Update/Add hooks required for data consistency with ERPs and CRMs.
  4. PrestaShop 9 introduces breaking changes AI doesn’t know about. Legacy product page removed, 100% Symfony admin auth, hook alias deprecation — a vibe-coded module without an audit can be incompatible on 40% of its features.
  5. The right approach: AI to accelerate, expert to validate. AI saves 40–80% on scaffolding. But every generated line must pass through a mental checklist: security, multi-shop, multi-language, performance, complete hooks, version compatibility.

Conclusion: Vibe Coding doesn’t replace 10 years of production failures

Vibe Coding is a wonderful tool in the hands of a developer who knows what they’re doing. It accelerates work by 30 to 50%.

But in the hands of someone who doesn’t know PrestaShop’s pitfalls — and there are countless — it’s a technical debt generator, a source of security vulnerabilities, and a recipe for unstable stores.

The 80% of vibe-coded modules that will never make it to production aren’t modules that are poorly coded in a syntactic sense. AI writes code that compiles, that executes, that appears to work. That’s precisely what makes them dangerous.

They fail on edge cases, specific contexts, inter-module interactions, load scaling, updates, and business constraints that only field experience can teach.

The expertise of a senior PrestaShop developer isn’t knowing how to write PHP. It’s knowing everything that can go wrong in production, and preventing it before it happens.

Vibe Coding generates code. Experience generates trust.

And in e-commerce, when every minute of downtime costs thousands of euros, trust is what has value.


You have an AI-generated module and you’re wondering if it’s ready for production? I offer a comprehensive technical audit with a detailed report, priority fixes, and technical debt assessment. Because the best time to find problems is before your customers do.

What about you — what’s your experience with Vibe Coding on PrestaShop? Any AI modules that held up in production? Disasters narrowly avoided?

LinkedIn

Follow my AI and e-commerce analysis

I share practical notes on AI agents, PrestaShop architecture, MCP and automation for e-commerce teams.

Follow on LinkedIn