Categories


Archives


Recent Posts


Categories


Magento 2: CRUD Models for Database Access

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

Today we’re going to cover creating Magento 2 CRUD models. CRUD stands for Create, Read, Update, and Delete, and commonly refers to framework features used to read and write information to/from the underlying database without directly writing any SQL statements.

Magento 1’s ORM (object relationship mapper) was not abandoned in Magento 2. It’s still an Active Record inspired pattern that uses Model, Resource Model, and Resource Model collection classes. However, due to major changes in Magento’s underlying object system, you’ll need to learn a new set of boilerplate class creation to use Magento’s CRUD features in your own modules.

This article will use the pestle command line framework’s generate_crud_model command to generate this new boiler plate, and then explain what each class in the boilerplate does. We’re going to create a model for a fictitious “To Do List” application.

Creating a Base Module

Before we get to the CRUD specific portion of this article, we’ll need to create a base Magento 2 module to work in.

You can create the base module files using the pestle command line framework’s generate_module command.

$ pestle.phar generate_module Pulsestorm ToDoCrud 0.0.1

Normally, we’d enable this module in Magento by running the following two commands

$ php bin/magento module:enable Pulsestorm_ToDoCrud
$ php bin/magento setup:upgrade    

However, you’ll want to hold off on running setup:upgrade for reasons we’ll mention below.

If you’re interested in creating a module by hand, or curious what the above pestle command is actually doing, take a look at our Introduction to Magento 2 — No More MVC article.

Next, we’re going to create a URL endpoint and view for our model. This is not directly related to the task at hand, but will provide us with a place to write and test PHP code that uses our model.

To create the URL endpoint, run the following pestle commands.

$ pestle.phar generate_route Pulsestorm_ToDoCrud frontend pulsestorm_todocrud

Then, create your view with the generate_view command

$ pestle.phar generate_view Pulsestorm_ToDoCrud frontend pulsestorm_todocrud_index_index Main content.phtml

Normally, you’d clear your Magento cache

php bin/magento cache:clean

and then access your endpoint at the following URL.

http://magento.example.com/pulsestorm_todocrud

However, since we’re holding off on running setup:upgrade, you’ll need to get through the Generating Crud Files section below before accessing your URL.

If you’re curious what the above commands are actually doing, be sure to read the Introduction to Magento 2 — No More MVC article.

Generating Crud Files

As previously mentioned, we’re going to create a model for a “To Do” item in our imaginary productivity application. We’ll want this model to have two main fields — the text of the to do item, and a date completed field.

To generate the base files needed for this module, run the following generate_crud_model command

$ pestle.phar generate_crud_model Pulsestorm_ToDoCrud TodoItem
Creating: /path/to/magento2/app/code/Pulsestorm/ToDoCrud/Model/TodoItemInterface.php
Creating: /path/to/magento2/app/code/Pulsestorm/ToDoCrud/Model/ResourceModel/TodoItem/Collection.php
Creating: /path/to/magento2/app/code/Pulsestorm/ToDoCrud/Model/ResourceModel/TodoItem.php
Creating: /path/to/magento2/app/code/Pulsestorm/ToDoCrud/Model/TodoItem.php
Creating: /path/to/magento2/app/code/Pulsestorm/ToDoCrud/Setup/InstallSchema.php
Creating: /path/to/magento2/app/code/Pulsestorm/ToDoCrud/Setup/InstallData.php

That’s 6 files in total. You’ll notice there’s no configuration files created or edited. Since Magento 2 no longer uses string based class aliases, the base CRUD models need zero configuration. All we need to do is create class files with the correct names.

Once we’ve created the class files, our next step is creating the database table for our model.

To add the database table, we’ll use the Install Schema feature of Magento’s ORM. Pestle has already created a boiler plate install schema class at the following location

