DEV Community

Nicolas Dabene
Nicolas Dabene

Posted on • Originally published at nicolas-dabene.fr

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

Beyond the Buzz: Why 80% of AI-Generated E-commerce Modules Crumble in Production

Estimated reading time: 15 minutes
Last updated: February 2026

The AI Promise vs. Real-World E-commerce Challenges

"Just describe your desired feature, and AI will code it for you."

Ever since Gene Kim introduced the concept of "Vibe Coding" and development assistants like Cursor, Claude Code, and GitHub Copilot gained widespread adoption, a compelling vision has emerged: anyone can now build software. The core idea is that deep technical understanding is no longer required; simply "convey the intent," and the AI handles the rest.

And honestly? For quick prototypes, demonstrations, or personal side projects, this approach can be genuinely impressive and effective.

However, after a decade of developing PrestaShop modules—solutions powering stores with 50,000 monthly orders, installed on complex multi-shop setups across 12 stores, 4 languages, 3 currencies, intricate business rules, and integrated ERP systems—I've observed a stark reality. Over the past six months, my audits, code reviews, and support requests have revealed a consistent pattern among "vibe-coded" modules:

They function flawlessly in development environments. They consistently fail in live production.

This isn't an anti-AI critique; I leverage AI daily in my own development process. Instead, this article aims to demonstrate, with practical examples and actual code snippets, why applying Vibe Coding to e-commerce—specifically to PrestaShop—presents a complex landscape that only profound domain expertise can successfully navigate.


1. PrestaShop Hooks: AI's Fundamental Misunderstanding

The AI-Generated Approach

When an LLM is asked to create a module that adds a reassurance message to a product page, it typically produces something like this:

public function hookDisplayProductAdditionalInfo($params)
{
    $product = $params['product'];

    $this->context->smarty->assign([
        'product_name' => $product->name,
        'reassurance_text' => 'Free shipping over €49',
    ]);

    return $this->display(__FILE__, 'views/templates/hook/reassurance.tpl');
}
Enter fullscreen mode Exit fullscreen mode

This code appears straightforward and works perfectly on a clean PrestaShop installation, keeping the client happy for a couple of days.

The Production Catastrophe

Issue 1: The $params['product'] variable is ambiguous.

Depending on the specific PrestaShop version (e.g., 1.7.6, 1.7.8, 8.1), whether the hook fires on a standard product page or within a quick-view modal, and the active theme, $params['product'] can manifest as:

  • A Product object.
  • An associative array (often generated by ProductPresenter).
  • null (if the hook is invoked in an unforeseen context).
  • An array with varying keys across different platform versions.

Robust, production-ready code must account for these variations:

public function hookDisplayProductAdditionalInfo($params)
{
    // Defensive handling of the product parameter
    $product = null;

    if (isset($params['product'])) {
        if (is_array($params['product'])) {
            // PrestaShop 1.7.7+ with ProductPresenter
            $productId = (int) ($params['product']['id_product'] ?? $params['product']['id'] ?? 0);
            if ($productId > 0) {
                $product = new Product($productId, false, $this->context->language->id, $this->context->shop->id);
            }
        } elseif ($params['product'] instanceof Product) {
            $product = $params['product'];
        }
    }

    // Fallback to context if the hook doesn't provide anything usable
    if (!Validate::isLoadedObject($product)) {
        $productId = (int) Tools::getValue('id_product');
        if ($productId > 0) {
            $product = new Product($productId, false, $this->context->language->id, $this->context->shop->id);
        }
    }

    if (!Validate::isLoadedObject($product) || !$product->active) {
        return '';
    }

    // ... rest of the processing
}
Enter fullscreen mode Exit fullscreen mode

Is it elegant? No. Is it essential for stability? Absolutely.

Issue 2: The hook itself might not be present.

