3.04

Models, Resource Models & Collections

Master the four types of ORM classes in Magento: Models, Resource Models, Collections, and Repositories, and understand their responsibilities and relationships.

Why This Matters: Understanding Models, Resource Models, Collections, and Repositories is fundamental to working with data in Magento. This is one of the most important topics for the exam and daily development.

ORM Objects Overview

mindmap root((ORM Objects)) Models Store data Getters/setters Model/ directory One row Resource Models Database operations save/load/delete Model/ResourceModel/ Custom queries Collections Load multiple models addFieldToFilter join Iterable Repositories API wrapper save/getById/getList/delete No state Interface

Four Types of ORM Classes

There are four types of classes related to loading and storing data in Magento:

  1. Models - Store data (one row)
  2. Resource Models - Database operations (save/load/delete)
  3. Collections - Load multiple models
  4. Repositories - API wrapper for data operations

Models

What are Models?

Models are classes with getters and setters to store data. An instantiated class would represent one row in the database.

Location: Stored directly in the module's Model/ directory

Example:

Magento\Cms\Model\Page

Model Initialization

This model is initialized with a reference to the Resource Model.

<?php
namespace Vendor\Module\Model;

use Magento\Framework\Model\AbstractModel;

class CustomEntity extends AbstractModel
{
    protected function _construct()
    {
        $this->_init(\Vendor\Module\Model\ResourceModel\CustomEntity::class);
    }
}

Data Storage in Models

Models that extend \Magento\Framework\Model\AbstractModel will thus extend \Magento\Framework\DataObject. This means that the data is stored in the class' $_data property.

Magic Getters and Setters

You can convert snake-case notation to camel-case notation and use magic methods:

// Column name: discount_amount
// Magic getter
$discountAmount = $model->getDiscountAmount();

// Magic setter
$model->setDiscountAmount(100);

// OR use getData/setData
$discountAmount = $model->getData('discount_amount');
$model->setData('discount_amount', 100);

Explicit Getters and Setters (Best Practice)

Instead of magic getters/setters, it's recommended to build out explicit methods:

public function getDiscountAmount(): float
{
    return (float)$this->getData('discount_amount');
}

public function setDiscountAmount(float $discountAmount): void
{
    $this->setData('discount_amount', $discountAmount);
}

Advantages:

  • Type hints - reduces unnecessary conversions (float to int, etc.)
  • Mockable - can mock these methods in unit tests
  • Custom handling - can customize how data is handled (e.g., json_encode in setter)

Resource Models

What are Resource Models?

Resource models are classes responsible for saving and loading data. They handle direct database operations.

Location: Stored in the module's Model/ResourceModel/ directory

Example:

Magento\Cms\Model\ResourceModel\Page

Resource Model Inheritance

These classes almost always inherit \Magento\Framework\Model\ResourceModel\Db\AbstractDb.

Primary Methods:

  • load() - Load data into model
  • save() - Save model data to database
  • delete() - Delete model data from database

Resource Model Configuration

Resource models are initialized with:

  • The table name (as found in db_schema.xml)
  • The name of the primary key column
<?php
namespace Vendor\Module\Model\ResourceModel;

use Magento\Framework\Model\ResourceModel\Db\AbstractDb;

class CustomEntity extends AbstractDb
{
    protected function _construct()
    {
        $this->_init('vendor_module_custom_entity', 'entity_id');
        //       table name                        primary key
    }
}

Custom Queries in Resource Models

The resource model is the place to put any custom selects (using the Magento ORM, and NOT writing direct SQL).

Example: Get Count with Custom Query

public function getAvailableToTranslate(string $language): int
{
    $select = $this->getConnection()->select();
    $select->from($this->getMainTable(), 'COUNT(id)');
    $select->where('needs_translation = ?', true);
    $select->where('language = ?', $language);
    
    return (int)$this->getConnection()->fetchOne($select);
}
Performance Benefit: This is often much more efficient than loading rows with a collection and iterating/tallying up the results.

Fetch Methods

Various fetch methods available for different result types:

Method Returns Use Case
fetchOne() Single value Get one result (e.g., COUNT, single column value)
fetchAll() Array of all rows Get all rows as associative arrays
fetchCol() Array of values from one column Get all values from a single column
fetchPairs() Key-value pairs Get first column as key, second as value
fetchRow() Single row as array Get one row as associative array
fetchAssoc() Associative array Get rows with first column as array key

