Skip to content
intermediate Phase 22 · Module Directories Deep Dive

Model Directory - Module Directories Deep Dive

Understanding the Magento 2 Model directory: data models, resource models, interfaces, and the data/resource/model architecture

45m
0 problems
Topic Progress 0%

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?

Question 1 options

2. What does a resource model handle?

Question 2 options

3. What is the benefit of using service contracts (interfaces)?

Question 3 options

Flashcards

Question

What are the three layers of Magento's model architecture?

Answer

Interface (service contract), Data Model (business logic), Resource Model (database)

Question

What does _init() do in a model's _construct()?

Answer

Binds the model to its resource model class

Question

What is the difference between getData() and getName()?

Answer

They're equivalent — getName() is a magic method that calls getData('name')

Question

What does a Factory create?

Answer

Model instances via DI, allowing constructor injection in the created objects

Question

Where should service contract interfaces live?

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'])