AI frequently suggests hookDisplayProductAdditionalInfo because it appears in official documentation. Yet, on a live store utilizing a custom theme (like Flavor or Warehouse), this specific hook might be:

  • Absent from the theme's templates.
  • Rendered at an unexpected position within the Document Object Model (DOM).
  • Contending for placement with several other modules registered to the same hook.

An experienced developer would proactively inspect the target theme, suggest a widget as a flexible alternative, or implement fallbacks using hooks such as hookDisplayFooterProduct or hookDisplayOverrideTemplate.

Issue 3: Overlooked action hooks.

While AI excels at generating "display" hooks, it consistently neglects crucial "action" hooks. A recent audit of a vibe-coded stock management module revealed:

  • It correctly managed hookActionProductUpdate for stock recalculations.
  • It entirely missed hookActionObjectProductDeleteAfter, leading to phantom products in the database.
  • It ignored hookActionProductAttributeUpdate, resulting in desynchronized product combinations.
  • It forgot hookActionObjectCombinationDeleteAfter, causing ERP system crashes.
  • It failed to handle hookActionObjectStockAvailableUpdateAfter, creating conflicts with PrestaShop's native stock management.

A single forgotten action hook can lead to data inconsistencies across hundreds of products.


2. Security: The Critical Gaps Left by AI

Vulnerable AJAX Endpoints

I've consistently observed this insecure pattern in roughly 90% of AI-generated modules that include an administrative interface:

// front/ajax.php — AI-generated
include('../../config/config.inc.php');

$action = Tools::getValue('action');

if ($action === 'updatePrice') {
    $id_product = Tools::getValue('id_product');
    $new_price = Tools::getValue('price');

    $product = new Product($id_product);
    $product->price = $new_price;
    $product->save();

    die(json_encode(['success' => true]));
}
Enter fullscreen mode Exit fullscreen mode

This snippet represents a critical security flaw. Anyone can manipulate product prices with a straightforward curl command:

curl "https://your-store.com/modules/mymodule/front/ajax.php?action=updatePrice&id_product=1&price=0.01"
Enter fullscreen mode Exit fullscreen mode

The immediate outcome: all products in your store could be discounted to a mere 1 cent.

Requirements for Secure Code

// controllers/front/ajax.php — secure version
class MyModuleAjaxModuleFrontController extends ModuleFrontController
{
    public function initContent()
    {
        // Verify this is actually an AJAX request
        if (!$this->ajax) {
            $this->ajaxRender(json_encode(['error' => 'Invalid request']));
            return;
        }

        parent::initContent();
    }

    public function displayAjaxUpdatePrice()
    {
        // 1. CSRF token verification
        if (!$this->isTokenValid()) {
            header('HTTP/1.1 403 Forbidden');
            $this->ajaxRender(json_encode(['error' => 'Invalid token']));
            return;
        }

        // 2. Permission check (admin employee logged in)
        $cookie = new Cookie('psAdmin');
        if (!$cookie->id_employee) {
            header('HTTP/1.1 401 Unauthorized');
            $this->ajaxRender(json_encode(['error' => 'Unauthorized']));
            return;
        }

        $employee = new Employee((int) $cookie->id_employee);
        if (!Validate::isLoadedObject($employee)
            || !$employee->hasAuthOnShop($this->context->shop->id)) {
            header('HTTP/1.1 403 Forbidden');
            $this->ajaxRender(json_encode(['error' => 'Insufficient permissions']));
            return;
        }

        // 3. Strict input validation
        $idProduct = (int) Tools::getValue('id_product');
        $newPrice = (float) Tools::getValue('price');

        if ($idProduct <= 0) {
            $this->ajaxRender(json_encode(['error' => 'Invalid product ID']));
            return;
        }

        if ($newPrice < 0 || $newPrice > 999999.99) {
            $this->ajaxRender(json_encode(['error' => 'Invalid price range']));
            return;
        }

        // 4. Verify the product belongs to the current shop context
        $product = new Product($idProduct, false, null, $this->context->shop->id);
        if (!Validate::isLoadedObject($product)) {
            $this->ajaxRender(json_encode(['error' => 'Product not found in current shop']));
            return;
        }

        // 5. Secure update with multi-shop handling
        $product->price = $newPrice;

        if (Shop::getContext() === Shop::CONTEXT_SHOP) {
            $product->save();
        } else {
            // In multi-shop context, force current shop
            $product->id_shop_default = $this->context->shop->id;
            $product->save();
        }

        // 6. Log the action for audit trail
        PrestaShopLogger::addLog(
            sprintf('Product #%d price updated to %f by employee #%d via module MyModule',
                $idProduct, $newPrice, (int) $cookie->id_employee),
            1,
            null,
            'Product',
            $idProduct,
            true,
            (int) $cookie->id_employee
        );

        // 7. Flush product price cache
        Product::flushPriceCache();

        $this->ajaxRender(json_encode([
            'success' => true,
            'product_id' => $idProduct,
            'new_price' => $newPrice,
        ]));
    }
}
Enter fullscreen mode Exit fullscreen mode

