Magento 2 – Development environment
I just made available on github a project that allows you to install a local development environment with a working version of magento within it.
Here is the url of its repo : https://github.com/berliozd/magento-env
Description
After cloning the project, you will have to execute two commands.
The first one should be executed from your local machine and will allow you to build the necessary docker containers which are described in the docker-compose.yml file.
6 docker containers will be needed:
- php
- nginx
- mysql
- redis
- rabbitmq
- elasticsearch
The second command must be executed from the php container.
It will create the magento project using composer. The latest version of magento (aka Adobe commerce) will be installed. At this time the latest version is 2.4.
As the magento source code is downloaded from the repository https://repo.magento.com/, you will be asked for access keys. To obtain these keys, you must have a magento developper account and access the magento marketplace at this page https://marketplace.magento.com/customer/accessKeys/.
Once the magento project is created, the script will continue with the magento installation itself.
Once this is finished, you will be able to access your new magento instance.
Step by step guide
- clone the git projet
git clone git@github.com:berliozd/magento-env.git .
- run pre.sh from your local machine et the root of the git project
docker/pre.sh
- run bash inside the PHP container
docker-compose -f docker-compose.mac.yml run php bash
- run init.sh from the PHP container
docker/php/init.sh
That's all.
- The frontend will be accessible on : http://dev.addeos.
- The backoffice will be accessible on http://dev/addeos/admin. (user: admin, password: admin@addeos1)
You can now use that development environment to test some tutorial I wrote : https://www.addeos.com/magento-2-tutorial-adding-a-new-model-part-1-creating-the-db-table
Conclusion
It is with this solution that I am currently working on my Magento projects.
It allows me to quickly install a new instance of Magento.
I would be interested to know what solutions are used by magento developers around the world. Feel free to share in the comments how you work and how you configure your work environment.
Magento 2 – Tutorial : new model – Part 2 – creating the PHP classes
Introduction
This article is the 2nd part of a series in which I go through the procedure to create a new model in magento 2, with its own magento 2 admin grid in the Back Office.
You can go back to the first part if you wish.
As a preamble, I have to say that this tutorial series has been developed and tested with Magento version 2.4.2.
I remind you that for the purpose of the article I am going to create a "provider" model and that our code will be contained in an "Addeos_ModelTutorial" module.
In the first part I have explained how to create the DB table using declarative XML. In this second part, I will create the PHP classes :
- the API data interface
- the model that implements the interface
- the resource model
- the collection resource model
Creating the API data interface
If we want to respect the concept of service contracts which is a good thing and recommanded by Magento, I need to create some PHP interfaces first. Here is some more details about service contracts.
By convention, these PHP interfaces have to be in an Api folder and a Data folder for data interfaces.
The first interface to create is the ProviderInterface : Api/Data/ProviderInterface.php
.
Here is the code :
<?php declare(strict_types=1); namespace Addeos\ModelTutorial\Api\Data; /** * @api */ interface ProviderInterface { const ENTITY_ID = 'entity_id'; const CREATED_AT = 'created_at'; const UPDATED_AT = 'updated_at'; const NAME = 'name'; const DESCRIPTION = 'description'; public function getEntityId(): ?int; public function setEntityId(int $entityId); public function getName(): ?string; public function setName(string $name); public function getDescription(): ?string; public function setDescription(string $description); public function getCreatedAt(): ?string; public function setCreatedAt(string $createdAt); public function getUpdatedAt(): ?string; public function setUpdatedAt(string $updatedAt); }
Creating the PHP model
I then need to create the PHP model that will implement the interface we have just created. This PHP class will extend \Magento\Framework\Model\AbstractModel
.
Here is the code :
<?php declare(strict_types=1); namespace Addeos\ModelTutorial\Model; use Addeos\ModelTutorial\Api\Data\ProviderInterface; use Magento\Framework\Model\AbstractModel; class Provider extends AbstractModel implements ProviderInterface { public function getName(): ?string { return $this->getData(self::NAME); } public function setName(string $name) { $this->setData(self::NAME, $name); } public function getDescription(): ?string { return $this->getData(self::DESCRIPTION); } public function setDescription(string $description) { $this->setData(self::DESCRIPTION, $description); } public function getCreatedAt(): ?string { return $this->getData(self::CREATED_AT); } public function setCreatedAt(string $createdAt) { $this->setData(self::CREATED_AT, $createdAt); } public function getUpdatedAt(): ?string { return $this->getData(self::CREATED_AT); } public function setUpdatedAt(string $updatedAt) { $this->setData(self::UPDATED_AT, $updatedAt); } }
Creating the resource model
I now need to create resource model.
The file is Model/ResourceModel/Provider.php
.
Here is the code :
<?php declare(strict_types=1); namespace Addeos\ModelTutorial\Model\ResourceModel; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; class Provider extends AbstractDb { protected function _construct() { $this->_init('addeos_provider', 'entity_id'); } }
Creating the collection resource model
I now need to create the provider collection resource model.
The file is ModelResourceModel/Provider/Collection.php
.
Her is the code :
<?php namespace Addeos\ModelTutorial\Model\ResourceModel\Provider; use Addeos\ModelTutorial\Model\Provider; use Addeos\ModelTutorial\Model\ResourceModel\Provider as ProviderResourceModel; use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; class Collection extends AbstractCollection { protected $_idFieldName = 'entity_id'; protected $_eventPrefix = 'addeos_provider_collection'; protected $_eventObject = 'addeos_provider_collection'; protected function _construct() { $this->_init(Provider::class, ProviderResourceModel::class); } }
And that's all. From that point we have all the necessary classes for now.
In next post, I will explain now how to create the grid in Back Office to visualize the data of our new table.
Magento 2 – Tutorial : adding a new model – Part 1 – creating the DB table
Introduction
Here is the first post of a series during which I will got through all the procedure to create a new model in magento 2, with its own grid in the Back Office.
Here are the steps we will have to through :
- Creating the DB table
- Creating the API data object
- Creating the model
- Creating the resource model
- Creating the collection resource model
- Creating the grid
I will cover all these steps in a series of posts.
As a preamble, I have to say that this tutorial series has been developed and tested with Magento version 2.4.2.
For the purpose of the series, we will create a simple model for a "provider" entity with the following columns :
- id : the provider unique identifier
- created_at : the date the provider was created
- updated_at : the date the provider was updated
- name : the provider name
- description : the provider description
We will assume that our code will be held in a module Addeos_ModelTutorial.
Creating the table in DB
Rather than using InstallSchema.php file we can now use XML declarative schema definition for creating new tables, or even updating existing tables. This is available since Magento version 2.3.
We will create a db_schema.xml
file.
He is the content of the file:
<?xml version="1.0"?> <schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="addeos_provider" resource="default" engine="innodb" comment="Porviders"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Entity ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> <column xsi:type="varchar" name="name" nullable="true" comment="Name" length="50"/> <column xsi:type="varchar" name="description" nullable="true" comment="Description" length="50"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> <index referenceId="ADDEOS_PROVIDER_ENTITY_ID" indexType="btree"> <column name="entity_id"/> </index> </table> </schema>
More information on declarative schema can be found here.
Once this file is in place, you will simply need to run a setup upgrade command from your magento installation root directory :
bin/magento setup:upgrade --keep-generated
Once this is done, your new table will be present in DB.
And this is all for that first step.
In the next post I explain the different PHP classes we need to create.
Magento 2 – Quick tip : how to quickly execute any magento 2 code in command line
The solution I am about to present allows you to execute any magento 2 code from the command line.
While developing on magento 2, the code you are working on can be difficult to access and test because it sometimes needs to be executed in a specific context or after having clicked several times to access a specific page. That's why you often need to test some code without being obliged to go all the way.
I simply use a simple PHP script that can be executed in command line.
Here is a the empty structure of this script :
<?php require '/var/www/html/app/bootstrap.php'; use Magento\Framework\App\Bootstrap; class tmp { public function __construct() { $params = $_SERVER; $bootstrap = Bootstrap::create(BP, $params); $obj = $bootstrap->getObjectManager(); } public function execute() { } } $script = new tmp(); $script->execute();
I store this file in my magento folder in a __utils folder, name it tmp.ph for example and execute it like that :