Collections

What are Collections?

Collections load multiple models. They handle loading multiple rows for a database entity or table.

Location: Stored in Model/ResourceModel/[ModelName]/Collection.php

Example:

Magento\Cms\Model\ResourceModel\Page\Collection

Collection Initialization

This class is initialized with a reference to the Model and Resource Model.

<?php
namespace Vendor\Module\Model\ResourceModel\CustomEntity;

use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;

class Collection extends AbstractCollection
{
    protected function _construct()
    {
        $this->_init(
            \Vendor\Module\Model\CustomEntity::class,
            \Vendor\Module\Model\ResourceModel\CustomEntity::class
        );
    }
}

Collections Store State

⚠️ Important: Collections store state. If you wish to utilize a collection in one of your classes, you must use a Factory and instantiate that collection instead of injecting the collection class itself.
// DON'T inject the collection directly
// DO inject the CollectionFactory

public function __construct(
    \Magento\Cms\Model\ResourceModel\Page\CollectionFactory $collectionFactory
) {
    $this->collectionFactory = $collectionFactory;
}

public function getPages()
{
    $collection = $this->collectionFactory->create(); // Instantiate fresh
    return $collection;
}

Collections are Iterable

Because collections implement the \IteratorAggregate interface, you can loop through a collection in a foreach() method.

$collection = $this->collectionFactory->create();
$collection->addFieldToFilter('status', 1);

foreach ($collection as $item) {
    echo $item->getName();
}

Common Collection Methods

1. addFieldToFilter()

This is the equivalent of adding a WHERE clause to a SQL select.

$collection->addFieldToFilter('product_id', ['neq' => $productId]);
// SQL: SELECT * FROM table_name WHERE product_id <> 4;
Common Conditionals:
  • ['eq' => $value] - Equal to
  • ['neq' => $value] - Not equal to
  • ['gt' => $value] - Greater than
  • ['gteq' => $value] - Greater than or equal
  • ['lt' => $value] - Less than
  • ['lteq' => $value] - Less than or equal
  • ['like' => '%value%'] - LIKE pattern
  • ['in' => [$val1, $val2]] - IN array
  • ['nin' => [$val1, $val2]] - NOT IN array
  • ['null' => true] - IS NULL
  • ['notnull' => true] - IS NOT NULL

2. addAttributeToFilter()

On EAV entities (Magento\Eav\Model\Entity\Collection\AbstractCollection): this is the same as addFieldToFilter() for EAV-enabled entities. If you call either method, you get the same result.

// For EAV entities (products, categories, customers)
$collection->addAttributeToFilter('status', 1);

3. join()

Joins in another table.

$collection->join(
    ['table_alias' => 'table_name'],
    'main_table.product_id = table_alias.product_id',
    ['column_a']
);

// Results in:
// SELECT *, table_alias.column_a 
// FROM original_table AS main_table
// INNER JOIN table_name AS table_alias 
//     ON main_entity.product_id = table_alias.product_id;
Note: main_table is the default alias with collections.

4. load()

This fetches the results of the collection. Data is loaded from the database and then models are hydrated with said data.

$collection->load(); // Executes the query and loads data

5. getSize()

This creates a copy of the select, strips out the columns, and uses the COUNT method.

$count = $collection->getSize(); // Returns count of items
⚠️ Note: If you are making many customizations to collections, such as adding groups, you will likely have problems with this method and would want to use an after plugin to alter the results of this select.

Repositories

What are Repositories?

Repositories handle the primary actions that happen to a database table. They are often a wrapper for the Resource Model.

Key Concept: Repositories do not store state. They are essentially a wrapper to ease the effort of data operations.

Repository Methods

Repositories usually have:

  • save() - Save a model
  • getById() - Get one row/model from the database
  • getList() - Accepts a SearchCriteria class, returns multiple items
  • delete() - Delete a model

Repository Structure

Repositories do not inherit another class. They represent more of an idea (unlike models, resource models, or collections).

Common Injected Classes:

  • Model's Factory
  • Resource Model
  • Collection Factory
  • SearchResults Factory

Example:

Magento\Cms\Model\BlockRepository

Repository Example

<?php
namespace Vendor\Module\Model;

use Vendor\Module\Api\CustomEntityRepositoryInterface;
use Vendor\Module\Model\CustomEntityFactory;
use Vendor\Module\Model\ResourceModel\CustomEntity as ResourceModel;

