The article is meant for Magento developers who are going to be familiar with Magento 2 and gain professional knowledge. It’s written in a clear and simple manner, but in case you face some difficulties, you’re welcome to comment on.
Creating Collection
Collections are Magento methods to provide the user with a simple interface for group operations on Magento entities. Collection objects allow the user to fetch model’s data from database with desired fields, joins, sorting and grouping. In order to create a simple collections class, we need to extend \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection class and override single-underscore _construct method with our own.
1 2 3 4 |
protected function _construct() { $this->_init(\Magento\Sales\Model\Order::class, \Magento\Sales\Model\ResourceModel\Order::class); } |
We need to pass our Model and Resource model class names to AbstractCollection’s _init method.
Group Operations
Selecting
Collections provide a way to select only required fields from the database or create any join request to fetch related data. Let’s look at addFieldToSelect method from \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection class which can be used to tell collection what field it has to fetch from database.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public function addFieldToSelect($field, $alias = null) { if ($field === '*') { // If we will select all fields $this->_fieldsToSelect = null; $this->_fieldsToSelectChanged = true; return $this; } if (is_array($field)) { if ($this->_fieldsToSelect === null) { $this->_fieldsToSelect = $this->_getInitialFieldsToSelect(); } foreach ($field as $key => $value) { $this->addFieldToSelect($value, is_string($key) ? $key : null); } $this->_fieldsToSelectChanged = true; return $this; } if ($alias === null) { $this->_fieldsToSelect[] = $field; } else { $this->_fieldsToSelect[$alias] = $field; } $this->_fieldsToSelectChanged = true; return $this; } |
If we ignore first two checks from this method, we’ll see that this method sets required field and its alias into class parameter _fieldsToSelect. This parameter is used later in our collection.
First, if block in this method checks for ‘*’ in our requested field and in this case sets _fieldsToSelect flag to null (collection will select all available fields) and the second if block checks if fields argument is an array and in this case recursively calls addFieldToSelect method for every value in passed array.
Parameter _fieldsToSelect and _fieldsToSelectChanged flag (which is set by addFieldToSelect method) are later used in extended getSelect Method. If any field has been changed, then \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection::_initSelectFields is called which passes our required columns to \Magento\Framework\DB\Select class as columns part.
Selecting custom columns
In order to add any custom query to our select query (like SUM or COUNT), we can additionally use \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection::addExpressionFieldToSelect method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public function addExpressionFieldToSelect($alias, $expression, $fields) { // validate alias if (!is_array($fields)) { $fields = [$fields => $fields]; } $fullExpression = $expression; foreach ($fields as $fieldKey => $fieldItem) { $fullExpression = str_replace('{{' . $fieldKey . '}}', $fieldItem, $fullExpression); } $this->getSelect()->columns([$alias => $fullExpression]); return $this; } |
Filtering
In order to filter entities in our collection, we can use \Magento\Framework\Data\Collection\AbstractDb::addFieldToFilter method. It requires field and condition.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
if (is_array($field)) { $conditions = []; foreach ($field as $key => $value) { $conditions[] = $this->_translateCondition($value, isset($condition[$key]) ? $condition[$key] : null); } $resultCondition = '(' . implode(') ' . \Magento\Framework\DB\Select::SQL_OR . ' (', $conditions) . ')'; } else { $resultCondition = $this->_translateCondition($field, $condition); } $this->_select->where($resultCondition, null, Select::TYPE_CONDITION); return $this; |
For every field/condition pair that was passed, this method (if an array was passed) converts our conditions into simple SQL and passes them to \Magento\Framework\DB\Select class as WHERE part.
Sorting
In order to sort our collection, we can use \Magento\Framework\Data\Collection\AbstractDb::setOrder, \Magento\Framework\Data\Collection\AbstractDb::addOrder or \Magento\Framework\Data\Collection\AbstractDb::unshiftOrder methods. All these methods pass our field and sort direction to private \Magento\Framework\Data\Collection\AbstractDb::_setOrder method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
private function _setOrder($field, $direction, $unshift = false) { $this->_isOrdersRendered = false; $field = (string)$this->_getMappedField($field); $direction = strtoupper($direction) == self::SORT_ORDER_ASC ? self::SORT_ORDER_ASC : self::SORT_ORDER_DESC; unset($this->_orders[$field]); // avoid ordering by the same field twice if ($unshift) { $orders = [$field => $direction]; foreach ($this->_orders as $key => $dir) { $orders[$key] = $dir; } $this->_orders = $orders; } else { $this->_orders[$field] = $direction; } return $this; } |
This method sets our desired order into internal parameter _orders which is used later. In case of using unshiftOrder method, additional conditional block is used which sets passed order at first place into _orders array.
Loading
In order to load our collection, we can call \Magento\Framework\Data\Collection\AbstractDb::load method. This method checks if collection has been already loaded and if not, calls \Magento\Framework\Data\Collection\AbstractDb::loadWithFilter method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public function loadWithFilter($printQuery = false, $logQuery = false) { $this->_beforeLoad(); $this->_renderFilters()->_renderOrders()->_renderLimit(); $this->printLogQuery($printQuery, $logQuery); $data = $this->getData(); $this->resetData(); if (is_array($data)) { foreach ($data as $row) { $item = $this->getNewEmptyItem(); if ($this->getIdFieldName()) { $item->setIdFieldName($this->getIdFieldName()); } $item->addData($row); $this->beforeAddLoadedItem($item); $this->addItem($item); } } $this->_setIsLoaded(); $this->_afterLoad(); return $this; } |
First line of this method calls \Magento\Framework\Data\Collection\AbstractDb::_beforeLoad method. We can override this method in our custom collection if it requires some processing before accessing database. Then _render* methods are called. These methods convert our filter/order/etc. parameters from the array to SQL expressions and set them into \Magento\Framework\DB\Select object. By calling \Magento\Framework\Data\Collection\AbstractDb::printLogQuery method, we can print or log our combined SQL request. Actual data loading is done by calling \Magento\Framework\Data\Collection\AbstractDb::getData. For each row that we received from storage, engine collection creates instances of our Resource Model class and stores them into _items parameter after hydrating with data.
Saving
This is a very simple operation from the collection’s perspective.
1 2 3 4 5 6 7 |
public function save() { foreach ($this->getItems() as $item) { $item->save(); } return $this; } |
Save method iterates through every item in collection and calls save method for each of them.
A comprehensive tutorial, thanks for posting it.