A complete beginner's roadmap to becoming a confident Magento 2 developer, no prior Magento experience needed.
Introduction: Why Magento 2 Is Worth Your Time
Imagine being handed the keys to one of the most powerful e-commerce engines in the world. That's exactly what Magento 2 is, a battle-tested, enterprise-grade platform trusted by thousands of online stores, from small boutiques to Fortune 500 companies like Coca-Cola, Ford, and Nestle.
But here's the honest truth: Magento 2 has a steep learning curve. It's not a weekend project you can master in a few hours.
It's a sophisticated framework built on top of PHP, and it comes with its own architecture, conventions, and quirks. That can feel overwhelming at first.
This guide exists to change that.
Whether you're a PHP developer making your first Magento leap, a web developer exploring backend e-commerce, or a self-taught coder ready for a serious challenge, this tutorial will walk you through Magento 2 from the ground up.
Step by step. In plain English.
Let's get started!
Table of Contents
- What Is Magento 2?
- Magento 2 vs Magento 1, What Changed?
- System Requirements & Installation
- Understanding the Magento 2 Architecture
- The Magento 2 File Structure Explained
- Creating Your First Magento 2 Module
- Magento 2 Routing & Controllers
- Layouts, Blocks & Templates
- Models, Resource Models & Collections
- Dependency Injection in Magento 2
- Plugins (Interceptors): The Magento Magic
- Events & Observers
- Admin Grids and Forms
- REST & GraphQL APIs
- CLI Commands
- Best Practices & Next Steps
What Is Magento 2?
Magento 2 is an open-source e-commerce platform written in PHP. It was released by Magento Inc. (now owned by Adobe) and is the successor to the original Magento 1 platform.
At its core, Magento 2 helps businesses build and manage online stores. It handles everything from product catalogs and shopping carts to payments, shipping, customer accounts, and reporting.
Two Editions
There are two main versions:
- 1. Magento Open Source (Community Edition): Free to download and use. Perfect for smaller businesses and developers learning the platform.
- 2. Adobe Commerce (Enterprise Edition): A paid, feature-rich version with advanced capabilities like B2B functionality, advanced search (via Elasticsearch), and Adobe's cloud infrastructure.
For this tutorial, we'll focus on Magento Open Source, which is all you need to learn the platform deeply.
What Do Developers Love and Find Challenging About Magento 2?
Developers love Magento 2 for its:
Massive ecosystem of extensions and themes
- Powerful flexibility, almost everything is customizable
- Strong separation of concerns and modular architecture
- Active community and documentation
And they sometimes struggle with:
The steep learning curve
- Heavy system requirements
- Slow performance in development without proper setup
- The initial "where do I even start?" feeling
This guide solves that last problem.
Magento 2 vs Magento 1, What Changed?
If you've worked with Magento 1 before, Magento 2 will feel both familiar and completely different.
Here's a quick comparison:
| Feature | Magento 1 | Magento 2 |
|---|---|---|
| PHP Version | PHP 5.x | PHP 7.4+ / 8.x |
| Frontend | Prototype.js | RequireJS, Knockout.js |
| Dependency Injection | Mage::getModel() | Constructor injection |
| ORM (Object-Relational Mapping) | Zend_Db | Magento ORM + Zend_Db2 |
| Admin Panel | Outdated UI | Modern, responsive |
| Testing | Minimal | PHPUnit integrated |
| CLI (Command-Line Interface) | None | Magento CLI (bin/magento) |
| Code Generation | None | Generated factories, proxies |
The biggest architectural difference is that Magento 2 uses Dependency Injection (DI) throughout the entire codebase.
If you've never worked with DI (Dependency Injection) before, don't worry, we'll cover it thoroughly in this guide.
Magento 2 System Requirements & Installation
Before diving into code, let's get Magento 2 running on your machine.
System Requirements
Make sure your development environment meets these minimums:
OS: Linux/Unix/macOS (Windows via WSL2)
Web Server: Apache 2.4+ or Nginx 1.x
PHP: 8.1 or 8.2 (recommended for Magento 2.4.x)
Database: MySQL 8.0+ or MariaDB 10.4+
Composer: 2.x
Elasticsearch/OpenSearch: 7.x or 8.x (required for Magento 2.4+)
RAM: At least 2GB (4GB+ recommended)
Storage: 10GB free space minimum
Recommended Magento 2 Development Setup
For local development, the easiest approach is using Docker with a ready-made Magento environment.
Two popular options are:
- 1. Warden: A powerful Docker-based environment specifically designed for Magento development. Highly recommended for serious Magento work.
- 2. Docker Compose: You can set up your own Docker Compose file with PHP, MySQL, Nginx, and Elasticsearch containers.
Installing Magento 2 via Composer
Once your environment is ready, install Magento 2 using Composer:
Create the Magento project
composer create-project --repository-url=https://repo.magento.com/ \
magento/project-community-edition=2.4.7 magento2
Navigate to the project directory
cd magento2
Set correct file permissions
find var generated vendor pub/static pub/media app/etc -type f -exec chmod g+w {} +
find var generated vendor pub/static pub/media app/etc -type d -exec chmod g+ws {} +
chown -R :www-data .
chmod u+x bin/magento
Note: You'll need Magento Marketplace credentials (free account) for repo.magento.com. Create one at marketplace.magento.com.
Further Reading: How to Set Magento 2 File and Folder Permissions?
Running the Setup Wizard
You can install Magento 2 either through the browser-based wizard or the CLI.
The CLI method is faster and more developer-friendly:
bin/magento setup:install \
--base-url=http://magento2.local/ \
--db-host=localhost \
--db-name=magento2 \
--db-user=root \
--db-password=yourpassword \
--admin-firstname=Admin \
--admin-lastname=User \
--admin-email=admin@example.com \
--admin-user=admin \
--admin-password=Admin123! \
--language=en_US \
--currency=USD \
--timezone=America/Chicago \
--use-rewrites=1 \
--search-engine=elasticsearch7 \
--elasticsearch-host=localhost \
--elasticsearch-port=9200
After installation completes, disable two-factor auth for local dev (optional but convenient):
bin/magento module:disable Magento_TwoFactorAuth
bin/magento cache:flush
Read More: How to Install Magento 2
Understanding the Magento 2 Architecture
Here's where things get interesting.
To write good Magento 2 code, you need to understand how it thinks.
The Module-Based Architecture
Everything in Magento 2 is a module.
Your custom features, core Magento functionality, third-party extensions, they're all modules.
A module is essentially a directory containing PHP classes, configuration files, templates, and assets that add specific functionality to the platform.
This modularity means:
- Features are isolated and reusable
- You can enable or disable modules without breaking others
- You extend functionality by creating new modules, not editing core files (very important!)
The Request Lifecycle
When a browser makes a request to a Magento store, here's what happens at a high level:
Browser Request
↓
index.php (entry point)
↓
Bootstrap (sets up the application)
↓
Front Controller (routes the request)
↓
Router (finds the matching controller)
↓
Controller (processes the request, prepares data)
↓
View (Layout XML → Blocks → Templates)
↓
Response (HTML sent back to browser)
Understanding this flow is key to debugging and building features correctly.
The MVC Pattern (Sort of)
Magento 2 loosely follows MVC (Model-View-Controller):
- 1. Model: Represents data and business logic. In Magento 2, models work with Resource Models (database abstraction) and Collections (groups of models).
- 2. View: Composed of three layers: Layout XML (structure), Blocks (PHP data helpers), and Templates (.phtml files with HTML + PHP).
- 3. Controller: Handles the request, calls the model, sets up the view.
Service Contracts
One thing that sets Magento 2 apart is its concept of Service Contracts, interfaces that define the public API of a module.
Instead of calling model classes directly, you call interfaces. This ensures your code remains stable even if the underlying implementation changes.
The Magento 2 File Structure Explained
Opening a Magento 2 project for the first time can feel like opening a mystery box.
Let's make sense of it.
magento2/ ├── app/ │ ├── code/ ← Your custom modules live here │ ├── design/ ← Custom themes live here │ └── etc/ ← Application-wide config (env.php, config.php) ├── bin/ │ └── magento ← The CLI tool (your best friend) ├── dev/ ← Testing tools, gruntfile, etc. ├── generated/ ← Auto-generated code (factories, proxies, interceptors) ├── lib/ ← Core libraries (non-Composer) ├── pub/ │ ├── static/ ← Compiled static assets (CSS, JS) │ └── media/ ← Product images, uploaded files ├── setup/ ← Installation/upgrade scripts ├── var/ │ ├── cache/ ← Application cache │ ├── log/ ← Log files (very useful for debugging) │ └── page_cache/ ← Full page cache └── vendor/ ← Composer dependencies (including Magento core) |
The Most Important Folders for Developers
app/code/ — This is where you'll spend 90% of your time. All custom modules you build go here, organized as Vendor/ModuleName.
app/design/ — Custom themes for frontend and admin customization.
var/log/ — Contains system.log, exception.log, and debug.log. When something goes wrong, look here first.
generated/ — Magento auto-generates code here (factories, interceptors). Never edit files in this directory manually. If you see strange errors, clearing this folder often helps: rm -rf generated/*
vendor/magento/ — The Magento core code. You can read it to understand how things work, but never edit it. Any changes will be lost on the next composer update.
Creating Your First Magento 2 Module
Time to write some code!
We'll create a simple "Hello World" module to understand the module structure.
Step 1. Create the Module Directory
Magento 2 modules follow the naming convention Vendor_ModuleName.
Let's create one:
mkdir -p app/code/MyCompany/HelloWorld
Step 2. Create the Module Declaration File
Every module needs a module.xml file to declare its existence:
mkdir -p app/code/MyCompany/HelloWorld/etc
app/code/MyCompany/HelloWorld/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="MyCompany_HelloWorld" setup_version="1.0.0">
</module>
</config>
|
Step 3. Create the Composer File
app/code/MyCompany/HelloWorld/composer.json:
{
"name": "mycompany/module-hello-world",
"description": "A simple Hello World module",
"type": "magento2-module",
"version": "1.0.0",
"require": {
"php": "~8.1.0||~8.2.0",
"magento/framework": "*"
},
"autoload": {
"files": ["registration.php"],
"psr-4": {
"MyCompany\\HelloWorld\\": ""
}
}
}
|
Step 4. Create the Registration File
app/code/MyCompany/HelloWorld/registration.php:
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'MyCompany_HelloWorld',
__DIR__
);
|
Step 5. Enable the Module
bin/magento module:enable MyCompany_HelloWorld
bin/magento setup:upgrade
bin/magento cache:flush
Verify it's enabled:
bin/magento module:status MyCompany_HelloWorld
You should see: Module is enabled
Congratulations! You've created your first Magento 2 module. It doesn't do much yet, let's fix that.
Magento 2 Routing & Controllers
Let's make the module actually respond to a URL.
How Routing Works
Magento 2 routes follow this URL pattern:
http://yourstore.com/frontname/controller_path/action_name
For example: http://yourstore.com/helloworld/index/index
helloworld — The frontname (defined in routes.xml)
index — The controller folder name
index — The action class name
Step 1. Create routes.xml
app/code/MyCompany/HelloWorld/etc/frontend/routes.xml:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="standard">
<route id="helloworld" frontName="helloworld">
<module name="MyCompany_HelloWorld"/>
</route>
</router>
</config>
|
Step 2: Create the Controller
app/code/MyCompany/HelloWorld/Controller/Index/Index.php:
<?php
namespace MyCompany\HelloWorld\Controller\Index;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\View\Result\PageFactory;
class Index implements HttpGetActionInterface
{
/**
* @var PageFactory
*/
private PageFactory $resultPageFactory;
/**
* Constructor
*
* @param PageFactory $resultPageFactory
*/
public function __construct(PageFactory $resultPageFactory)
{
$this->resultPageFactory = $resultPageFactory;
}
/**
* Execute action based on request and return result
*
* @return \Magento\Framework\View\Result\Page
*/
public function execute()
{
return $this->resultPageFactory->create();
}
}
|
Notice: The controller implements HttpGetActionInterface instead of extending Action. This is the modern, recommended approach in Magento 2.4+.
Now when you visit http://yourstore.com/helloworld/index/index, Magento will execute this controller. It'll return an empty page for now, we need to add a layout and template nex
Layouts, Blocks & Templates
Magento 2's view layer is built from three components that work together: Layout XML, Blocks, and Templates.
Let's understand each one.
Layout XML: The Blueprint
Layout XML files define the structure of a page: which blocks appear, where they go, and how they're configured.
app/code/MyCompany/HelloWorld/view/frontend/layout/helloworld_index_index.xml:
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"
layout="1column">
<body>
<referenceContainer name="content">
<block class="MyCompany\HelloWorld\Block\Hello"
name="hello_world_block"
template="MyCompany_HelloWorld::hello.phtml"/>
</referenceContainer>
</body>
</page>
|
The filename helloworld_index_index.xml maps to the route helloworld/index/index. This naming convention is how Magento knows which layout file to load for which page.
Block: The Data Helper
Blocks act as data providers for templates. They're PHP classes that fetch data from models and expose it to the template.
app/code/MyCompany/HelloWorld/Block/Hello.php:
<?php
namespace MyCompany\HelloWorld\Block;
use Magento\Framework\View\Element\Template;
use Magento\Framework\View\Element\Template\Context;
class Hello extends Template
{
/**
* Constructor
*
* @param Context $context
* @param array $data
*/
public function __construct(
Context $context,
array $data = []
) {
parent::__construct($context, $data);
}
/**
* Get greeting message
*
* @return string
*/
public function getGreeting(): string
{
return 'Hello, World! Welcome to Magento 2 Development!';
}
}
|
Template: The View File
Templates are .phtml files, a mix of HTML and PHP. They call block methods to get data.
app/code/MyCompany/HelloWorld/view/frontend/templates/hello.phtml:
<?php
/** @var \MyCompany\HelloWorld\Block\Hello $block */
?>
<div class="hello-world-container">
<h1><?= $block->escapeHtml($block->getGreeting()) ?></h1>
<p>You've just created your first Magento 2 page!</p>
</div>
|
Security note: Always use $block->escapeHtml() when outputting user-facing data. Magento provides multiple escape methods: escapeHtml(), escapeUrl(), escapeJs(), escapeCss(). Using them prevents XSS attacks.
Now flush your cache and visit the URL: bin/magento cache:flush
You should see your "Hello, World!" page at /helloworld/index/index.
Models, Resource Models & Collections
Magento 2's data layer has three parts that often confuse beginners
Here's the clear explanation.
The Three-Part Data Layer
- 1. Model: Represents a single database record. Contains business logic and data manipulation methods.
- 2. Resource Model: Handles the actual database operations (INSERT, UPDATE, DELETE, SELECT). Think of it as the database adapter for the Model.
- 3. Collection: Represents a group of Models. Used to query multiple records with filters, sorting, and pagination.
Let's build a complete example.
Imagine we're storing custom greetings in the database.
Step 1: Create the Database Table via InstallSchema
app/code/MyCompany/HelloWorld/Setup/InstallSchema.php:
<?php
namespace MyCompany\HelloWorld\Setup;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\DB\Ddl\Table;
class InstallSchema implements InstallSchemaInterface
{
/**
* Installs DB schema for a module
*
* @param SchemaSetupInterface $setup
* @param ModuleContextInterface $context
* @return void
*/
public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
{
$installer = $setup;
$installer->startSetup();
if (!$installer->tableExists('mycompany_greeting')) {
$table = $installer->getConnection()
->newTable($installer->getTable('mycompany_greeting'))
->addColumn(
'greeting_id',
Table::TYPE_INTEGER,
null,
['identity' => true, 'nullable' => false, 'primary' => true, 'unsigned' => true],
'Greeting ID'
)
->addColumn(
'message',
Table::TYPE_TEXT,
255,
['nullable' => false],
'Greeting Message'
)
->addColumn(
'is_active',
Table::TYPE_SMALLINT,
null,
['nullable' => false, 'default' => '1'],
'Is Active'
)
->addColumn(
'created_at',
Table::TYPE_TIMESTAMP,
null,
['nullable' => false, 'default' => Table::TIMESTAMP_INIT],
'Created At'
)
->setComment('Greeting Table');
$installer->getConnection()->createTable($table);
}
$installer->endSetup();
}
}
|
Step 2: Create the Model
app/code/MyCompany/HelloWorld/Model/Greeting.php:
<?php
namespace MyCompany\HelloWorld\Model;
use Magento\Framework\Model\AbstractModel;
use MyCompany\HelloWorld\Model\ResourceModel\Greeting as GreetingResource;
class Greeting extends AbstractModel
{
/**
* Initialize resource model
*
* @return void
*/
protected function _construct()
{
$this->_init(GreetingResource::class);
}
}
|
Step 3: Create the Resource Model
app/code/MyCompany/HelloWorld/Model/ResourceModel/Greeting.php:
<?php
namespace MyCompany\HelloWorld\Model\ResourceModel;
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
class Greeting extends AbstractDb
{
/**
* Initialize resource model
*
* @return void
*/
protected function _construct()
{
// Table name, primary key column
$this->_init('mycompany_greeting', 'greeting_id');
}
}
|
Step 4: Create the Collection
app/code/MyCompany/HelloWorld/Model/ResourceModel/Greeting/Collection.php:
<?php
namespace MyCompany\HelloWorld\Model\ResourceModel\Greeting;
use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;
use MyCompany\HelloWorld\Model\Greeting as GreetingModel;
use MyCompany\HelloWorld\Model\ResourceModel\Greeting as GreetingResource;
class Collection extends AbstractCollection
{
/**
* Initialize collection
*
* @return void
*/
protected function _construct()
{
$this->_init(GreetingModel::class, GreetingResource::class);
}
}
// Using the Model in Your Code
// Load a single greeting by ID
$greeting = $this->greetingFactory->create()->load(1);
echo $greeting->getMessage();
// Get all active greetings via Collection
$collection = $this->greetingCollectionFactory->create()
->addFieldToFilter('is_active', 1)
->setOrder('created_at', 'DESC');
foreach ($collection as $item) {
echo $item->getMessage();
}
|
Run bin/magento setup:upgrade to create the database table.
Dependency Injection in Magento 2
Dependency Injection is the heart of Magento 2. If you only understand one architectural concept deeply, make it this one.
What Is Dependency Injection?
Instead of a class creating its own dependencies (using new ClassName() or Mage::getModel()), dependencies are injected from outside, typically through the constructor.
Without DI (old way):
class MyClass
{
public function doSomething()
{
// Bad: tightly coupled, hard to test
$helper = new \Magento\Catalog\Helper\Product();
return $helper->getProduct();
}
}
|
With DI (Magento 2 way):
class MyClass
{
private \Magento\Catalog\Helper\Product $productHelper;
public function __construct(
\Magento\Catalog\Helper\Product $productHelper
) {
$this->productHelper = $productHelper;
}
public function doSomething()
{
// Good: loosely coupled, easy to test and swap
return $this->productHelper->getProduct();
}
}
|
Magento's Object Manager reads your constructor and automatically instantiates and injects all required dependencies. You never call the Object Manager directly in your code, the framework handles it.
The di.xml File: Configuring Dependencies
The di.xml file is where you configure how the DI container should behave: which concrete class to use for an interface, constructor argument values, and more
app/code/MyCompany/HelloWorld/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">
<!-- Bind an interface to its concrete implementation -->
<preference for="MyCompany\HelloWorld\Api\GreetingInterface"
type="MyCompany\HelloWorld\Model\Greeting"/>
<!-- Configure constructor argument values -->
<type name="MyCompany\HelloWorld\Block\Hello">
<arguments>
<argument name="greeting" xsi:type="string">
Hello from di.xml!
</argument>
</arguments>
</type>
</config>
|
Interfaces Over Concrete Classes
A best practice is to always type-hint interfaces in your constructors, not concrete classes:
// Prefer this (interface)
public function __construct(
\Magento\Catalog\Api\ProductRepositoryInterface $productRepository
) {}
// Over this (concrete class)
public function __construct(
\Magento\Catalog\Model\ProductRepository $productRepository
) {}
|
This makes your code more flexible and testable.
Virtual Types
Virtual types let you create variants of a class with different constructor arguments, without writing a new PHP class:
<virtualType name="MyCompany\HelloWorld\Model\CustomLogger"
type="Magento\Framework\Logger\Monolog">
<arguments>
<argument name="name" xsi:type="string">helloworld</argument>
</arguments>
</virtualType>
|
Plugins (Interceptors): The Magento Magic
Plugins are one of Magento 2's most powerful features.
They let you modify the behavior of any public method in any class, without touching the original code.
There are three types of plugins:
- 1. Before: Runs before the original method. Can modify the input arguments.
- 2. After: Runs after the original method. Can modify the return value.
- 3. Around: Wraps the original method. Has complete control over execution.
Creating a Plugin
Let's say we want to add " - Extended!" to every product name fetched from the catalog.
Step 1: Declare the Plugin in di.xml
<type name="Magento\Catalog\Model\Product">
<plugin name="mycompany_helloworld_product_name_plugin"
type="MyCompany\HelloWorld\Plugin\Product\NamePlugin"
sortOrder="10"
disabled="false"/>
</type>
|
Step 2: Create the Plugin Class
app/code/MyCompany/HelloWorld/Plugin/Product/NamePlugin.php:
<?php
namespace MyCompany\HelloWorld\Plugin\Product;
use Magento\Catalog\Model\Product;
class NamePlugin
{
/**
* After plugin for getName method
* Modifies the product name after it's fetched
*
* @param Product $subject The original object
* @param string $result The original method's return value
* @return string
*/
public function afterGetName(Product $subject, string $result): string
{
return $result . ' - Extended!';
}
}
|
Before Plugin Example
Before plugins receive the original arguments and can modify them:
public function beforeSetName(
Product $subject,
string $name
): array {
// Prepend "Product: " to every product name on save
return ['Product: ' . $name];
}
|
Around Plugin Example
Around plugins are the most powerful (and most dangerous if overused):
public function aroundGetName(
Product $subject,
callable $proceed
): string {
// You control whether the original method runs
$originalResult = $proceed();
if ($subject->getTypeId() === 'simple') {
return '[Simple] ' . $originalResult;
}
return $originalResult;
}
|
Best Practice: Use after and before plugins whenever possible. Use around only when you need to conditionally skip the original method. Around plugins add more overhead.
Events & Observers
While plugins modify method behavior, Events & Observers respond to things that happen in the system.
Think of it like a publish/subscribe pattern: Magento fires an event ("a product was saved"), and your observer code runs in response.
Registering an Observer
app/code/MyCompany/HelloWorld/etc/events.xml (or etc/frontend/events.xml / etc/adminhtml/events.xml for scope-specific):
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="catalog_product_save_after">
<observer name="mycompany_product_save_observer"
instance="MyCompany\HelloWorld\Observer\ProductSaveAfter"/>
</event>
</config>
|
Creating the Observer Class
app/code/MyCompany/HelloWorld/Observer/ProductSaveAfter.php:
<?php
namespace MyCompany\HelloWorld\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Psr\Log\LoggerInterface;
class ProductSaveAfter implements ObserverInterface
{
/**
* @var LoggerInterface
*/
private LoggerInterface $logger;
/**
* Constructor
*
* @param LoggerInterface $logger
*/
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* Execute observer
*
* @param Observer $observer
* @return void
*/
public function execute(Observer $observer): void
{
/** @var \Magento\Catalog\Model\Product $product */
$product = $observer->getEvent()->getProduct();
$this->logger->info(
sprintf(
'Product saved: ID=%s, Name=%s',
$product->getId(),
$product->getName()
)
);
}
}
|
Finding Available Events
Magento has hundreds of built-in events.
To find them, search the codebase for $this->_eventManager->dispatch( or $this->eventManager->dispatch).
You can also fire your own custom events:
$this->eventManager->dispatch(
'mycompany_custom_event',
[
'product' => $product,
'extra_data' => 'some value'
]
);
|
Plugins vs Events, When to Use Which?
| Use Plugins When | Use Events When |
|---|---|
| You need to modify method inputs/outputs | You need to react to something that happened |
| You want to wrap a specific method | You want to do something after a save/load/delete |
| Changing behavior of a class | Logging, sending emails, updating related data |
| You need access to the original return value | Decoupled reactions to business processes |
Admin Grids and Forms
Most Magento modules need an admin panel section for managing data. Magento 2 provides a powerful UI component system for building admin grids (listing pages) and forms (create/edit pages).
Setting Up Admin Routes
app/code/MyCompany/HelloWorld/etc/adminhtml/routes.xml:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="admin">
<route id="mycompany_greeting" frontName="mycompany_greeting">
<module name="MyCompany_HelloWorld"/>
</route>
</router>
</config>
|
Adding an Admin Menu Item
app/code/MyCompany/HelloWorld/etc/adminhtml/menu.xml:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd">
<menu>
<add id="MyCompany_HelloWorld::greeting_management"
title="Greetings"
module="MyCompany_HelloWorld"
sortOrder="51"
parent="Magento_Backend::content"
action="mycompany_greeting/greeting/index"
resource="MyCompany_HelloWorld::greeting_management"/>
</menu>
</config>
|
The Admin Grid Controller
app/code/MyCompany/HelloWorld/Controller/Adminhtml/Greeting/Index.php:
<?php
namespace MyCompany\HelloWorld\Controller\Adminhtml\Greeting;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\View\Result\PageFactory;
class Index extends Action
{
const ADMIN_RESOURCE = 'MyCompany_HelloWorld::greeting_management';
/**
* @var PageFactory
*/
private PageFactory $resultPageFactory;
/**
* Constructor
*
* @param Context $context
* @param PageFactory $resultPageFactory
*/
public function __construct(
Context $context,
PageFactory $resultPageFactory
) {
parent::__construct($context);
$this->resultPageFactory = $resultPageFactory;
}
/**
* Index action
*
* @return \Magento\Framework\View\Result\Page
*/
public function execute()
{
$resultPage = $this->resultPageFactory->create();
$resultPage->getConfig()
->getTitle()
->prepend(__('Manage Greetings'));
return $resultPage;
}
}
|
UI Components for grids involve several XML configuration files (the ui_component directory).
This is a deep topic. exploring the Magento_Ui module source code and studying existing admin modules like Magento_Catalog will give you the best real-world examples to follow.
REST & GraphQL APIs
Modern Magento 2 stores often power headless frontends, mobile apps, and third-party integrations.
Magento 2 provides both REST and GraphQL APIs out of the box.
Defining a Custom REST API Endpoint
Start by defining the API interface (Service Contract):
app/code/MyCompany/HelloWorld/Api/GreetingRepositoryInterface.php:
<?php
namespace MyCompany\HelloWorld\Api;
interface GreetingRepositoryInterface
{
/**
* Get greeting by ID
*
* @param int $id
* @return \MyCompany\HelloWorld\Api\Data\GreetingInterface
* @throws \Magento\Framework\Exception\NoSuchEntityException
*/
public function getById(
int $id
): Data\GreetingInterface;
/**
* Get list of greetings
*
* @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
* @return \MyCompany\HelloWorld\Api\Data\GreetingSearchResultsInterface
*/
public function getList(
\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
): Data\GreetingSearchResultsInterface;
/**
* Save greeting
*
* @param \MyCompany\HelloWorld\Api\Data\GreetingInterface $greeting
* @return \MyCompany\HelloWorld\Api\Data\GreetingInterface
*/
public function save(
Data\GreetingInterface $greeting
): Data\GreetingInterface;
/**
* Delete greeting
*
* @param \MyCompany\HelloWorld\Api\Data\GreetingInterface $greeting
* @return bool
*/
public function delete(
Data\GreetingInterface $greeting
): bool;
}
|
Register the REST route in webapi.xml:
app/code/MyCompany/HelloWorld/etc/webapi.xml:
<?xml version="1.0"?>
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation=
"urn:magento:module:Magento_Webapi:etc/webapi.xsd">
<route url="/V1/mycompany/greetings/:greetingId" method="GET">
<service
class="MyCompany\HelloWorld\Api\GreetingRepositoryInterface"
method="getById"/>
<resources>
<resource ref="anonymous"/>
</resources>
</route>
<route url="/V1/mycompany/greetings" method="GET">
<service
class="MyCompany\HelloWorld\Api\GreetingRepositoryInterface"
method="getList"/>
<resources>
<resource ref="Magento_Customer::self"/>
</resources>
</route>
<route url="/V1/mycompany/greetings" method="POST">
<service
class="MyCompany\HelloWorld\Api\GreetingRepositoryInterface"
method="save"/>
<resources>
<resource
ref="MyCompany_HelloWorld::greeting_management"/>
</resources>
</route>
</routes>
|
Now your API endpoint is available at:
GET /rest/V1/mycompany/greetings/1
GET /rest/V1/mycompany/greetings?searchCriteria[pageSize]=10
POST /rest/V1/mycompany/greetings
Testing the API
Use Postman or cURL (Client URL) to test:
# Get a greeting (no auth needed for anonymous resources)
curl -X GET \
"https://yourstore.com/rest/V1/mycompany/greetings/1" \
-H "Content-Type: application/json"
# Get admin token first for protected endpoints
curl -X POST \
"https://yourstore.com/rest/V1/integration/admin/token" \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "Admin123!"}'
CLI (Command-Line Interface) Commands
The bin/magento CLI tool is your command center.
Here are the commands you'll use every day:
Cache Management
# Flush all cache
bin/magento cache:flush
# Clean specific cache type
bin/magento cache:clean config
# Disable full page cache (helpful in development)
bin/magento cache:disable full_page
# List all cache types and their status
bin/magento cache:status
Indexing
# Reindex everything
bin/magento indexer:reindex
# Reindex a specific indexer
bin/magento indexer:reindex catalog_product_price
# Check indexer status
bin/magento indexer:status
Deployment & Static Content
# Deploy static content (required after theme changes in production)
bin/magento setup:static-content:deploy en_US
# Compile Dependency Injection (required in production)
bin/magento setup:di:compile
# Run upgrade scripts after installing/updating a module
bin/magento setup:upgrade
Module Management
# Enable a module
bin/magento module:enable MyCompany_HelloWorld
# Disable a module
bin/magento module:disable MyCompany_HelloWorld
# List all modules and their status
bin/magento module:status
Creating Custom CLI Commands in Magento 2
You can create your own CLI commands for tasks like data migrations or scheduled processes:
app/code/MyCompany/HelloWorld/Console/Command/SayHello.php:
<?php
namespace MyCompany\HelloWorld\Console\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class SayHello extends Command
{
/**
* Configure the command
*/
protected function configure(): void
{
$this->setName('mycompany:hello')
->setDescription('Prints a hello world message');
}
/**
* Execute the command
*
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(
InputInterface $input,
OutputInterface $output
): int {
$output->writeln(
'<info>Hello from Magento 2 CLI!</info>'
);
return Command::SUCCESS;
}
}
|
Register it in di.xml:
<type name="Magento\Framework\Console\CommandList">
<arguments>
<argument name="commands" xsi:type="array">
<item
name="mycompany_say_hello"
xsi:type="object">
MyCompany\HelloWorld\Console\Command\SayHello
</item>
</argument>
</arguments>
</type>
|
Then run:
bin/magento mycompany:hello
Magento 2 Best Practices & Next Steps
You've covered a tremendous amount of ground. Before you go build something amazing, here are the best practices that separate good Magento developers from great ones.
Golden Rules of Magento 2 Development
Never edit core files. Use plugins, observers, preferences, and layout overrides instead.
Any changes to vendor files will be lost on the next composer update.
- Always work with interfaces, not concrete classes. This makes your code testable and future-proof.
- Use Service Contracts. When integrating with other modules, use their repository interfaces and API interfaces, not their models directly.
- Enable developer mode during development. It shows detailed errors, disables caching, and enables developer tools: bin/magento deploy:mode:set develope
- Use $this->_logger or inject Psr\Log\LoggerInterface for logging Never use echo, var_dump, or die in production code.
- Write unit tests. Magento 2 has excellent PHPUnit support. Every model, helper, and service class should have tests.
- Follow the Magento coding standards. Use the built-in code sniffer: vendor/bin/phpcs --standard=Magento2 app/code/MyCompany/
Magento 2 Performance Tips for Developers
- Always run in production mode for performance testing
- Use Redis for cache and session storage in production
- Use Varnish for full page cache in production
- Minimize database queries, use collections with joins rather than lazy loading in loops
- Use afterLoad and beforeSave methods instead of observers when possible for model-specific logic
Read More: Why Is My Magento Store So Slow? (And How to Actually Fix It)
What to Learn Next
Now that you have the fundamentals, here's a recommended learning path:
- Frontend Development (Themes): Learn about Magento 2 themes, the LESS/CSS compilation process, the ui_component library, Knockout.js for dynamic frontend, and RequireJS for JavaScript management.
- Checkout Customization: One of the most complex but most requested skills. Magento 2's checkout is built entirely in Knockout.js and has its own customization patterns.
- Performance Optimization: Learn about Full Page Cache, Varnish, flat tables, and asynchronous operations.
- Testing: Write unit tests with PHPUnit, integration tests, and use the Magento Testing Framework (MTF).
- Adobe Commerce (Enterprise): If you work with enterprise clients, explore the exclusive B2B features, Staging & Preview, and Customer Segmentation.
- Headless/PWA Studio: The modern approach to Magento frontends using React and GraphQL. Adobe's PWA Studio is the official framework, but Vue Storefront is also widely used.
Useful Resources
Official Magento Developer Documentation: developer.adobe.com/commerce
Magento Community Forums: magento-opensource.com/resources/resources-community-support
Magento Stack Exchange: magento.stackexchange.com
Conclusion
You've just taken a deep dive into the world of Magento 2 development. From spinning up your first installation to building modules with controllers, layouts, models, plugins, observers, and REST APIs, you now have a solid foundation to build on.
Here's the truth about learning Magento 2: the best way to really understand it is to build real things. Create a module that solves an actual problem. Break things. Read core Magento code to understand how the experts do it. Ask questions in the community forums.
Magento 2 is complex, but that complexity comes with tremendous power. Once you have it under your fingers, you can build virtually any e-commerce feature imaginable.
The learning curve is steep, but so is the reward. Keep going.
Happy coding!