This transformation takes the code from 8 lines to 65 lines. Each additional line strategically neutralizes a potential real-world attack vector.

SQL Injections: A Persistent Threat in 2026

AI frequently favors "easy-to-read" SQL queries:

// AI-generated — SQL injection vulnerability
$sql = "SELECT * FROM " . _DB_PREFIX_ . "product
        WHERE reference = '" . $reference . "'";
$results = Db::getInstance()->executeS($sql);
Enter fullscreen mode Exit fullscreen mode
// Secure version
$sql = new DbQuery();
$sql->select('p.id_product, p.reference, p.price, pl.name');
$sql->from('product', 'p');
$sql->innerJoin('product_lang', 'pl', 'p.id_product = pl.id_product AND pl.id_lang = ' . (int) $this->context->language->id);
$sql->innerJoin('product_shop', 'ps', 'p.id_product = ps.id_product AND ps.id_shop = ' . (int) $this->context->shop->id);
$sql->where('p.reference = \'' . pSQL($reference) . '\'');
$sql->where('ps.active = 1');
$results = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
Enter fullscreen mode Exit fullscreen mode

Observe the crucial details that AI consistently omits:

  • The pSQL() function for proper value escaping.
  • The product_shop join, indispensable for multi-shop environments.
  • The product_lang join, essential for current language support.
  • The use of _PS_USE_SQL_SLAVE_ for read-only queries, optimizing performance.
  • Explicit filtering for active = 1.

3. Performance: The Stealthy System Killer

The Ubiquitous N+1 Query Problem

Here’s an example from a vibe-coded cross-selling module I recently audited:

// AI-generated code — N+1 queries
public function hookDisplayShoppingCartFooter($params)
{
    $cart = $this->context->cart;
    $products = $cart->getProducts();
    $recommendations = [];

    foreach ($products as $product) {
        // Query 1 per product: fetch the category
        $category = new Category($product['id_category_default'], $this->context->language->id);

        // Query 2 per product: fetch products from the same category
        $categoryProducts = $category->getProducts($this->context->language->id, 1, 10);

        foreach ($categoryProducts as $catProduct) {
            // Query 3 per recommended product: check stock
            $stockAvailable = StockAvailable::getQuantityAvailableByProduct($catProduct['id_product']);

            if ($stockAvailable > 0) {
                // Query 4 per recommended product: fetch the image
                $image = Image::getCover($catProduct['id_product']);
                $catProduct['image_url'] = $this->context->link->getImageLink(
                    $catProduct['link_rewrite'],
                    $image['id_image'],
                    'home_default'
                );
                $recommendations[] = $catProduct;
            }
        }
    }

    $this->context->smarty->assign('recommendations', array_slice($recommendations, 0, 4));
    return $this->display(__FILE__, 'views/templates/hook/recommendations.tpl');
}
Enter fullscreen mode Exit fullscreen mode