I won't go into details but I run the previous command in the PHP docker container where magento runs. Prior to running the commande, I have to cd in the magento root.
This can be very helpful because it allows you to execute some code out of any context and really isolate a specific process and identify why potential bugs occurs, but also it can handy if you want to :
- test and debug patch code before writing a proper patch
- execute SQL statement
- verify collections contents
- automatise test data creation (product, sales rules or any other entities)
- check some third party API calls
Here is a full example.
<?php require '/var/www/html/app/bootstrap.php'; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Customer\Model\ResourceModel\CustomerRepository; use Magento\Framework\App\Bootstrap; use Magento\Framework\App\State; use Magento\Framework\DataObject; use Magento\Framework\ObjectManagerInterface; use Magento\Quote\Api\CartManagementInterface; use Magento\Quote\Model\Quote; use Magento\Quote\Model\ResourceModel\Quote\Payment; use Magento\QuoteGraphQl\Model\Cart\AssignBillingAddressToCart; use Magento\QuoteGraphQl\Model\Cart\AssignShippingAddressToCart; use Magento\QuoteGraphQl\Model\Cart\AssignShippingMethodToCart; use Magento\QuoteGraphQl\Model\Cart\CreateEmptyCartForCustomer; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; use Magento\QuoteGraphQl\Model\Cart\QuoteAddressFactory; use Magento\Store\Model\App\Emulation; use Magento\Quote\Model\QuoteManagement; class createOrder { private State $state; private ObjectManagerInterface $objectManager; private CustomerRepository $customerRepository; private CreateEmptyCartForCustomer $createEmptyCartforCustomer; private GetCartForUser $getCartForUser; private Emulation $emulation; private ProductRepositoryInterface $productRepository; private QuoteManagement $quoteManagement; private QuoteAddressFactory $quoteAddressFactory; private AssignShippingAddressToCart $assignShippingAddressToCart; private AssignBillingAddressToCart $assignBillingAddressToCart; private AssignShippingMethodToCart $assignShippingMethodToCart; private Payment $resourcePayment; private \Magento\Quote\Model\ResourceModel\Quote $quoteResourceModel; private CartManagementInterface $cartManagement; public function __construct() { $params = $_SERVER; $bootstrap = Bootstrap::create(BP, $params); $obj = $bootstrap->getObjectManager(); $this->state = $obj->get(State::class); $this->state->setAreaCode('frontend'); $obj = $bootstrap->getObjectManager(); $this->objectManager = $obj; $this->customerRepository = $obj->get(CustomerRepository::class); $this->createEmptyCartforCustomer = $obj->get(CreateEmptyCartForCustomer::class); $this->getCartForUser = $obj->get(GetCartForUser::class); $this->emulation = $obj->get(Emulation::class); $this->emulation->startEnvironmentEmulation(3); $this->productRepository = $obj->get(ProductRepositoryInterface::class); $this->quoteManagement = $obj->get(QuoteManagement::class); $this->quoteAddressFactory = $obj->get(QuoteAddressFactory::class); $this->assignBillingAddressToCart = $obj->get(AssignBillingAddressToCart::class); $this->assignShippingAddressToCart = $obj->get(AssignShippingAddressToCart::class); $this->assignShippingMethodToCart = $obj->get(AssignShippingMethodToCart::class); $this->resourcePayment = $obj->get(Payment::class); $this->quoteResourceModel = $obj->get(\Magento\Quote\Model\ResourceModel\Quote::class); $this->cartManagement = $obj->get(CartManagementInterface::class); } public function execute() { $customer = $this->customerRepository->get('customer@example.com', 3); // Create empty cart $maskedCartId = $this->createEmptyCartforCustomer->execute($customer->getId()); // Get cart /** @var Quote $cart */ $cart = $this->getCartForUser->execute($maskedCartId, $customer->getId(), 3); // Add bundle to cart $bundleRequest = ['bundle_option' => [578 => 1122]]; $bundle = $this->productRepository->get('bundle_sku'); $cart->addProduct($bundle, new DataObject($bundleRequest)); $this->quoteResourceModel->save($cart); $quote = $this->cartManagement->getCartForCustomer($customer->getId()); $addressInputBilling = [ 'country_code' => "FR", 'street' => ["55 billing street"], 'telephone' => "0123456789", 'postcode' => "75000", 'city' => "Paris", 'firstname' => "Customer", 'lastname' => "Test", 'save_in_address_book' => false ]; $billingAddress = $this->quoteAddressFactory->createBasedOnInputData($addressInputBilling); $this->assignBillingAddressToCart->execute($quote, $billingAddress, false); // Set shipping address $addressInputShipping = [ 'country_code' => "FR", 'street' => ["55 shipping street"], 'telephone' => "01223456789", 'postcode' => "75001", 'city' => "PAris", 'firstname' => "Customer", 'lastname' => "Test", 'save_in_address_book' => false ]; $shippingAddress = $this->quoteAddressFactory->createBasedOnInputData($addressInputShipping); $this->assignShippingAddressToCart->execute($quote, $shippingAddress); // Set shipping method $this->assignShippingMethodToCart->execute($quote, $shippingAddress, 'my_carrier_code', 'method_code'); $payment = $quote->getPayment(); $payment->setMethod('free'); $this->resourcePayment->save($payment); $order = $this->quoteManagement->submit($quote); } } $script = new createOrder(); $script->execute();
In that example, we can see that we use the object manager in the constructor for creating several instances of specific repositories, or services that we later use in the execute method.
We simply programmatically create an order in that example using all the different services that it requires.
I recall that this script is only for testing and debugging your code. Directly using the object manager is not a good practice and we are using it here because dependency injection cannot be used.
That's all.
Magento 2 – Quick tip : how to log and debug easily
Whatever the development you are doing, and whatever the environment and technology you are working on, you always need to debug your code.
This is how i have been processing for many times in a magento 2 environment and coding with PhpStorm IDE.
First, i have added 2 simple live templates inside my PhpStorm IDE.
The first one will allow me to temporarily and quickly add a logging function inside my code :

Here is the live template code if you need to copy paste it.
<?php private function log($str) { $str = 'CLASS : ' . str_pad(__CLASS__, 50, ' ') . ' - LINE : ' . debug_backtrace()[0]['line'] . ' - FUNCTION : ' . str_pad(debug_backtrace()[1]['function'], 15, ' ') . ' - STR : ' . $str; $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); /** @var \Magento\Framework\Filesystem\DirectoryList $directory */ $directory = $objectManager->get(\Magento\Framework\Filesystem\DirectoryList::class); $rootPath = $directory->getPath(\Magento\Framework\App\Filesystem\DirectoryList::VAR_DIR); $logger = new \Zend\Log\Logger(); $writer = new \Zend\Log\Writer\Stream($rootPath . '/log/exception.log'); $logger->addWriter($writer); $logger->debug($str); }
This logging function is not perfect but it allows you to log any info just like we could do with magento 1 and famous Mage::Log
function. It also gives a little of contextual information, the PHP class, the PHP method and the line number where the log have been added.
Here is the second live template i have added :

It allows to quickly add a log in the code.
With these 2 live template i can easily and quickly temporarily add log in my code.
Here is how you add the logging function in your code :

And here is how you call it from anywhere in your code :

After that you will just need to open you command line and start a tail -f var/log/exception.log
command and you will see the logs coming.
And that's all. Please remember that this is very temporary and must not live inside production code (directly using object manager is not a good practice) so don't forget to remove this function and calls before commiting you code.
Magento 2 – Quick tip : Activate support in PhpStorm
You can enable Magento 2 support in PhpStorm.
It will bring great features in the IDE that will help in your coding.
Here is in the IDE settings how to enable it.
Go in section Languages & Frameworks > PHP > Frameworks
And check "Enable Magento Integration"
You will also need to set the magento installation path.

Apply the setting changes by clicking on "Ok" or "Apply" button and close the settings window.
You will then need to wait for the code project to be reindexed.
Once done, you will be able to see several nice features in the editor.
- Navigation between schema.graphqls files and PHP resolver file
Here you can see the little annotation C.

By clicking on the C you will be redirect to the resolver class.

In PHP classes, you can see several annotation icons.

- Navigation between configuration files and PHP files

This annotation will bring you to some configuration files (di.xml).
Magento 2 : Totals collection explained
What is it?
Quote Totals collection is an important process in magento.
This process also exists on Invoice
and Creditmemo
objects but we will only look into detail how it is implemented for the Quote
object which is the one you will be more frequently interacting with. This process is very frequent in magento. Basically, it is executed every time a change is done on the cart. It can be:
- in
Quote
ModelafterLoad
event - at order placing
\Magento\InstantPurchase\Model\PlaceOrder::placeOrder
- when saving a quote item
\Magento\Quote\Model\Quote\Item\Repository::save
- and many more places...
What is it for?
The objective of that process is (as it is well named) to collect the totals for the Quote
object.
The Quote
object is a complex and holds many prices information (grand_total
, subtotal
, subtotal_with_discount
, etc.).
The value of these different prices are calculated using specific rules and are impacted differently.
This is what the total collection does.
How is it implemented?
The Quote
instance is the main actor but he is not alone. There is also a TotalsCollector
object and many TotalCollector
objects that play their role.
Let's see what object owns what and what they are responsible for.
- The
Quote
object has aTotalsCollector
. - The
TotalsCollector
object has a list ofTotalCollector
.
The TotalsCollector
is responsible for :
- looping on addresses (in many cases, 2 addresses, one billing and one shipping), see
\Magento\Quote\Model\Quote\TotalsCollector::collect
. - executing a list of total collectors for each address, see
\Magento\Quote\Model\Quote\TotalsCollector::collectAddressTotals
.
Let's go a little more in detail.
The TotalsCollector
initiates a global Total
object (\Magento\Quote\Model\Quote\Address\Total
).
/** @var \Magento\Quote\Model\Quote\Address\Total $total */
$total = $this->totalFactory->create(\Magento\Quote\Model\Quote\Address\Total::class);
For each address, the TotalsCollector
will get a new Total
object that will come to "enrich" the global Total
object.
This is done by calling the collect
method of each TotalCollector
, the resulting Total
contains the data for the current address.
These are the data :
shipping_amount
base_shipping_amount
shipping_description
subtotal
base_subtotal
subtotal_with_discount
base_subtotal_with_discount
grand_total
base_grand_total
Depending on the type of each data, it can be used to either increment the same data on the main Total
object. This is the case for subtotal
for example :
$total->setSubtotal((float)$total->getSubtotal() + $addressTotal->getSubtotal());
Or to either simply replace the same data in the main Total
object. This is the case for shipping_amount
for example :
$total->setShippingAmount($addressTotal->getShippingAmount());
There is the list of TotalCollector
object that the TotalsCollector
natively holds :
Magento\Quote\Model\Quote\Address\Total\Subtotal
Magento\Tax\Model\Sales\Total\Quote\Subtotal
Magento\Weee\Model\Total\Quote\Weee
Magento\SalesRule\Model\Quote\Discount
Magento\Quote\Model\Quote\Address\Total\Shipping
Magento\Tax\Model\Sales\Total\Quote\Shipping
Magento\SalesRule\Model\Quote\Address\Total\ShippingDiscount
Magento\Tax\Model\Sales\Total\Quote\Tax
Magento\Weee\Model\Total\Quote\WeeeTax
Magento\Quote\Model\Quote\Address\Total\Grand
How is it configured?
The list of TotalCollector
is loaded from xml config.
See \Magento\Quote\Model\Quote\Address\Total\Collector::__construct
and \Magento\Sales\Model\Config\Ordered::_initCollectors
.
Evertything happens in sales.xml
file.
You will find a node element like this one : <section name="quote">
In Magento_Quote module :
<section name="quote">
<group name="totals">
<item name="subtotal" instance="Magento\Quote\Model\Quote\Address\Total\Subtotal" sort_order="100"/>
<item name="shipping" instance="Magento\Quote\Model\Quote\Address\Total\Shipping" sort_order="350"/>
<item name="grand_total" instance="Magento\Quote\Model\Quote\Address\Total\Grand" sort_order="550"/>
</group>
</section>
And then in Magento_SalesRule module :
<section name="quote">
<group name="totals">
<item name="discount" instance="Magento\SalesRule\Model\Quote\Discount" sort_order="300"/>
<item name="shipping_discount" instance="Magento\SalesRule\Model\Quote\Address\Total\ShippingDiscount" sort_order="400"/>
</group>
</section>
And then in Magento_Tax module :
<section name="quote">
<group name="totals">
<item name="tax_subtotal" instance="Magento\Tax\Model\Sales\Total\Quote\Subtotal" sort_order="200"/>
<item name="tax_shipping" instance="Magento\Tax\Model\Sales\Total\Quote\Shipping" sort_order="375"/>
<item name="tax" instance="Magento\Tax\Model\Sales\Total\Quote\Tax" sort_order="450"/>
</group>
</section>
And then in Magento_Weee module :
<section name="quote">
<group name="totals">
<item name="weee" instance="Magento\Weee\Model\Total\Quote\Weee" sort_order="225"/>
<item name="weee_tax" instance="Magento\Weee\Model\Total\Quote\WeeeTax" sort_order="460"/>
</group>
</section>
If I cumulate these configurations, we will have that :
<section name="quote">
<group name="totals">
<item name="subtotal" instance="Magento\Quote\Model\Quote\Address\Total\Subtotal" sort_order="100"/>
<item name="tax_subtotal" instance="Magento\Tax\Model\Sales\Total\Quote\Subtotal" sort_order="200"/>
<item name="weee" instance="Magento\Weee\Model\Total\Quote\Weee" sort_order="225"/>
<item name="discount" instance="Magento\SalesRule\Model\Quote\Discount" sort_order="300"/>
<item name="shipping" instance="Magento\Quote\Model\Quote\Address\Total\Shipping" sort_order="350"/>
<item name="tax_shipping" instance="Magento\Tax\Model\Sales\Total\Quote\Shipping" sort_order="375"/>
<item name="shipping_discount" instance="Magento\SalesRule\Model\Quote\Address\Total\ShippingDiscount" sort_order="400"/>
<item name="tax" instance="Magento\Tax\Model\Sales\Total\Quote\Tax" sort_order="450"/>
<item name="weee_tax" instance="Magento\Weee\Model\Total\Quote\WeeeTax" sort_order="460"/>
<item name="grand_total" instance="Magento\Quote\Model\Quote\Address\Total\Grand" sort_order="550"/>
</group>
</section>
For order_invoice and order_creditmemo, we will find the same types of xml declarations.
For order_invoice :
<section name="order_invoice">
<group name="totals">
<item name="subtotal" instance="Magento\Sales\Model\Order\Invoice\Total\Subtotal" sort_order="50"/>
<item name="discount" instance="Magento\Sales\Model\Order\Invoice\Total\Discount" sort_order="100"/>
<item name="shipping" instance="Magento\Sales\Model\Order\Invoice\Total\Shipping" sort_order="150"/>
<item name="tax" instance="Magento\Sales\Model\Order\Invoice\Total\Tax" sort_order="200"/>
<item name="cost_total" instance="Magento\Sales\Model\Order\Invoice\Total\Cost" sort_order="250"/>
<item name="grand_total" instance="Magento\Sales\Model\Order\Invoice\Total\Grand" sort_order="350"/>
</group>
</section>
For order_creditmemo :
<section name="order_creditmemo">
<group name="totals">
<item name="subtotal" instance="Magento\Sales\Model\Order\Creditmemo\Total\Subtotal" sort_order="50"/>
<item name="discount" instance="Magento\Sales\Model\Order\Creditmemo\Total\Discount" sort_order="150"/>
<item name="shipping" instance="Magento\Sales\Model\Order\Creditmemo\Total\Shipping" sort_order="200"/>
<item name="tax" instance="Magento\Sales\Model\Order\Creditmemo\Total\Tax" sort_order="250"/>
<item name="cost_total" instance="Magento\Sales\Model\Order\Creditmemo\Total\Cost" sort_order="300"/>
<item name="grand_total" instance="Magento\Sales\Model\Order\Creditmemo\Total\Grand" sort_order="400"/>
</group>
</section>
Conclusion
The total collection is an important process in Magento and it's good to know how it works a bit because you will certainly be interacting with it at one point or another as a magento developer. It is used in magento natively to handle the things that are not the cart items but that impact the total price. This is how the shipping is handled and also the wee tax (Waste Electrical and Electronic Equipment) but you could imagine using it also for you own needs some day.
ElasticSearch / Magento 2 – various commands
Magento 2 uses ElasticSearch as a search engine.
It can be helpfull to directly request ElasticSearch to verify the data that have been indexed.
First you can get all indices.
In the command below ElastichSearch is accessible on host name 'elasticsearch' and on port 9200.
curl -X GET 'http://elasticsearch:9200/_cat/indices?v'
This will return the following data :
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size yellow open magento2_product_3_v10 h3qCDZVzT16OTOZAx3pMuA 1 1 0 0 208b 208b yellow open magento2_product_2_v10 uppg4DzLRSeNdf5HrgHcQg 1 1 189 0 79.4kb 79.4kb yellow open magento2_product_1_v10 nh8ckn7kS8-3l-F9dX8Tkw 1 1 0 0 208b 208b
You can then request documents in a specific indice.
Here for example, we are getting the 10 first documents in the indice magento2_product_2_v10.
curl -X GET --header 'Content-Type: application/json' \ http://elasticsearch:9200/magento2_product_2_v10/_search -d '{"size" : 10}'
The response look like that :
{ "took": 3, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 189, "relation": "eq" }, "max_score": 1.0, "hits": [ ... ] } }
You can se that the total number of documents that is returned in the "hits" > "total" field.
A hit structure will look like that :
{ "_index": "magento2_product_2_v10", "_type": "document", "_id": "1692", "_score": 1.0, "_source": { "store_id": "2", "options": [ "Prod 1" ], "sku": "prod_sku_1", "category_ids": [ 2 ], "position_category_2": "0", "name_category_2": "Default Category" } },
You can get documents using a query. Below we are requesting the documents in category 3.
curl -XGET --header 'Content-Type: application/json' http://elasticsearch:9200/magento2_product_2_v10/_search \ -d '{"query" : {"bool": {"must": [{"term": {"category_ids": "3"}}]}}}'
Magento 2 : create new stores using a store create processor
I have previously already created a post with a code example that show how to create stores programmaticaly. Here is this previous post : https://www.addeos.com/magento-2-create-a-store-storegroup-website-programmaticaly
Here I present an other way of doing if using a native create processor present in Magento_Store module.
We are going to use a helper that contains the logic for creating new stores.
This helper holds the native processor. It passes a data table to it and execute its run method.
<?php /** * @author Didier Berlioz * Copyright (c) Addeos All rights reserved. */ namespace Addeos\Store\Helper; use Exception; use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; use Magento\Store\Api\StoreRepositoryInterface; use Magento\Store\Model\Config\Importer\Processor\Create; use Magento\Store\Model\ResourceModel\Store as StoreModel; use Psr\Log\LoggerInterface; class Store extends AbstractHelper { /** * @var Create */ private $storeCreateProcessor; /** * @var StoreRepositoryInterface */ private $storeRepository; /** * @var StoreModel */ private $storeResourceModel; /** * @var LoggerInterface */ private $logger; /** * @var ConfigInterface */ private $config; /** * Store constructor. * @param Context $context * @param Create $storeCreateProcessor * @param StoreRepositoryInterface $storeRepository * @param StoreModel $storeResourceModel * @param LoggerInterface $logger */ public function __construct( Context $context, Create $storeCreateProcessor, StoreRepositoryInterface $storeRepository, StoreModel $storeResourceModel, LoggerInterface $logger ) { parent::__construct($context); $this->storeCreateProcessor = $storeCreateProcessor; $this->storeRepository = $storeRepository; $this->storeResourceModel = $storeResourceModel; $this->logger = $logger; } public function createStores($data) { try { $this->storeCreateProcessor->run($data); foreach ($data['stores'] as $storeData) { $this->updateStore($storeData['code'], $storeData['group_id'], $storeData['website_id']); } } catch (Exception $e) { $this->logger->error(__FILE__ . ' : ' . $e->getMessage()); } } private function updateStore($code, $groupId, $websiteId): void { try { $store = $this->storeRepository->get($code); $store->setStoreGroupId($groupId); $store->setWebsiteId($websiteId); $this->storeResourceModel->save($store); } catch (Exception $e) { $this->logger->error(__FILE__ . ' : ' . $e->getMessage()); } }
And then an installer that will use that helper. What this installer does is basically building the data and passing it to the helper.
In that example, we are creating 2 new websites which each have a group and a store.
<?php /** * @author Didier Berlioz * Copyright (c) Addeos All rights reserved. */ namespace Addeos\Store\Setup; use Magento\Framework\Setup\InstallDataInterface; use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\ModuleDataSetupInterface; use Addeos\Store\Helper\Store; class InstallData implements InstallDataInterface { const FIRST_STORE_ID = 8; const SECOND_STORE_ID = 9; const FIRST_WEBSITE_ID = 6; const SECOND_WEBSITE_ID = 7; const FIRST_GROUP_ID = 6; const SECOND_GROUP_ID = 7; const FIRST_STORE_COUNTRY = 'DE'; const SECOND_STORE_COUNTRY = 'AT'; const FIRST_STORE_LOCALE = 'de_DE'; const SECOND_STORE_LOCALE = 'at_DE'; const ROOT_CATEGORY_ID = 2; /** * @var Store */ private $storeHelper; /** * InstallData constructor. * @param Store $storeHelper */ public function __construct(Store $storeHelper) { $this->storeHelper = $storeHelper; } /** * @inheritDoc */ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context) { $this->storeHelper->createStores($this->getMyStoresData()); } /** * @return array */ private function getMyStoresData(): array { return [ 'websites' => [ [ 'website_id' => self::FIRST_WEBSITE_ID, 'code' => 'first_website_code', 'name' => 'First website name', 'sort_order' => 4, 'default_group_id' => self::FIRST_GROUP_ID, 'is_default' => 0, ], [ 'website_id' => self::SECOND_WEBSITE_ID, 'code' => 'second_website_code', 'name' => 'Second website name', 'sort_order' => 5, 'default_group_id' => self::SECOND_GROUP_ID, 'is_default' => 0, ], ], 'groups' => [ [ 'group_id' => self::FIRST_GROUP_ID, 'website_id' => self::FIRST_WEBSITE_ID, 'code' => 'first_group_code', 'name' => 'First group name', 'root_category_id' => self::ROOT_CATEGORY_ID, 'default_store_id' => self::FIRST_STORE_ID, ], [ 'group_id' => self::SECOND_GROUP_ID, 'website_id' => self::SECOND_WEBSITE_ID, 'code' => 'second_group_code', 'name' => 'Second group name', 'root_category_id' => self::ROOT_CATEGORY_ID, 'default_store_id' => self::SECOND_STORE_ID, ], ], 'stores' => [ [ 'store_id' => self::FIRST_STORE_ID, 'code' => 'first-store-code', 'website_id' => self::FIRST_WEBSITE_ID, 'group_id' => self::FIRST_GROUP_ID, 'name' => 'First store name', 'sort_order' => 0, 'is_active' => 0, 'locale' => self::FIRST_STORE_LOCALE, 'country' => self::FIRST_STORE_COUNTRY, ], [ 'store_id' => self::SECOND_STORE_ID, 'code' => 'second-store-code', 'website_id' => self::SECOND_WEBSITE_ID, 'group_id' => self::SECOND_GROUP_ID, 'name' => 'Second store name', 'sort_order' => 0, 'is_active' => 0, 'locale' => self::SECOND_STORE_LOCALE, 'country' => self::SECOND_STORE_COUNTRY, ], ], ]; } }
Magento 2 : Database anonymization module
This is my first module deployed on packagist.
This module once installed will offer a new command for anonymizing the database.
This can be useful on a development environment when the database has been retrieved from a production environment. It will anonymize all the customer's data (replace personal data) to be compatible on a GDPR point of view.
Install the module using composer :
composer require addeos/anonymize
Once the module is installed on your magento, simply call it using the following command :
php bin/magento addeos:anonymize
After execution is done, all your customer personal data will be transformed.
This must not be executed on a production environment!
Sometimes, some magento application can be set on production application mode even if the magento is not a production application. This can be on a preproduction magento for example.
In this case, there is a safety that will prevent the command to work but you can still call the command with a -f 1
option
php bin/magento addeos:anonymize -f 1