#File: app/code/Pulsestorm/ToDoCrud/Setup/InstallSchema.php
public function install(\Magento\Framework\Setup\SchemaSetupInterface $setup, \Magento\Framework\Setup\ModuleContextInterface $context)
{
    //...

    //START table setup
    $table = $installer->getConnection()->newTable(
                $installer->getTable('pulsestorm_todocrud_todoitem')
        )->addColumn(
                'pulsestorm_todocrud_todoitem_id',
                \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
                null,
                array (
      'identity' => true,'nullable' => false,'primary' => true,'unsigned' => true,
    ),
                'Entity ID'
            )->addColumn(
                'title',
                \Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
                255,
                array (
      'nullable' => false,
    ),
                'Demo Title'
            )->addColumn(
                'creation_time',
                \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP,
                null,
                array (
    ),
                'Creation Time'
            )->addColumn(
                'update_time',
                \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP,
                null,
                array (
    ),
                'Modification Time'
            )->addColumn(
                'is_active',
                \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
                null,
                array (
      'nullable' => false,'default' => '1',
    ),
                'Is Active'
            );

    //...
    $installer->getConnection()->createTable($table);        
}

This code creates a PHP data structure that represents a MySQL table. Magento will automatically run this install schema class when we run bin/magento setup:upgrade. You may see this install schema class referred to as a migration, or by its name in Magento 1 — a setup resource class.

Pestle creates a stock database table migration for you. This stock migration will create a table named pulsestorm_todocrud_todoitem, with the following columns

pulsestorm_todocrud_todoitem_id
title
creation_time
update_time
is_active

The first column is the table’s primary key, and will serve as the model’s ID. Pestle bases its name on the name of the table.

The second column, title, is optional, and not used my Magento’s ORM.

The last three columns (creation_time, update_time, and is_active) are fields that Magento expects to find in a model. While not strictly required, having these fields in your models is always a good idea.

Before we run this migration, we’ll want to add our columns. Change the code block above so it looks like the following.

#File: app/code/Pulsestorm/ToDoCrud/Setup/InstallSchema.php    
$table = $installer->getConnection()->newTable(
            $installer->getTable('pulsestorm_todocrud_todoitem')
    )->addColumn(
            'pulsestorm_todocrud_todoitem_id',
            \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
            null,
            array (
  'identity' => true,'nullable' => false,'primary' => true,'unsigned' => true,
),
            'Entity ID'
        )->addColumn(                
            'item_text',
            \Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
            255,
            array (
  'nullable' => false,
),
            'Text of the to do item'

        )->addColumn(                
            'date_completed',
            \Magento\Framework\DB\Ddl\Table::TYPE_DATETIME,
            null,
            array (
  'nullable' => true,
),
            'Date the item was completed'
 )->addColumn(
            'creation_time',
            \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP,
            null,
            array (
),
            'Creation Time'
        )->addColumn(
            'update_time',
            \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP,
            null,
            array (
),
            'Modification Time'
        )->addColumn(
            'is_active',
            \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
            null,
            array (
  'nullable' => false,'default' => '1',
),
            'Is Active'
        );

We’ve removed the title column, and added an item_text column, and a date_completed column. Covering Magento’s data definition language classes in full is beyond the scope of this article, but you can find the base table class (and its TYPE_ constants) here.

#File: vendor/magento//framework/DB/Ddl/Table.php

With the above in place, we’re ready to run our migrations.

Running a Magento 2 Migration

In Magento 1, the core system code automatically ran any needed migrations whenever an uncached HTTP(S) request was made. When it worked, this feature was super useful, as the simple act of adding a module to the system also automatically added its data tables. Unfortunately, when this didn’t work, it could leave the system in a half updated state that was hard to recover from.

In Magento 2, a system owner is required to run the setup:upgrade command when they want to run a migration. Let’s give that a try with the above code in our system. Run the following

php bin/magento setup:upgrade

and you should see the pulsestorm_todocrud_todoitem table created in your database.