Consider a shopping cart with 5 products, each belonging to categories containing 50 items. This code could easily trigger over 1,000 SQL queries on every cart page load.

On an e-commerce platform experiencing significant traffic, such a module would inevitably cause the server to crash within 24 hours.

The Optimized Solution

public function hookDisplayShoppingCartFooter($params)
{
    $cart = $this->context->cart;
    $products = $cart->getProducts();

    if (empty($products)) {
        return '';
    }

    // Cache: don't recalculate on every page load
    $cacheKey = 'mymodule_reco_' . $cart->id . '_' . md5(serialize(array_column($products, 'id_product')));

    if ($cachedOutput = $this->getCachedRecommendations($cacheKey)) {
        return $cachedOutput;
    }

    $productIds = array_column($products, 'id_product');
    $categoryIds = array_unique(array_column($products, 'id_category_default'));
    $idLang = (int) $this->context->language->id;
    $idShop = (int) $this->context->shop->id;

    // A SINGLE query to fetch everything
    $sql = new DbQuery();
    $sql->select('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');
    $sql->from('product', 'p');
    $sql->innerJoin('product_lang', 'pl',
        'p.id_product = pl.id_product AND pl.id_lang = ' . $idLang . ' AND pl.id_shop = ' . $idShop);
    $sql->innerJoin('product_shop', 'ps',
        'p.id_product = ps.id_product AND ps.id_shop = ' . $idShop);
    $sql->innerJoin('stock_available', 'sa',
        'p.id_product = sa.id_product AND sa.id_product_attribute = 0 AND sa.id_shop = ' . $idShop);
    $sql->leftJoin('image_shop', 'img',
        'p.id_product = img.id_product AND img.id_shop = ' . $idShop . ' AND img.cover = 1');
    $sql->where('p.id_category_default IN (' . implode(',', array_map('intval', $categoryIds)) . ')');
    $sql->where('p.id_product NOT IN (' . implode(',', array_map('intval', $productIds)) . ')');
    $sql->where('ps.active = 1');
    $sql->where('sa.quantity > 0');
    $sql->orderBy('RAND()');
    $sql->limit(4);

    $recommendations = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);

    if (empty($recommendations)) {
        return '';
    }

    // Build image URLs in batch (no extra queries)
    foreach ($recommendations as &$reco) {
        if ($reco['id_image'] > 0) {
            $reco['image_url'] = $this->context->link->getImageLink(
                $reco['link_rewrite'],
                $reco['id_product'] . '-' . $reco['id_image'],
                ImageType::getFormattedName('home')
            );
        } else {
            $reco['image_url'] = $this->context->link->getImageLink(
                $reco['link_rewrite'],
                $this->context->language->iso_code . '-default',
                ImageType::getFormattedName('home')
            );
        }
    }

    $this->context->smarty->assign('recommendations', $recommendations);
    $output = $this->display(__FILE__, 'views/templates/hook/recommendations.tpl');

    // Cache for 30 minutes
    $this->cacheRecommendations($cacheKey, $output, 1800);

    return $output;
}
Enter fullscreen mode Exit fullscreen mode

The outcome: a single database query instead of a thousand, complete with integrated caching. This module is now robust enough to handle high traffic.


4. Multi-shop Support: AI's Glaring Oversight

This particular area is where the vast majority of AI-generated modules critically fail. The AI simply possesses no inherent understanding of the intricate complexities involved in PrestaShop's multi-shop architecture.

AI's Knowledge Gap

PrestaShop's multi-shop functionality operates across three distinct contexts:

Shop::CONTEXT_SHOP    → Applies to a single individual store.
Shop::CONTEXT_GROUP   → Applies to an entire group of stores.
Shop::CONTEXT_ALL     → Applies across all stores within the installation.
Enter fullscreen mode Exit fullscreen mode

Furthermore, nearly every database table can have a corresponding _shop table to manage shop-specific data. When an AI-generated module executes:

