Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Models, resource models and collections

To work with a table from code, Magento needs three classes. A model represents one record, a resource model runs the database queries, and a collection selects many records at once. There is no command to create them — the files are created by hand.

app/code/Acme/Hello/
└── Model/
    ├── Review.php                      the model
    └── ResourceModel/
        ├── Review.php                  the resource model
        └── Review/
            └── Collection.php          the collection

The resource model

// app/code/Acme/Hello/Model/ResourceModel/Review.php
namespace Acme\Hello\Model\ResourceModel;
 
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
 
class Review extends AbstractDb
{
    protected function _construct(): void
    {
        $this->_init('acme_hello_review', 'review_id');
    }
}

The _init() method names the table and the primary key column. Note that _construct() with a single underscore is a Magento method, not the PHP constructor.

The model

// app/code/Acme/Hello/Model/Review.php
namespace Acme\Hello\Model;
 
use Magento\Framework\Model\AbstractModel;
 
class Review extends AbstractModel
{
    protected function _construct(): void
    {
        $this->_init(ResourceModel\Review::class);
    }
}

The model names its resource model. It keeps column values in an internal array: they are read with getData('author') or the "magic" method getAuthor(), and written with setData('author', 'Anna') or setAuthor('Anna').

The collection

// app/code/Acme/Hello/Model/ResourceModel/Review/Collection.php
namespace Acme\Hello\Model\ResourceModel\Review;
 
use Acme\Hello\Model\ResourceModel\Review as ReviewResource;
use Acme\Hello\Model\Review;
use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;
 
class Collection extends AbstractCollection
{
    protected function _construct(): void
    {
        $this->_init(Review::class, ReviewResource::class);
    }
}

The collection knows which objects to create for table rows and which resource model reads them.

Creating a record

A new model comes from a factory, and the resource model saves it:

// app/code/Acme/Hello/Model/ReviewService.php
namespace Acme\Hello\Model;
 
use Acme\Hello\Model\ResourceModel\Review as ReviewResource;
 
class ReviewService
{
    public function __construct(
        private ReviewFactory $reviewFactory,
        private ReviewResource $reviewResource,
    ) {}
 
    public function add(int $productId, string $author, int $rating): int
    {
        $review = $this->reviewFactory->create();
        $review->setData(['product_id' => $productId, 'author' => $author, 'rating' => $rating]);
 
        $this->reviewResource->save($review);
 
        return (int) $review->getId();
    }
}

The method adds a row to the table and returns its number. A record is loaded by its number with $this->reviewResource->load($review, 5) and deleted with $this->reviewResource->delete($review).

Reading a list

Let's print the latest reviews on the /hello page. A view model receives the collection factory:

// app/code/Acme/Hello/ViewModel/LatestReviews.php
namespace Acme\Hello\ViewModel;
 
use Acme\Hello\Model\ResourceModel\Review\CollectionFactory;
use Magento\Framework\View\Element\Block\ArgumentInterface;
 
class LatestReviews implements ArgumentInterface
{
    public function __construct(private CollectionFactory $collectionFactory) {}
 
    public function getReviews(): array
    {
        $collection = $this->collectionFactory->create();
        $collection->addFieldToFilter('rating', ['gteq' => 4])
            ->setOrder('created_at', 'DESC')
            ->setPageSize(5);
 
        return $collection->getItems();
    }
}

The method returns up to five latest reviews rated four or higher. The database query runs only when getItems() is called.

<!-- app/code/Acme/Hello/view/frontend/templates/reviews.phtml -->
<?php $viewModel = $block->getData('view_model'); ?>
<ul>
    <?php foreach ($viewModel->getReviews() as $review): ?>
        <li><?= $escaper->escapeHtml($review->getAuthor()) ?>: <?= (int) $review->getRating() ?></li>
    <?php endforeach; ?>
</ul>

Add a block with this template and view model to the hello_index_index.xml layout and run bin/magento cache:clean — a list of authors with ratings appears on the page. While the table is empty, the list is empty; the patches lesson adds the first records.

Filter conditions

ConditionMeaning
eq, neqEqual, not equal
gt, gteq, lt, lteqGreater, not less, less, not greater
likePattern match
in, ninIn a list, not in a list
null, notnullEmpty, not empty

Common mistake

A model is created with new Review(), or the Review class itself is requested in a constructor. In the first case the object does not get its dependencies; in the second all classes share one instance with someone else's data. Always take a new record from ReviewFactory and a list from CollectionFactory.

Thanks for reading!

Donate to the site — we work hard for you.