mysql> show create table pulsestorm_todocrud_todoitem\G;
*************************** 1. row ***************************
       Table: pulsestorm_todocrud_todoitem
Create Table: CREATE TABLE `pulsestorm_todocrud_todoitem` (
  `pulsestorm_todocrud_todoitem_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'Entity ID',
  `item_text` varchar(255) NOT NULL COMMENT 'Text of the to do item',
  `date_completed` datetime DEFAULT NULL COMMENT 'Date the item was completed',
  `creation_time` timestamp NULL DEFAULT NULL COMMENT 'Creation Time',
  `update_time` timestamp NULL DEFAULT NULL COMMENT 'Modification Time',
  `is_active` smallint(6) NOT NULL DEFAULT '1' COMMENT 'Is Active',
  PRIMARY KEY (`pulsestorm_todocrud_todoitem_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='pulsestorm_todocrud_todoitem'
1 row in set (0.00 sec)    

If your migration did not run, it may be because you ran setup:upgrade before you added the install schema class. If this is the case, you’ll want to remove the information that lets Magento know the Pulsestorm_ToDoCrud module is installed in the system. You can find this information in the setup_module table.

mysql> select * from setup_module where module = 'Pulsestorm_ToDoCrud';
+---------------------+----------------+--------------+
| module              | schema_version | data_version |
+---------------------+----------------+--------------+
| Pulsestorm_ToDoCrud | 0.0.1          | 0.0.1        |
+---------------------+----------------+--------------+
1 row in set (0.00 sec)

If you delete this row

mysql> DELETE from setup_module where module = 'Pulsestorm_ToDoCrud';

and try running setup:upgrade again, Magento should call the install method and install your table.

The InstallSchema.php file is meant to hold code for creating the structure of your database tables. If you wanted to install actual data into the tables you’ve created, you’d use the InstallData.php file

app/code/Pulsestorm/ToDoCrud/Setup/InstallData.php

The InstallData.php file is beyond the scope of this article, but take a look at some core modules to get an idea for how you’d use it

$ find vendor/magento/ -name InstallData.php
vendor/magento//magento2-base/dev/tests/api-functional/_files/Magento/TestModuleIntegrationFromConfig/Setup/InstallData.php
vendor/magento//module-authorization/Setup/InstallData.php
vendor/magento//module-bundle/Setup/InstallData.php
//...
vendor/magento//module-widget-sample-data/Setup/InstallData.php
vendor/magento//module-wishlist-sample-data/Setup/InstallData.php    

A Base Magento 2 CRUD Model

Now that we’ve got a table definition installed, lets take a look at the other four files pestle created for us.

First is the base model file

#File: app/code/Pulsestorm/ToDoCrud/Model/TodoItem.php
<?php
namespace Pulsestorm\ToDoCrud\Model;
class TodoItem extends \Magento\Framework\Model\AbstractModel implements TodoItemInterface, \Magento\Framework\DataObject\IdentityInterface
{
    const CACHE_TAG = 'pulsestorm_todocrud_todoitem';

    protected function _construct()
    {
        $this->_init('Pulsestorm\ToDoCrud\Model\ResourceModel\TodoItem');
    }

    public function getIdentities()
    {
        return [self::CACHE_TAG . '_' . $this->getId()];
    }
}

This is our main model class file. Objects instantiated with this class are the bread and butter for Magento 2 CRUD programming. Like Magento 1, all Magento CRUD models extend the base abstract model class.

Magento\Framework\Model\AbstractModel

Unlike Magento 1, all CRUD models also implement an IdentityInterface. This interface forces model developers to define a getIdentities method

<?php
#File: vendor/magento/framework/DataObject/IdentityInterface.php
namespace Magento\Framework\DataObject;
interface IdentityInterface
{
    public function getIdentities();
}

You’ll also notice the model implements a Pulsestorm/ToDoCrud/Model/TodoItemInterface class.

#File: app/code/Pulsestorm/ToDoCrud/Model/TodoItemInterface.php
<?php
namespace Pulsestorm\ToDoCrud\Model;
interface TodoItemInterface 
{

}    

While not strictly necessary, the model specific TodoItemInterface interface plays an important role when it comes time to exporting CRUD models to Magento’s new service contracts based API. While beyond the scope of this article, the model specific interfaces for Magento CRUD models will determine which class methods are available via the Magento API.

The last thing to make note of is the _construct method

#File: app/code/Pulsestorm/ToDoCrud/Model/TodoItem.php
protected function _construct()
{
    $this->_init('Pulsestorm\ToDoCrud\Model\ResourceModel\TodoItem');
}

A model’s _construct method is a leftover concept from Magento 1. It’s an alternative constructor. The implementation of this _construct method is beyond the scope of this article. All you need to know is that _construct will be called whenever a model is instantiated. Every CRUD model in Magento must use the _construct method to call the _init method. The _init method accepts a single string parameter — the name of this model’s resource model.

A Magento 2 Resource Model

In Magento 2, the model class defines the methods an end-user-programmer will use to interact with a model’s data. A resource model class contains the methods that will actually fetch the information from the database. Each CRUD model in Magento 2 has a corresponding resource model class.

#File: app/code/Pulsestorm/ToDoCrud/Model/ResourceModel/TodoItem.php
<?php
namespace Pulsestorm\ToDoCrud\Model\ResourceModel;
class TodoItem extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
{
    protected function _construct()
    {
        $this->_init('pulsestorm_todocrud_todoitem','pulsestorm_todocrud_todoitem_id');
    }
}

Every CRUD resource model class extends the Magento\Framework\Model\ResourceModel\Db\AbstractDb class. This base class contains the basic logic for fetching information from a single database table.

For a basic model like ours, the only thing a resource model must do is call the _init method from _construct. The _init method for a resource model accepts two arguments. The first is the name of the database table (pulsestorm_todocrud_todoitem), and the second is the ID column for the model (pulsestorm_todocrud_todoitem_id).

While it’s beyond the scope of this article, Magento 2’s active record implementation contains no method for linking tables via primary keys. How to use multiple database tables is up to each individual module developer, and a resource model will typically contain the SQL generating methods needed to fetch information from related tables.

A Magento 2 Collection Model

With a model and resource model, you have everything you need to fetch and save individual models into the database. However, there are times where you’ll want to fetch multiple models of a particular type. To solve this problem, every CRUD model in Magento 2 has a corresponding resource model collection. A collection collects individual models. It’s considered a resource model since it builds the SQL code necessary to pull information from a database table.

#File: app/code/Pulsestorm/ToDoCrud/Model/ResourceModel/TodoItem/Collection.php
<?php
namespace Pulsestorm\ToDoCrud\Model\ResourceModel\TodoItem;
class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection
{
    protected function _construct()
    {
        $this->_init('Pulsestorm\ToDoCrud\Model\TodoItem','Pulsestorm\ToDoCrud\Model\ResourceModel\TodoItem');
    }
}

All collections in Magento 2 extend the base \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection collection class. Like a model and resource model, a collection resource model must call the _init method. A collection resource model’s _init method accepts two arguments. The first is the model that this collection collects. The second is that collected model’s resource model.

Using a Crud Model

Now that we’ve explored what each of the 6 files created by pestle’s generate_crud_model do, we’re ready to get into using the CRUD models.

First, let’s make sure the view we setup earlier is working. Open the following file and add the following var_dump to the _prepareLayout method.

#File: app/code/Pulsestorm/ToDoCrud/Block/Main.php

<?php
namespace Pulsestorm\ToDoCrud\Block;
class Main extends \Magento\Framework\View\Element\Template
{
    function _prepareLayout()
    {
        var_dump("I am Here");
        exit;
    }
}

We’re going to write our code in the _prepareLayout method, and then halt execution. While this is something you’d never do in a finished module, working out code samples in this sort of environment is common practice when learning and developing If you load the endpoint we created earlier

http://magento.example.com/pulsestorm_todocrud

and you should see your I am Here text on a white screen.

The first thing we’re going to do is instantiate a new To Do Item model, set its text, and then save it. In Magento 1, we’d use code that looked something like this

function _prepareLayout()
{
    $model = Mage::getModel('pulsestorm_todocrud/todoitem')
    ->setItemText('Finish my Magento Article')
    ->save();
}

However, Magento 2 no longer uses static factory methods on a global Mage class. Instead, we need to use two new Magento OO systems.

  1. We’ll use automatic constructor dependency injection to …
  2. … inject a factory object, and then use the factory object to instantiate our CRUD model

If you’re not familiar with automatic constructor dependency injection, you’ll want to work your way through our Magento 2 object manager series. If you’re not familiar with Factory objects — that’s because we haven’t covered them yet!

Magento 2 Factory Objects

In object oriented programming, a factory method is a method that’s used to instantiate an object. Factory methods exist to ensure system developers have control over how a particular object is instantiated, and how its arguments are passed in. There’s a certain school of though that thinks direct use of the new keyword in programming

$object = new Foo;

is an anti-pattern, as directly instantiating an object creates a hard coded dependency in a method. Factory methods give the system owner the ability to control which objects are actually returned in a given context.

A factory object serves a similar purpose. In Magento 2, each CRUD model has a corresponding factory class. All factory class names are the name of the model class, appended with the word “Factory”. So, since our model class is named

Pulsestorm/ToDoCrud/Model/TodoItem

this means our factory class is named

Pulsestorm/ToDoCrud/Model/TodoItemFactory

To get an instance of the factory class, replace your block class with the following.

#File: app/code/Pulsestorm/ToDoCrud/Block/Main.php    
<?php
namespace Pulsestorm\ToDoCrud\Block;
class Main extends \Magento\Framework\View\Element\Template
{
    protected $toDoFactory;
    public function __construct(
        \Magento\Framework\View\Element\Template\Context $context,
        \Pulsestorm\ToDoCrud\Model\TodoItemFactory $toDoFactory
    )
    {
        $this->toDoFactory = $toDoFactory;
        parent::__construct($context);
    }

    function _prepareLayout()
    {
        var_dump(
            get_class($this->toDoFactory)
        );
        exit;
    }
}

What we’ve done here is use automatic constructor dependency injection to inject a Pulsestorm\ToDoCrud\Model\TodoItemFactory factory object, and assign it to the toDoFactory object property in the constructor method.

#File: app/code/Pulsestorm/ToDoCrud/Block/Main.php        
protected $toDoFactory;
public function __construct(
    \Magento\Framework\View\Element\Template\Context $context,
    \Pulsestorm\ToDoCrud\Model\TodoItemFactory $toDoFactory
)
{
    $this->toDoFactory = $toDoFactory;
    parent::__construct($context);
}

We also had to inject a block context object and pass that to our parent constructor. We’ll cover these context object in future articles, but if you’re curious about learning more, this quickies post is a good place to start.

In addition to injecting a factory into our block, we also added the following to our _prepareLayout method

#File: app/code/Pulsestorm/ToDoCrud/Block/Main.php    
function _prepareLayout()
{
    var_dump(
        get_class($this->toDoFactory)
    );
    exit;
}

This will dump the toDoFactory‘s class name to the screen, and is a quick sanity check that our automatic constructor dependency injection worked. Reload your page with the above in place, and you should see the following

string 'Pulsestorm\ToDoCrud\Model\TodoItemFactory' (length=41)

Next, replace your _prepareLayout method with this code

#File: app/code/Pulsestorm/ToDoCrud/Block/Main.php    

function _prepareLayout()
{
    $todo = $this->toDoFactory->create();
    $todo->setData('item_text','Finish my Magento article')
    ->save();
    var_dump('Done');
    exit;
}

This code calls the create method of our factory. This will instantiate a \Pulsestorm\ToDoCrud\Model\TodoItemFactory object for us. Then, we set the item_text property of our model, and call its save method. Reload your page to run the above code, and then check your database table

mysql> select * from pulsestorm_todocrud_todoitem\G
*************************** 1. row ***************************
pulsestorm_todocrud_todoitem_id: 1
                      item_text: Finish my Magento article
                 date_completed: NULL
                  creation_time: NULL
                    update_time: NULL
                      is_active: 1
1 row in set (0.00 sec)

You’ll find that Magento has saved the information you requested. If you wanted to fetch this specific model again, you’d use code that looked like the following.

#File: app/code/Pulsestorm/ToDoCrud/Block/Main.php        
function _prepareLayout()
{
    $todo = $this->toDoFactory->create();

    $todo = $todo->load(1);        
    var_dump($todo->getData());
    exit;
}

Here we’ve used the factory to create our model, used the model’s load method to load a model with the ID of 1, and then dumped the model’s data using the various magic setter and getter methods available to a Magento 2 model.

#File: app/code/Pulsestorm/ToDoCrud/Block/Main.php        
function _prepareLayout()
{
    $todo = $this->toDoFactory->create();

    $todo = $todo->load(1);        

    var_dump($todo->getData());

    var_dump($todo->getItemText());

    var_dump($todo->getData('item_text'));
    exit;
}

Finally, if we wanted to use a CRUD model’s collection object, we’d use code that looked like this

#File: app/code/Pulsestorm/ToDoCrud/Block/Main.php          
function _prepareLayout()
{
    $todo = $this->toDoFactory->create();

    $collection = $todo->getCollection();

    foreach($collection as $item)
    {
        var_dump('Item ID: ' . $item->getId());
        var_dump($item->getData());
    }
    exit;
}   

Again, this code uses a factory object to create a CRUD model object. Then, we use the CRUD model object’s getCollection method to fetch the model’s collection. Then, we iterate over the items returned by the collection.

Once instantiated via a factory, Magento 2’s CRUD models behave very similarly, if not identically, to their Magento 1 counterparts. If you’re curious about Magento 1’s CRUD objects, our venerable Magento 1 for PHP MVC Developers article may be of interest, as well as the Varien Data Collections article.

Where did the Factory Come From

You may be thinking to yourself — how did Magento instantiate a Pulsestorm/ToDoCrud/Model/TodoItemFactory class if I never defined one? Factory classes are another instance of Magento 2 using code generation (first covered in our Proxy object article). Whenever Magento’s object manager encounters a class name that ends in the word Factory, it will automatically generate the class in the var/generation folder if the class does not already exist. You can see your generated factory class at the following location

#File: var/generation/Pulsestorm/ToDoCrud/Model/TodoItemFactory.php 
<?php
namespace Pulsestorm\ToDoCrud\Model;

/**
 * Factory class for @see \Pulsestorm\ToDoCrud\Model\TodoItem
 */
class TodoItemFactory
{
    //...
}

Wrap Up

That’s the basics of Magento’s CRUD models. While much of the talk from the Magento 2 core team has been around Repositories, Service Contracts, and the various API transport layers these features enable, Magento’s abstract CRUD models remain the bread and butter objects for Magento 2 programming. The amount of legacy code relying on these models, and the practicality of the ActiveRecord pattern ensure that these models will remain a vital part of any Magento 2 developer’s toolkit.

Originally published March 2, 2016
Series Navigation<< Magento 2 and the Less CSS PreprocessorMagento 2: Understanding Object Repositories >>

Copyright © Alan Storm 1975 – 2017 All Rights Reserved

Originally Posted: 2nd March 2016