// Only works in single-shop mode
Configuration::updateValue('MY_MODULE_SETTING', $value);
Enter fullscreen mode Exit fullscreen mode

In a multi-shop environment, this single line of code can:

  • Unintentionally overwrite configuration settings for ALL stores (if the context is CONTEXT_ALL).
  • Only save the configuration for the current group of stores (if the context is CONTEXT_GROUP).
  • Work correctly only if the context is CONTEXT_SHOP and even then, often with implicit assumptions that can lead to issues.

The Correct Implementation

// Explicit multi-shop handling
public function saveConfiguration()
{
    $shops = Shop::getShops(true, null, true);

    if (Shop::getContext() === Shop::CONTEXT_SHOP) {
        // Save for the current store only
        Configuration::updateValue(
            'MY_MODULE_SETTING',
            Tools::getValue('MY_MODULE_SETTING'),
            false,
            null,
            (int) $this->context->shop->id
        );
    } elseif (Shop::getContext() === Shop::CONTEXT_ALL) {
        // User wants to apply to all stores
        foreach ($shops as $idShop) {
            Configuration::updateValue(
                'MY_MODULE_SETTING',
                Tools::getValue('MY_MODULE_SETTING'),
                false,
                null,
                (int) $idShop
            );
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Multi-shop Installation: A Developer's Ordeal

A typical install() method for an AI-generated module often looks like this:

// Naive installation
public function install()
{
    return parent::install()
        && $this->registerHook('displayProductAdditionalInfo')
        && $this->installDb();
}

private function installDb()
{
    $sql = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'mymodule_data` (
        `id` INT AUTO_INCREMENT PRIMARY KEY,
        `id_product` INT NOT NULL,
        `custom_field` VARCHAR(255) NOT NULL
    )';

    return Db::getInstance()->execute($sql);
}
Enter fullscreen mode Exit fullscreen mode

Now, contrast that with a robust, multi-shop aware install() implementation:

// Multi-shop aware installation
public function install()
{
    if (Shop::isFeatureActive()) {
        Shop::setContext(Shop::CONTEXT_ALL);
    }

    if (!parent::install()) {
        $this->_errors[] = $this->l('Could not install module base');
        return false;
    }

    $hooks = [
        'displayProductAdditionalInfo',
        'displayBackOfficeHeader',
        'actionProductUpdate',
        'actionProductDelete',
        'actionShopDataDuplication', // ← Crucial for multi-shop!
        'actionObjectShopDeleteAfter',
    ];

    foreach ($hooks as $hook) {
        if (!$this->registerHook($hook)) {
            $this->_errors[] = sprintf($this->l('Could not register hook: %s'), $hook);
            return false;
        }
    }

    if (!$this->installDb()) {
        return false;
    }

    // Default configuration for all stores
    $this->initializeDefaultConfig();

    return true;
}

private function installDb()
{
    $sql = [];

    $sql[] = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . '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=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8mb4';

    // _shop table for multi-shop
    $sql[] = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . '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=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8mb4';

    // _lang table for multilingual support
    $sql[] = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . '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=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8mb4';

    foreach ($sql as $query) {
        if (!Db::getInstance()->execute($query)) {
            $this->_errors[] = $this->l('Database installation error');
            return false;
        }
    }

    return true;
}

// Crucial hook: when a store is duplicated, data must follow
public function hookActionShopDataDuplication($params)
{
    $oldShopId = (int) $params['old_id_shop'];
    $newShopId = (int) $params['new_id_shop'];

    Db::getInstance()->execute('
        INSERT INTO `' . _DB_PREFIX_ . 'mymodule_data_shop` (`id_mymodule_data`, `id_shop`, `active`)
        SELECT `id_mymodule_data`, ' . $newShopId . ', `active`
        FROM `' . _DB_PREFIX_ . 'mymodule_data_shop`
        WHERE `id_shop` = ' . $oldShopId
    );
}
Enter fullscreen mode Exit fullscreen mode

AI never generates hookActionShopDataDuplication. Without this critical hook, duplicating a store will inevitably corrupt or break the module's data within the new shop.


5. Testing and Validation: The Non-Existent Layers in AI Code

An AI-generated module typically lacks any form of testing infrastructure. While AI can produce functional code, it rarely builds the essential quality framework around it.

In contrast, a professionally developed module structure typically includes:

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
Enter fullscreen mode Exit fullscreen mode

Upgrade Scripts: The Overlooked Necessity

As modules evolve, their underlying database schemas must also adapt. AI consistently fails to generate necessary upgrade scripts for database migration:

// upgrade/upgrade-1.2.0.php
function upgrade_module_1_2_0($module)
{
    $sql = [];

    // Add a column without breaking existing data
    $sql[] = 'ALTER TABLE `' . _DB_PREFIX_ . 'mymodule_data`
              ADD COLUMN `priority` INT UNSIGNED NOT NULL DEFAULT 0 AFTER `custom_field`';

    // Migrate existing data
    $sql[] = 'UPDATE `' . _DB_PREFIX_ . 'mymodule_data` SET `priority` = 1 WHERE `active` = 1';

    // New hook needed
    if (!$module->registerHook('displayAfterBodyOpeningTag')) {
        return false;
    }

    foreach ($sql as $query) {
        if (!Db::getInstance()->execute($query)) {
            return false;
        }
    }

    // Clear cache
    if (method_exists('Cache', 'clean')) {
        Cache::clean('mymodule_*');
    }

    return true;
}
Enter fullscreen mode Exit fullscreen mode

Without these upgrade files, attempting to update a module will inevitably lead to broken existing installations. This is a lesson typically learned the hard way, often after a flood of support tickets.


6. Compatibility: The Mark of True Craftsmanship

Interoperability with Other Modules

A module never exists in isolation. A typical PrestaShop store often runs between 30 and 80 installed modules. An AI-generated module, lacking an understanding of this ecosystem, might:

  • Unintentionally overwrite existing overrides without proper checks, leading to conflicts and crashes with other modules.
  • Load jQuery multiple times or introduce incompatible versions, resulting in widespread JavaScript errors.
  • Modify ObjectModel instances without utilizing appropriate hooks, preventing other modules from reacting to these changes.
  • Inject CSS/JS assets indiscriminately across all pages instead of targeting relevant ones, causing a global performance slowdown.
// What AI generates
public function hookDisplayHeader()
{
    // Loaded on ALL front pages
    $this->context->controller->addCSS($this->_path . 'views/css/style.css');
    $this->context->controller->addJS($this->_path . 'views/js/script.js');
}
Enter fullscreen mode Exit fullscreen mode
// What you should do
public function hookDisplayHeader()
{
    $controller = $this->context->controller;

    // Only load on relevant pages
    if ($controller instanceof ProductController) {
        $this->context->controller->registerStylesheet(
            'mymodule-product',
            'modules/' . $this->name . '/views/css/product.css',
            ['media' => 'all', 'priority' => 150]
        );
        $this->context->controller->registerJavascript(
            'mymodule-product',
            'modules/' . $this->name . '/views/js/product.js',
            ['position' => 'bottom', 'priority' => 150, 'attribute' => 'defer']
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

PrestaShop Version Compatibility

A professionally built module must be capable of adapting to different PrestaShop versions:

// Adapt code based on version
if (version_compare(_PS_VERSION_, '8.0.0', '>=')) {
    // PrestaShop 8: uses Symfony and Doctrine
    // Legacy AdminControllers are deprecated
    // Theme system has evolved
} elseif (version_compare(_PS_VERSION_, '1.7.7', '>=')) {
    // PrestaShop 1.7.7+: new hooks, new ProductPresenter
    // Symfony partially integrated
} else {
    // PrestaShop 1.7.x legacy
    // Still pre-Symfony code everywhere
}
Enter fullscreen mode Exit fullscreen mode

7. The Core Insight: Where Vibe Coding Shines and Where It Fails

To be clear, here's my perspective after six months of incorporating AI into my daily PrestaShop development workflow:

AI's Strengths in Development

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%

AI's Critical Weaknesses in E-commerce Production

Problem Production Consequence
Security (tokens, permissions, SQL injection) Store hacked, data stolen
Multi-shop complexities Corrupted data across storefronts
Performance (N+1 queries, lack of caching) Server outages during peak traffic
Incomplete action hook implementation Inconsistent data, ERP/CRM desynchronization
Absence of upgrade scripts Module update failures
Cross-version compatibility Module crashes on different PS versions
Insufficient error handling White screens, 500 server errors
GDPR compliance oversights Significant legal risks
Accessibility (a11y) adherence Legal non-compliance

8. My Workflow: AI as an Enabler, Not a Replacement

I'm not opposed to Vibe Coding. My concern lies with Vibe Coding when used without an adequate safety net.

Here’s how I integrate AI into my daily development routine:

1. Precision in Prompting

I never simply ask, "create a wishlist module." Instead, I provide highly specific instructions:

"Generate the WishlistRepository class with add, remove, and getByCustomer methods. Ensure it utilizes DbQuery, manages multi-shop environments with _shop joins, sanitizes values with pSQL and (int) casting, and has the getByCustomer method leverage _PS_USE_SQL_SLAVE_."

2. Rigorous Human Review

Every line of AI-generated code undergoes a thorough review against my mental checklist:

  • Security: Token validation, permission checks, proper escaping.
  • Multi-shop: Correct context handling, appropriate _shop table joins.
  • Multi-language: Inclusion of _lang table joins.
  • Performance: Query optimization, caching strategies.
  • Hooks: Comprehensive implementation of both action and display hooks.
  • Compatibility: Adherence to PrestaShop versions and theme structures.
  • Error Handling: Strategic use of try/catch, Validate functions, and sensible fallbacks.

3. Iteration and Real-World Validation

AI excels at rapid iteration and generating variations. However, the ultimate validation remains human-driven, involving exhaustive testing on a live store, with genuine data, and within a true multi-shop environment.


Conclusion: Expertise Remains Irreplaceable

Vibe Coding stands as an exceptional tool when wielded by a developer who possesses a deep understanding of their craft. It can boost productivity by a significant 30 to 50 percent.

Yet, in the hands of someone unaware of PrestaShop’s numerous pitfalls – and they are indeed countless – it transforms into a prolific generator of technical debt, a breeding ground for critical security flaws, and a direct path to unstable e-commerce operations.

The 80% of AI-generated modules that never see the light of production aren't syntactically flawed. AI competently writes code that compiles, executes, and appears to function correctly. This inherent deceptiveness is precisely what makes them so hazardous.

Their failures emerge in specific edge cases, unique contexts, intricate inter-module dependencies, under load scaling, during system updates, and against nuanced business requirements—all insights gained exclusively through extensive field experience.

The true value of a senior PrestaShop developer's expertise isn't merely writing PHP; it's anticipating every potential failure in production and proactively preventing it.

Vibe Coding produces code.
Experience cultivates trust.

And in the high-stakes world of e-commerce, where every minute of downtime can translate into thousands of euros in lost revenue, trust is the paramount currency.


Are you uncertain whether your AI-generated module is truly ready for a live environment? I provide comprehensive technical audits, delivering detailed reports, identifying priority fixes, and assessing technical debt. The ideal moment to uncover problems is always before your customers do.

Want to dive deeper into practical PrestaShop development and best practices?
Connect with Nicolas Dabène for expert insights and discussions:

  • Catch his tutorials and tips on YouTube.
  • Follow his professional journey and engage in industry conversations on LinkedIn.

What are your experiences with Vibe Coding in PrestaShop? Have any AI-powered modules genuinely thrived in production, or have you narrowly averted disaster? Share your stories below!

Top comments (0)