Model, Resource Model and Collection

Magento splits one entity across a model, a resource model and a collection; here is what each owns and how they are wired.

Syntax$this->_init(\Vendor\Faq\Model\ResourceModel\Question::class); // inside the model's _construct()

An entity in Magento is one row type in one table: an FAQ question, a CMS block, a cart price rule. Magento splits that entity across three classes, and each has one job.

The three classes

The model holds the data and the behaviour of a single record. It contains no SQL. The resource model is the only class that talks to the database for that entity; it names the table and the primary key. The collection loads many rows in one select and yields models.

ClassExtends_init arguments
Model\QuestionFramework\Model\AbstractModelresource model class
Model\ResourceModel\QuestionFramework\Model\ResourceModel\Db\AbstractDbtable name, id column
Model\ResourceModel\Question\CollectionFramework\Model\ResourceModel\Db\Collection\AbstractCollectionmodel class, resource model class

Each one declares its partner inside _construct(), an initialiser Magento calls at the end of the real constructor. It is not PHP's __construct, which is where injected dependencies arrive.

Getters are data access

A model declares no properties for its columns. $question->getTitle() and $question->getData('title') are the same call: Magento\Framework\DataObject::__call turns the camelCase name into the snake_case data key. Writing the getter out by hand gives you a return type and somewhere to document the value.

Making the files load

New classes under app/code do nothing until the module is installed: run bin/magento setup:upgrade, then bin/magento cache:flush. The QuestionFactory and CollectionFactory classes are generated code; developer mode generates them on first use, production mode needs bin/magento setup:di:compile.

Example

Example · php
<?php
// app/code/Vendor/Faq/Model/Question.php
declare(strict_types=1);

namespace Vendor\Faq\Model;

use Magento\Framework\Model\AbstractModel;
use Vendor\Faq\Model\ResourceModel\Question as QuestionResource;

class Question extends AbstractModel
{
    /**
     * @var string
     */
    protected $_eventPrefix = 'vendor_faq_question';

    protected function _construct()
    {
        $this->_init(QuestionResource::class);
    }

    public function getTitle(): ?string
    {
        // Shorthand and longhand: getTitle() and getData('title') are the
        // same call. __call() maps the camelCase name to the snake_case key.
        $title = $this->getData('title');

        return $title === null ? null : (string)$title;
    }

    public function isAnswered(): bool
    {
        return trim((string)$this->getData('answer')) !== '';
    }
}

// Output: nothing on its own. The model is data plus behaviour; the
// resource model loads and saves it.

When to use it

  • Store a custom entity such as an FAQ question in its own table.
  • Read or write one record without putting SQL in a controller.
  • Load every active record of a custom entity for an admin grid.

More examples

The resource model, which owns the table

AbstractDb::_init($mainTable, $idFieldName) is the whole configuration of a simple resource model, and it is called from _construct(). Magento\Framework\Model\ResourceModel\AbstractResource declares _construct() abstract and calls it from the real constructor, so every resource model must implement it; overriding _init() instead leaves the abstract method unimplemented, which is a fatal error, and the override would never be called. Note that _init takes two arguments here, unlike the model's one-argument version. The table itself is declared in etc/db_schema.xml and created by bin/magento setup:upgrade.

Example · php
<?php
// app/code/Vendor/Faq/Model/ResourceModel/Question.php
declare(strict_types=1);

namespace Vendor\Faq\Model\ResourceModel;

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

class Question extends AbstractDb
{
    protected function _construct()
    {
        $this->_init('vendor_faq_question', 'question_id');
    }
}

// Output: nothing. load(), save() and delete() come from AbstractDb
// once the table and the id column are named.

The collection, which loads many rows

The collection names both other classes: the model it produces for each row, and the resource model that supplies the connection and the table. Reach for it when you need many records at once. Always get one from the generated CollectionFactory, because filters added to a collection accumulate and a reused instance returns the wrong rows.

Example · php
<?php
// app/code/Vendor/Faq/Model/ResourceModel/Question/Collection.php
declare(strict_types=1);

namespace Vendor\Faq\Model\ResourceModel\Question;

use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;
use Vendor\Faq\Model\Question;
use Vendor\Faq\Model\ResourceModel\Question as QuestionResource;

class Collection extends AbstractCollection
{
    /**
     * @var string
     */
    protected $_idFieldName = 'question_id';

    protected function _construct()
    {
        $this->_init(Question::class, QuestionResource::class);
    }
}

// Output: nothing. Injecting the generated CollectionFactory and calling
// create() gives a fresh select over vendor_faq_question.

The table the three classes sit on

Declarative schema: you describe the table you want and Magento works out the difference. The resource model's _init('vendor_faq_question', 'question_id') must match this file exactly. Apply it with bin/magento setup:upgrade, and generate the required whitelist with bin/magento setup:db-declaration:generate-whitelist --module-name=Vendor_Faq. Without the whitelist entry in etc/db_schema_whitelist.json the change is ignored.

Example · xml
<!-- app/code/Vendor/Faq/etc/db_schema.xml -->
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
    <table name="vendor_faq_question" resource="default" engine="innodb" comment="FAQ questions">
        <column xsi:type="int" name="question_id" unsigned="true" nullable="false" identity="true"/>
        <column xsi:type="varchar" name="title" length="255" nullable="false"/>
        <column xsi:type="text" name="answer" nullable="true"/>
        <column xsi:type="smallint" name="is_active" unsigned="true" nullable="false" default="1"/>
        <column xsi:type="int" name="sort_order" unsigned="true" nullable="false" default="0"/>
        <constraint xsi:type="primary" referenceId="PRIMARY">
            <column name="question_id"/>
        </constraint>
        <index referenceId="VENDOR_FAQ_QUESTION_IS_ACTIVE" indexType="btree">
            <column name="is_active"/>
        </index>
    </table>
</schema>

Discussion

  • Be the first to comment on this lesson.