- Introduction to Magento 2 — No More MVC
- Magento 2: Serving Frontend Files
- Magento 2: Adding Frontend Files to your Module
- Magento 2: Code Generation with Pestle
- Magento 2: Adding Frontend Assets via Layout XML
- Magento 2 and RequireJS
- Magento 2 and the Less CSS Preprocessor
- Magento 2: CRUD Models for Database Access
- Magento 2: Understanding Object Repositories
- Magento 2: Understanding Access Control List Rules
- Magento 2: Admin Menu Items
- Magento 2: Advanced Routing
- Magento 2: Admin MVC/MVVM Endpoints
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.
- We’ll use automatic constructor dependency injection to …
- … 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.