class CustomEntityRepository implements CustomEntityRepositoryInterface
{
    private $entityFactory;
    private $resourceModel;
    
    public function __construct(
        CustomEntityFactory $entityFactory,
        ResourceModel $resourceModel
    ) {
        $this->entityFactory = $entityFactory;
        $this->resourceModel = $resourceModel;
    }
    
    public function getById(int $id)
    {
        $entity = $this->entityFactory->create();
        $this->resourceModel->load($entity, $id);
        
        if (!$entity->getId()) {
            throw new NoSuchEntityException(__('Entity not found'));
        }
        
        return $entity;
    }
    
    public function save($entity)
    {
        $this->resourceModel->save($entity);
        return $entity;
    }
    
    public function delete($entity)
    {
        $this->resourceModel->delete($entity);
        return true;
    }
}

Why Repository Wraps Resource Model

Repositories are often a wrapper for the Resource Model. The latter has no way to instantiate a model: the repository creates the model (via the model's factory) and then passes it to the Resource Model's load() method.

Repository Interface Requirement

⚠️ Important: If you modify a repository, make sure that the output stays the same. For example, if you want to utilize a different model, make sure this new model implements the appropriate interface (e.g., ProductInterface). This is because many areas of Magento rely on the repository returning the correct interface.

Repositories as API Endpoints

Repositories are commonly an API Endpoint (the service class in a webapi.xml file).

For this situation, you would:

  1. Create an interface that the repository then implements
  2. Declare the route and resources required in webapi.xml

Repository getList() vs Collection

How do they differ?

For a normal model (like a CMS page or an order), there would be no difference.

⚠️ For EAV Entities (products, categories, customers): There is a big difference:
  • Repository getList(): ALL attributes are loaded
  • Collection: Only the attributes you select are loaded
Performance Impact: If you are loading many products, loading all attributes can be a major performance slowdown. With collections, you can select which attributes you want to be loaded.

Why This Option Exists

Originally, a repository was designed for use through an API: APIs are less concerned about speed and more about how much information can be returned.

Best Practice: When iterating through a list of products and you only use a few attributes, use a collection with selected attributes for better performance.

Responsibilities Summary

Models

Responsibility: Store data

  • Once loaded from database (hydrated), ready for use
  • Data stored in $_data property
  • Getters and setters
  • Magic methods available
  • Represents one row

Resource Models

Responsibility: Handle database operations

  • Save, load, delete by default
  • Custom queries for performance
  • Direct database interaction
  • Hydrates models with data
  • Uses fetch methods

Collections

Responsibility: Load multiple rows

  • Load multiple models
  • Iterable (foreach)
  • addFieldToFilter, join, load, getSize
  • Store state (use Factory)
  • For EAV: select specific attributes

Repositories

Responsibility: API wrapper

  • save, getById, getList, delete
  • Wraps Resource Model
  • Do not store state
  • API endpoints
  • For EAV: loads ALL attributes

How They Relate to One Another

  • A collection loads a list of models
  • A resource model directly interacts with the database
  • A resource model handles loading (hydrating) a model class that is already instantiated
  • A model stores data - get() and set() are used frequently
  • A repository creates models and uses resource models to load/save data
  • Collections implement \IteratorAggregate so you can use foreach()

Exam Tips

Key Points to Remember

  • Models: Model/ directory, store data, one row, getters/setters
  • Resource Models: Model/ResourceModel/ directory, database operations, inherit AbstractDb
  • Collections: Model/ResourceModel/[ModelName]/Collection.php, load multiple models, iterable
  • Repositories: Wrapper, save/getById/getList/delete, no state, API endpoints
  • Magic methods: Convert snake_case to camelCase (discount_amount → getDiscountAmount())
  • Explicit methods preferred: Type hints, mockable, custom handling
  • Collections store state: Use CollectionFactory, not direct injection
  • addFieldToFilter: WHERE clause equivalent
  • addAttributeToFilter: Same as addFieldToFilter for EAV entities
  • main_table: Default alias in collections
  • Fetch methods: fetchOne, fetchAll, fetchCol, fetchPairs, fetchRow, fetchAssoc
  • Repository vs Collection (EAV): Repository loads ALL attributes, Collection loads selected
  • Performance: Collections with selected attributes better for EAV
  • IteratorAggregate: Collections can be used in foreach()
  • Repository wraps Resource Model: Creates model via factory, passes to resource model

Further Reading