<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Alexandre Navaeian — E-commerce Dev Notes]]></title><description><![CDATA[Alexandre Navaeian — E-commerce Dev Notes]]></description><link>https://alexnava.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a396b217f090e8b8f43fe16/ca5c1256-5342-445c-ae02-460fec3efbd7.png</url><title>Alexandre Navaeian — E-commerce Dev Notes</title><link>https://alexnava.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 23 Oct 2026 17:09:02 GMT</lastBuildDate><atom:link href="https://alexnava.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[PrestaShop 9: 3 legacy AJAX controller traps (and how to fix them)]]></title><description><![CDATA[Porting a PrestaShop module to PS9? Fix the AJAX 404s, blank pages and fatal errors in legacy ModuleAdminController: getAdminLink, l() and ajaxRender.]]></description><link>https://alexnava.hashnode.dev/prestashop-9-3-legacy-ajax-controller-traps-and-how-to-fix-them</link><guid isPermaLink="true">https://alexnava.hashnode.dev/prestashop-9-3-legacy-ajax-controller-traps-and-how-to-fix-them</guid><category><![CDATA[PrestaShop]]></category><category><![CDATA[PHP]]></category><category><![CDATA[ecommerce]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[Alexandre Navaeian (Réussir avec motivation)]]></dc:creator><pubDate>Mon, 22 Jun 2026 17:46:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a396b217f090e8b8f43fe16/0a8ee98f-e77d-41a0-ae76-b5b2254565e8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When you migrate a module from PrestaShop 1.7/8 to PrestaShop 9, the back office may load correctly, your forms may still work… and then the first AJAX call from a <code>ModuleAdminController</code> returns a <strong>404</strong>, a broken response, or sometimes a <strong>blank page</strong> with no readable error.</p>
<p>I deliberately keep the legacy approach — <code>ModuleAdminController</code> + <code>HelperForm</code> + <code>getContent()</code> — when I need to stay <strong>cross-compatible between PrestaShop 8 and PrestaShop 9</strong> without rewriting the whole module as Symfony controllers.</p>
<p>PrestaShop 9 still supports many legacy patterns, but it is stricter about a few contracts: URL generation, translations, and inherited method signatures.</p>
<p>Here are three common traps, with the fix for each.</p>
<h2>Trap 1 — <code>getAdminLink()</code>: put legacy AJAX params in the 4th argument</h2>
<p>In PrestaShop 1.7/8, many modules used to build AJAX URLs by hand and the dispatcher was often forgiving.</p>
<p>In PrestaShop 9, URL generation is less forgiving, especially around legacy/Symfony route conversion. Manually appending <code>&amp;ajax=1&amp;action=...</code> can easily produce broken or non-routed URLs.</p>
<pre><code class="language-php">// ❌ Fragile on PS9
\(url = \)this-&gt;context-&gt;link-&gt;getAdminLink('AdminMyModuleAjax')
     . '&amp;ajax=1&amp;action=MyRequest';
