Three-Layer Model Architecture
The Model Pattern
Magento uses a three-layer pattern for data objects:
Service Contract (Interface)
↓
Data Model (business logic + data)
↓
Resource Model (database operations)
This separates concerns:
- Interface: Defines the API for external consumers
- Data Model: Contains business logic, validation, and data access
- Resource Model: Handles database queries and table operations
Directory Structure
Vendor/Module/Model/
├── WarrantyRepositoryInterface.php (service contract)
├── WarrantyInterface.php (data interface)
├── Warranty.php (data model)
├── WarrantyFactory.php (auto-generated)
├── ResourceModel/
│ ├── Warranty.php (resource model)
│ └── Warranty/Collection.php (collection)
└── ResourceModel/
└── Warranty/
└── Collection.php (alternative location)
Data Model Example
namespace Vendor\Module\Model;
use Magento\Framework\Model\AbstractModel;
use Vendor\Module\Model\ResourceModel\Warranty as WarrantyResource;
class Warranty extends AbstractModel
{
const CACHE_TAG = 'vendor_warranty';
protected function _construct(): void
{
$this->_init(WarrantyResource::class);
}
public function getId(): ?int
{
return parent::getId();
}
public function getName(): ?string
{
return $this->getData('name');
}
public function setName(string $name): Warranty
{
return $this->setData('name', $name);
}
public function getDuration(): ?int
{
return (int)$this->getData('duration');
}
public function setDuration(int $duration): Warranty
{
return $this->setData('duration', $duration);
}
}
The _construct Method
The _construct() (note: single underscore) method initializes the model with its resource model. It's called by the parent constructor — never call it directly.
protected function _construct(): void
{
$this->_init(WarrantyResource::class);
// or for collections:
$this->_init(WarrantyResource::class, "Collection"::class);
}
Resource Models
Resource Model Purpose
Resource models handle all database operations: save, load, delete, and queries. They know the table name and column structure.
Basic Resource Model
namespace Vendor\Module\Model\ResourceModel;
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
class Warranty extends AbstractDb
{
protected function _construct(): void
{
$this->_init('vendor_warranty', 'warranty_id');
// table name, primary key column
}
}
CRUD Operations
// Loading a model
$warranty = $this->warrantyFactory->create();
$this->resourceModel->load($warranty, $warrantyId);
// Saving a model
$warranty->setName('Premium Warranty');
$warranty->setDuration(24);
$this->resourceModel->save($warranty);
// Deleting a model
$this->resourceModel->delete($warranty);
Resource Model with Custom Queries
namespace Vendor\Module\Model\ResourceModel;
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
class Warranty extends AbstractDb
{
protected function _construct(): void
{
$this->_init('vendor_warranty', 'warranty_id');
}
public function loadActive(): array
{
$connection = $this->getConnection();
$select = $connection->select()
->from($this->getTable('vendor_warranty'))
->where('status = ?', 'active')
->order('created_at DESC');
return $connection->fetchAll($select);
}
public function getCountByStatus(string $status): int
{
$connection = $this->getConnection();
$select = $connection->select()
->from($this->getTable('vendor_warranty'), 'COUNT(*)')
->where('status = ?', $status);
return (int)$connection->fetchOne($select);
}
}
Collection Model
Collections represent a set of models with query building:
namespace Vendor\Module\Model\ResourceModel\Warranty;
use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;
use Vendor\Module\Model\Warranty;
use Vendor\Module\Model\ResourceModel\Warranty as WarrantyResource;
class Collection extends AbstractCollection
{
protected function _construct(): void
{
$this->_init(Warranty::class, "WarrantyResource"::class);
}
public function addActiveFilter(): Collection
{
$this->addFieldToFilter('status', 'active');
return $this;
}
}
Service Contracts (Interfaces)
Why Service Contracts?
Service contracts decouple the API from the implementation. Other modules depend on interfaces, not concrete classes. This allows Magento to upgrade internals without breaking extensions.
Repository Interface
namespace Vendor\Module\Api;
use Vendor\Module\Api\Data\WarrantyInterface;
use Magento\Framework\Api\SearchCriteriaInterface;
class WarrantyRepositoryInterface
{
public function save(WarrantyInterface $warranty): WarrantyInterface;
public function get(int $id): WarrantyInterface;
public function delete(WarrantyInterface $warranty): bool;
public function deleteById(int $id): bool;
public function getList(SearchCriteriaInterface $searchCriteria):
\Magento\Framework\Api\SearchResultsInterface;
}
Data Interface
namespace Vendor\Module\Api\Data;
interface WarrantyInterface
{
const ID = 'warranty_id';
const NAME = 'name';
const DURATION = 'duration';
const STATUS = 'status';
const CREATED_AT = 'created_at';
public function getId(): ?int;
public function getName(): ?string;
public function getDuration(): ?int;
public function getStatus(): ?string;
public function getCreatedAt(): ?string;
public function setId(?int $id): WarrantyInterface;
public function setName(?string $name): WarrantyInterface;
public function setDuration(?int $duration): WarrantyInterface;
public function setStatus(?string $status): WarrantyInterface;
public function setCreatedAt(?string $createdAt): WarrantyInterface;
}
Repository Implementation
namespace Vendor\Module\Model;
use Vendor\Module\Api\WarrantyRepositoryInterface;
use Vendor\Module\Api\Data\WarrantyInterface;
use Vendor\Module\Model\WarrantyFactory;
use Vendor\Module\Model\ResourceModel\Warranty as WarrantyResource;
use Vendor\Module\Model\ResourceModel\Warranty\CollectionFactory;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SearchResultsInterfaceFactory;
class WarrantyRepository implements WarrantyRepositoryInterface
{
public function __construct(
private WarrantyFactory $warrantyFactory,
private WarrantyResource $resource,
private CollectionFactory $collectionFactory,
private SearchResultsInterfaceFactory $searchResultsFactory
) {}
public function save(WarrantyInterface $warranty): WarrantyInterface
{
$this->resource->save($warranty);
return $warranty;
}
public function get(int $id): WarrantyInterface
{
$warranty = $this->warrantyFactory->create();
$this->resource->load($warranty, $id);
if (!$warranty->getId()) {
throw new \Magento\Framework\Exception\NoSuchEntityException(
__('Warranty with ID %1 not found', $id)
);
}
return $warranty;
}
public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface
{
$collection = $this->collectionFactory->create();
// Apply search criteria...
$searchResults = $this->searchResultsFactory->create();
$searchResults->setSearchCriteria($searchCriteria);
$searchResults->setItems($collection->getItems());
$searchResults->setTotalCount($collection->getSize());
return $searchResults;
}
}
di.xml Preference
Bind the interface to the implementation:
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="Vendor\Module\Api\WarrantyRepositoryInterface"
type="Vendor\Module\Model\WarrantyRepository"/>
</config>
Data Addition and Factory Pattern
Data Addition Pattern
Models use a data array internally. Getters/setters access this array:
// AbstractModel provides:
$warranty->getData(); // all data
$warranty->getData('name'); // specific field
$warranty->setData('name', 'X'); // set specific field
$warranty->addData(['name' => 'X', 'duration' => 12]);
$warranty->unsetData('name'); // remove field
Magic Methods
AbstractModel uses __call for dynamic getters/setters:
// These are equivalent:
$warranty->getName();
$warranty->getData('name');
$warranty->setName('Premium');
$warranty->setData('name', 'Premium');
// For complex keys:
$warranty->getSomeFieldName(); // maps to some_field_name
$warranty->setSomeFieldName('X');
Factory Pattern
Factories create model instances with DI:
// Auto-generated WarrantyFactory
class WarrantyFactory
{
public function create(array $data = []): Warranty
{
// Creates instance via ObjectManager
}
}
// Usage in a service class:
class WarrantyService
{
public function __construct(
private WarrantyFactory $warrantyFactory
) {}
public function createWarranty(string $name, int $duration): Warranty
{
$warranty = $this->warrantyFactory->create();
$warranty->setName($name);
$warranty->setDuration($duration);
$warranty->setStatus('active');
return $warranty;
}
}
When to Use Each Pattern
| Pattern | Use Case |
|---|---|
| Data Model | Business logic, validation, getters/setters |
| Resource Model | Database queries, table operations |
| Collection | Querying multiple records |
| Repository | CRUD API via service contract |
| Factory | Creating model instances |
Named Arguments (PHP 8+)
$warranty = $this->warrantyFactory->create([
'name' => 'Premium Warranty',
'duration' => 24,
'status' => 'active',
]);
This calls addData() internally with the provided array.
Quiz
1. What is the purpose of the _construct() method in a data model?
2. What does a resource model handle?
3. What is the benefit of using service contracts (interfaces)?
Flashcards
Question
What are the three layers of Magento's model architecture?
Click to reveal answer
Answer
Interface (service contract), Data Model (business logic), Resource Model (database)
Question
What does _init() do in a model's _construct()?
Click to reveal answer
Answer
Binds the model to its resource model class
Question
What is the difference between getData() and getName()?
Click to reveal answer
Answer
They're equivalent — getName() is a magic method that calls getData('name')
Question
What does a Factory create?
Click to reveal answer
Answer
Model instances via DI, allowing constructor injection in the created objects
Question
Where should service contract interfaces live?
Click to reveal answer
Answer
Vendor/Module/Api/ directory (e.g., WarrantyRepositoryInterface.php)
Revision Notes
Key Takeaways
- 1. Magento uses a three-layer model: Interface → Data Model → Resource Model
- 2. Data models extend AbstractModel and use _construct() to bind to resource models
- 3. Resource models extend AbstractDb and handle database operations
- 4. Service contracts (interfaces) decouple API from implementation
- 5. Factories create model instances with full DI support
Interview Tips
- • Explain the three-layer model architecture and why it exists
- • Know when to use repositories vs direct model access
- • Understand the factory pattern and why it's needed for DI
- • Be ready to discuss service contracts and backward compatibility
Cheat Sheet
Model architecture:
Interface → Data Model → Resource Model
Data Model:
extends AbstractModel
_construct(): $this->_init(ResourceModel::class)
$this->getData('key') / $this->setData('key', $val)
Resource Model:
extends AbstractDb
_construct(): $this->_init('table_name', 'pk_column')
Factory:
$this->factory->create(['key' => 'val'])