Chat on WhatsApp
Magento Development 11 min read

How to Build a Magento 2 Extension in the AI Era (Properly)

AI scaffolds Magento modules in seconds — but generates code that looks right and breaks the framework's conventions. Here's how to build a custom extension properly in the AI era: the canonical anatomy, a working example, the AI gotchas to reject, and the gates that keep it maintainable.

How to Build a Magento 2 Extension in the AI Era (Properly)

In 2026, almost nobody types a Magento module from a blank file anymore. You describe the feature, an AI coding tool (Claude Code, Cursor, Copilot) scaffolds it, and you review. That’s a genuine productivity jump — Magento’s ceremony-heavy structure is exactly what AI is good at. But it’s also where AI quietly produces code that looks right, compiles, and violates the framework’s conventions in ways that bite you six months later. This guide shows how to build a real extension properly in the AI era: the canonical anatomy, a working example, and the review discipline that separates a maintainable module from generated slop.

What “properly” means in Magento

Magento gives you formal extension points so you never have to modify the core. Building properly means every change goes through one of these:

  • Plugins (interceptors) — wrap a public method’s behaviour (before / after / around).
  • Observers — react to dispatched events without coupling to the emitter.
  • Preferences in di.xml — swap one class implementation for another (use sparingly).
  • ViewModels — inject logic into templates the modern way, replacing the old Helper-in-template anti-pattern.
  • Declarative schema (db_schema.xml) — describe DB changes as desired state, not imperative install scripts.

If an AI ever proposes editing a file under vendor/, that’s an automatic no. The whole point of the module system is that your code lives in app/code/Panth/… and reaches into the framework through these seams.

The anatomy of a minimal module

Every module needs three files before Magento will see it. Let’s build Panth_DevTools, a small developer utility module.

1. registration.php

<?php
declare(strict_types=1);

use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(
    ComponentRegistrar::MODULE,
    'Panth_DevTools',
    __DIR__
);

2. etc/module.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Panth_DevTools"/>
</config>

Note what is not here: no setup_version. Since Magento 2.3 the version lives in composer.json and schema changes are declarative, so a generator that still emits setup_version="1.0.0" is working from outdated training data — a small but telling sign to review the rest closely.

3. composer.json

{
  "name": "panth/module-dev-tools",
  "description": "Developer utility commands for Magento 2.",
  "type": "magento2-module",
  "license": "MIT",
  "require": {
    "php": "~8.4.0||~8.5.0",
    "magento/framework": "*"
  },
  "autoload": {
    "files": ["registration.php"],
    "psr-4": { "Panth\\DevTools\\": "" }
  }
}

Pinning PHP to ~8.4||~8.5 keeps you honest with the platform — that’s the supported range on current Magento. (See our breakdown of the 2.4.9 / Mage-OS 2.3.0 stack changes.)

A worked example: a CLI command done right

A console command is the cleanest demonstration of dependency injection — no templates, no layout, just constructor wiring and a service contract. Ours counts catalog products.

The command class

<?php
declare(strict_types=1);

namespace Panth\DevTools\Console\Command;

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class ProductCount extends Command
{
    public function __construct(
        private readonly ProductRepositoryInterface $productRepository,
        private readonly SearchCriteriaBuilder $searchCriteriaBuilder,
        ?string $name = null
    ) {
        parent::__construct($name);
    }

    protected function configure(): void
    {
        $this->setName('panth:catalog:product:count')
             ->setDescription('Counts catalog products.');
        parent::configure();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $criteria = $this->searchCriteriaBuilder->create();
        $total = $this->productRepository->getList($criteria)->getTotalCount();

        $output->writeln(sprintf('<info>%d products in catalog.</info>', $total));

        return Command::SUCCESS;
    }
}

Two things make this proper: it depends on the service contract (ProductRepositoryInterface), not a concrete model, and the dependencies arrive through the constructor — using PHP 8 promoted, readonly properties. No ObjectManager::getInstance() anywhere.

Registering it in etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\Console\CommandList">
        <arguments>
            <argument name="commands" xsi:type="array">
                <item name="panth_product_count" xsi:type="object">Panth\DevTools\Console\Command\ProductCount</item>
            </argument>
        </arguments>
    </type>
</config>

Enable and run:

bin/magento module:enable Panth_DevTools
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento panth:catalog:product:count
# => 2048 products in catalog.

Where AI gets Magento wrong

These are the recurring mistakes in generated Magento code. Scan every diff for them:

  • ObjectManager as a service locator. $om = \Magento\Framework\App\ObjectManager::getInstance(); $repo = $om->get(...) compiles and runs, and it is wrong everywhere except a handful of bootstrap contexts. Demand constructor injection.
  • Plugin where an observer (or nothing) belongs. A plugin on a hot method like getName() runs on every product on every page. Ask the model to justify plugin vs observer vs preference vs ViewModel before accepting one.
  • Preferences used as a hammer. Overriding a whole class with a <preference> when a single after plugin would do creates upgrade pain. Preferences are a last resort.
  • Imperative InstallSchema/UpgradeSchema scripts instead of declarative db_schema.xml. The old API still loads, so generated code often uses it — but it’s deprecated.
  • Business logic in .phtml templates instead of a ViewModel. If the template has an if doing real work, push it into a ViewModel implementing ArgumentInterface.
  • Missing declare(strict_types=1) and return types — required by the Magento2 coding standard and easy for a generator to drop.

A workflow for building extensions with AI — properly

The model is a fast junior developer. Treat the session like a code review pipeline:

  1. Front-load the context. Tell it: target Magento 2.4.9 / PHP 8.4, the Panth\* namespace, “extend only via plugins/observers/ViewModels/di.xml, never edit vendor.” A prompt with these rules produces dramatically better first drafts — we keep a prompts library for Magento exactly for this.
  2. Constrain to extension points. Don’t ask “make products do X”; ask “add a plugin on this service contract that does X.” Naming the seam keeps the model from inventing core hacks.
  3. Make it explain its choice. “Why a plugin and not an observer here?” A model that can’t justify the extension point chose it by coin flip.
  4. Review the diff like a senior. Run the gotcha list above against every file before it lands.
  5. Run the gates. Generated code isn’t done until it survives:
bin/magento setup:di:compile          # catches DI / type errors
vendor/bin/phpcs --standard=Magento2 app/code/Panth/DevTools
vendor/bin/phpstan analyse app/code/Panth/DevTools --level=6
bin/magento dev:tests:run unit         # if you wrote tests

This is the difference that matters: AI changes who writes the first draft, not who is accountable for the code. The conventions, the extension points, and the gates are still yours to enforce.

Bottom line

Building a Magento 2 extension in the AI era isn’t about prompting harder — it’s about pairing AI’s speed with Magento’s rules. Scaffold fast, but insist on service contracts, constructor injection, declarative schema, and the right extension point every time. Get that discipline right and AI makes you genuinely faster; skip it and you ship technical debt at machine speed.

Want this done for you? We build production-grade, fully-compatible modules as a Magento extension development service, and we work AI-first — see how we use Claude Code for Magento development.