</code></pre>
<p>The safer cross-compatible way is to pass <code>ajax</code>, <code>action</code>, and your own AJAX parameters through the <strong>4th argument</strong> of <code>getAdminLink()</code>:</p>
<pre><code class="language-php">// ✅ ajax + action in the 4th argument ($params)
\(url = \)this-&gt;context-&gt;link-&gt;getAdminLink(
    'AdminMyModuleAjax',
    true,   // with token
    [],     // Symfony route params, 3rd argument
    ['ajax' =&gt; 1, 'action' =&gt; 'MyRequest'] // legacy query params, 4th argument
);
</code></pre>
<p>The signature to keep in mind:</p>
<pre><code class="language-php">getAdminLink(\(controller, \)withToken = true, \(sfRouteParams = [], \)params = [])
</code></pre>
<p>The <strong>3rd argument</strong> is for Symfony route parameters. The <strong>4th argument</strong> is for legacy query parameters.</p>
<p>So anything AJAX-related — <code>ajax</code>, <code>action</code>, <code>id_item</code>, or your own parameters — belongs in the 4th argument. The security token does <strong>not</strong> go there: it is generated automatically by the 2nd argument (<code>true</code> / <code>$withToken</code>).</p>
<h2>Trap 2 — Don't rely on <code>$this-&gt;l()</code> inside module controllers</h2>
<p>Inside a <code>ModuleAdminController</code>, avoid relying on:</p>
<pre><code class="language-php">// ❌ Not reliable in PS9 module controllers
\(msg = \)this-&gt;l('Saved successfully');
</code></pre>
<p>Use the module instance instead.</p>
<p>For legacy cross-compatibility, this remains a practical option:</p>
<pre><code class="language-php">// ✅ Legacy-compatible translation through the module instance
\(msg = \)this-&gt;module-&gt;l('Saved successfully', 'mymodule');
</code></pre>
<p>If you are already working with PrestaShop's newer translation system, the cleaner PS9-friendly approach is:</p>
<pre><code class="language-php">// ✅ Newer translation system
\(msg = \)this-&gt;module-&gt;trans(
    'Saved successfully',
    [],
    'Modules.Mymodule.Admin'
);
</code></pre>
<p>In short:</p>
<ul>
<li><p>for legacy/cross-compatible controllers: use <code>$this-&gt;module-&gt;l(...)</code>;</p>
</li>
<li><p>for modern PS9-oriented code: prefer <code>$this-&gt;module-&gt;trans(...)</code>.</p>
</li>
</ul>
<p>The important point is: in a module controller, translate through the module instance, not through <code>$this-&gt;l()</code>.</p>
<h2>Trap 3 — <code>ajaxRender()</code>: don't override it with an incompatible signature</h2>
<p>This is a classic blank-page culprit.</p>
<p>Some older modules used to redefine <code>ajaxRender()</code> with a single argument, or use it as a custom JSON helper.</p>
<p>But in PrestaShop 9, <code>ControllerCore::ajaxRender()</code> has its own signature:</p>
<pre><code class="language-php">protected function ajaxRender(\(value = null, \)controller = null, $method = null)
</code></pre>
<p>If your controller declares an incompatible method like this:</p>
<pre><code class="language-php">// ❌ Incompatible override
public function ajaxRender($data)
{
    echo json_encode($data);
    exit;
}
</code></pre>
<p>PHP 8 + PrestaShop 9 can throw a fatal compatibility error such as:</p>
<pre><code class="language-txt">Declaration must be compatible with ControllerCore::ajaxRender(...)
</code></pre>
<p>In production, this can easily look like a blank page or a silent AJAX failure.</p>
<p>The simplest fix: <strong>do not override</strong> <code>ajaxRender()</code>. Create your own dedicated JSON response method instead:</p>
<pre><code class="language-php">// ✅ Dedicated JSON helper, no signature clash
protected function renderAjaxJson(array $data): void
{
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}
</code></pre>
<h2>Putting it together</h2>
<p>Controller side:</p>
<pre><code class="language-php">class AdminMyModuleAjaxController extends ModuleAdminController
{
    public function ajaxProcessMyRequest(): void
    {
        $id = (int) Tools::getValue('id_item');

        // ... business logic ...

        $this-&gt;renderAjaxJson([
            'success' =&gt; true,
            'message' =&gt; $this-&gt;module-&gt;l('Update done', 'mymodule'),
        ]);
    }

    protected function renderAjaxJson(array $data): void
    {
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }
}
</code></pre>
<p>And the AJAX URL:</p>
<pre><code class="language-php">\(ajaxUrl = \)this-&gt;context-&gt;link-&gt;getAdminLink(
    'AdminMyModuleAjax',
    true,
    [],
    [
        'ajax' =&gt; 1,
        'action' =&gt; 'MyRequest',
    ]
);
</code></pre>
<p>If you need to pass an additional parameter:</p>
<pre><code class="language-php">\(ajaxUrl = \)this-&gt;context-&gt;link-&gt;getAdminLink(
    'AdminMyModuleAjax',
    true,
    [],
    [
        'ajax' =&gt; 1,
        'action' =&gt; 'MyRequest',
        'id_item' =&gt; (int) $idItem,
    ]
);
</code></pre>
<h2>Takeaway</h2>
<p>PrestaShop 9 did not completely remove the legacy <code>ModuleAdminController</code> approach, which is still useful when you need one module to remain compatible across multiple PrestaShop versions.</p>
<p>But PS9 is stricter about the contracts around legacy controllers:</p>
<ul>
<li><p>generate admin AJAX URLs properly;</p>
</li>
<li><p>pass legacy AJAX parameters in the 4th argument of <code>getAdminLink()</code>;</p>
</li>
<li><p>translate through the module instance;</p>
</li>
<li><p>do not override inherited methods with incompatible signatures.</p>
</li>
</ul>
<p>For new back-office pages, Symfony controllers remain the recommended long-term direction. But if you maintain existing legacy modules, these small fixes can keep your <code>ModuleAdminController</code>-based code stable across PrestaShop 8 and PrestaShop 9.</p>
<p>That is the approach I keep across <a href="https://www.datafirefly.com">DataFirefly</a> modules when compatibility matters: modernize where it makes sense, but avoid unnecessary rewrites when a clean legacy controller still does the job.</p>
<blockquote>
<p>💡 Hit a fourth AJAX trap on PS9? Drop it in the comments — I'll add it to the list.</p>
</blockquote>
]]></content:encoded></item></channel></rss>