There are several ways to create customers in Magento 2. A customer can create an account on their own using the sign-up form, a customer can be created through the admin interface, and there is even a built-in Magento 2 import feature that can mass import a huge number of customers from a CSV file, provided that the customer data in the CSV is import-ready.
But what if we have thousands of customers whose data still needs to be processed before they can be created? The best way to do this would be to create the customers programmatically.
In this article, we are going to cover the topic of creating customers programmatically, and to this purpose, we are going to create a simple Magento 2 module which is going to have a custom console command and a couple of models that are going to be used to read, process and create customers.
The Body
Let’s get started by first creating a module in the /app/code
directory of our Magento 2 installation. In this example, I am going to use Inchoo as the module vendor and I am going to name the module CustomerCreation, but you can name them as you see fit.
Inside our module, we are going to need the following directories and files:
registration.php
/etc/module.xml
/Console/Command/CreateCustomers.php
/etc/di.xml
/Model/Customer.php
/Model/Import/CustomerImport.php
registration.php
For our customer creation module to work, we need to register our module in the Magento system. Copy the following code to the registration.php
file:
<?php
MagentoFrameworkComponentComponentRegistrar::register(
MagentoFrameworkComponentComponentRegistrar::MODULE,
'Inchoo_CustomerCreation',
__DIR__
);
module.xml
Our module also needs to declare its name and existence. Copy the following code to the /etc/module.xml
file:
<?xml version="1.0"?>
<config xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance" xsi_noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Inchoo_CustomerCreation" setup_version="1.0.0" />
</config>
The Heart
Now that we have our module set up, we need a way to trigger our customer creation process. One way we can do this is by creating a custom console command.
Let’s start by importing a bunch of classes in the /Console/Command/CreateCustomers.php
file, let’s also define the class and make it extend the imported Command class:
<?php
namespace InchooCustomerCreationConsoleCommand;
use Exception;
use MagentoFrameworkAppFilesystemDirectoryList;
use MagentoFrameworkConsoleCli;
use MagentoFrameworkFilesystem;
use MagentoFrameworkAppState;
use MagentoFrameworkAppArea;
use SymfonyComponentConsoleCommandCommand;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleOutputOutputInterface;
use InchooCustomerCreationModelCustomer;
class CreateCustomers extends Command
{
// everything else goes here
}
You may notice that our Customer model has not yet been defined. Don’t worry about it right now, we are going to define it later.
Next, we need to create a constructor inside our class and inject some dependencies with it:
private $filesystem;
private $customer;
private $state;
public function __construct(
Filesystem $filesystem,
Customer $customer,
State $state
) {
parent::__construct();
$this->filesystem = $filesystem;
$this->customer = $customer;
$this->state = $state;
}
We also need to configure our console command by setting a name for it using the configure()
method inside our class. This method is inherited from the Command class and can also be used to set the command description, input arguments, and other options.
public function configure(): void
{
$this->setName('create:customers');
}
Let’s now define the execute()
method that will be triggered when we invoke the bin/magento create:customers
command. This method is also inherited from the Command class, and in it we are going to write our logic.
In the execute()
method, we first have to set the Area Code to global
. If we don’t do this, the customer creation process will produce an error later.
After that, we have to get the absolute path of our CSV file that contains all of the customer data. In this example, the CSV file is named customers.csv
and is located in the /pub/media/fixtures
directory (relative to the root directory, not our module).
Once we have the absolute path, we have to call the install()
method (this method is going to be defined later in our Customer model) and pass it the absolute path of the CSV file as an argument. If there are any errors, we will need to catch them and show the error messages in our CLI:
public function execute(InputInterface $input, OutputInterface $output): ?int
{
try {
$this->state->setAreaCode(Area::AREA_GLOBAL);
$mediaDir = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
$fixture = $mediaDir->getAbsolutePath() . 'fixtures/customers.csv';
$this->customer->install($fixture, $output);
return Cli::RETURN_SUCCESS;
} catch (Exception $e) {
$msg = $e->getMessage();
$output->writeln("<error>$msg</error>", OutputInterface::OUTPUT_NORMAL);
return Cli::RETURN_FAILURE;
}
}
For our custom command to work, we also have to configure the command name using dependency injection.
Add the following code to the /etc/di.xml
file:
<?xml version="1.0"?>
<config xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi_noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="MagentoFrameworkConsoleCommandList">
<arguments>
<argument name="commands" xsi_type="array">
<item name="CreateCustomers" xsi_type="object">InchooCustomerCreationConsoleCommandCreateCustomers</item>
</argument>
</arguments>
</type>
</config>
The Soul
It’s time to create our Customer model. This is where we are going to read the CSV data, process it, store it in an array, and send it off to be saved.
In the /Model/Customer.php
file, import the following classes and define the Customer class:
<?php
namespace InchooCustomerCreationModel;
use Exception;
use Generator;
use MagentoFrameworkFilesystemIoFile;
use MagentoStoreModelStoreManagerInterface;
use InchooCustomerCreationModelImportCustomerImport;
use SymfonyComponentConsoleOutputOutputInterface;
class Customer
{
// everything else goes here
}
We have to create a constructor in this class as well and inject some dependencies:
private $file;
private $storeManagerInterface;
private $customerImport;
private $output;
public function __construct(
File $file,
StoreManagerInterface $storeManagerInterface,
CustomerImport $customerImport
) {
$this->file = $file;
$this->storeManagerInterface = $storeManagerInterface;
$this->customerImport = $customerImport;
}
Let’s define the install()
method inside our Customer class. This is the method that we called earlier in our custom console command.
We start by retrieving the store and website IDs. After that, we retrieve the CSV header and then iterate through each CSV row, reading the data contained in those rows and passing them to the createCustomer()
method that we have yet to define.
public function install(string $fixture, OutputInterface $output): void
{
$this->output = $output;
// get store and website ID
$store = $this->storeManagerInterface->getStore();
$websiteId = (int) $this->storeManagerInterface->getWebsite()->getId();
$storeId = (int) $store->getId();
// read the csv header
$header = $this->readCsvHeader($fixture)->current();
// read the csv file and skip the first (header) row
$row = $this->readCsvRows($fixture, $header);
$row->next();
// while the generator is open, read current row data, create a customer and resume the generator
while ($row->valid()) {
$data = $row->current();
$this->createCustomer($data, $websiteId, $storeId);
$row->next();
}
}
To read the CSV header and rows, we use a generator. A generator allows us to write code that can iterate over a set of data without building an array in memory. If we have a large CSV file, this can help us to not exceed the memory limit. The readCsvRows()
method will read the row data and map it to the headers retrieved from the readCsvHeader()
method.
Create the following methods:
private function readCsvRows(string $file, array $header): ?Generator
{
$handle = fopen($file, 'rb');
while (!feof($handle)) {
$data = [];
$rowData = fgetcsv($handle);
if ($rowData) {
foreach ($rowData as $key => $value) {
$data[$header[$key]] = $value;
}
yield $data;
}
}
fclose($handle);
}
private function readCsvHeader(string $file): ?Generator
{
$handle = fopen($file, 'rb');
while (!feof($handle)) {
yield fgetcsv($handle);
}
fclose($handle);
}
Our final method in this class is the createCustomer()
method.
The $data
argument passed to the method is an associative array that contains key-value pairs from our CSV file, each key is a different field name from the header list (e.g. email_address) and each value is the associated record (e.g. john.doe@email.com).
Inside our method, we need to define the $customerData
array. This will also be an associative array in which we need to pair the values from our $data
array to keys that the importCustomerData()
method is going to need to save a customer. If any errors are caught, their error messages will be printed in our CLI.
The example below assumes that the CSV file has the exact data that Magento needs to create a customer, however, in most situations, this is not the case. Perhaps the customer date of birth does not have the proper format or we might not have the customer group id, just the customer group name. In that case, before we populate the $customerData
array, we need to make sure to process the data from the $data
array so that it contains the proper values.
private function createCustomer(array $data, int $websiteId, int $storeId): void
{
try {
// collect the customer data
$customerData = [
'email' => $data['email_address'],
'_website' => 'base',
'_store' => 'default',
'confirmation' => null,
'dob' => null,
'firstname' => $data['firstname'],
'gender' => null,
'group_id' => $data['customer_group_id'],
'lastname' => $data['last_name'],
'middlename' => null,
'password_hash' => $data['password_hash'],
'prefix' => null,
'store_id' => $storeId,
'website_id' => $websiteId,
'password' => null,
'disable_auto_group_change' => 0,
'some_custom_attribute' => 'some_custom_attribute_value'
];
// save the customer data
$this->customerImport->importCustomerData($customerData);
} catch (Exception $e) {
$this->output->writeln(
'<error>'. $e->getMessage() .'</error>',
OutputInterface::OUTPUT_NORMAL
);
}
}
The Entity
For our final step, we need to create our CustomerImport model. In the /Module/Import/CustomerImport.php
file, let’s import the Customer class from the Magento CustomerImportExport module and define our CustomerImport class. Make it extend the imported Customer class.
Our class is going to contain the importCustomerData()
method that we use to prepare and save our customer data.
<?php
namespace InchooCustomerCreationModelImport;
use MagentoCustomerImportExportModelImportCustomer;
class CustomerImport extends Customer
{
// everything else goes here
}
Now that our class has been defined, we need to create the importCustomerData()
method that handles the last steps of the customer creation process. In this method, we will create or update a customer entity and save any provided attribute data for that customer. The method will then return the customer entity ID. This method is actually a slight modification of a function found in the Customer class that we are extending.
public function importCustomerData(array $rowData)
{
$this->prepareCustomerData($rowData);
$entitiesToCreate = [];
$entitiesToUpdate = [];
$entitiesToDelete = [];
$attributesToSave = [];
$processedData = $this->_prepareDataForUpdate($rowData);
$entitiesToCreate = array_merge($entitiesToCreate, $processedData[self::ENTITIES_TO_CREATE_KEY]);
$entitiesToUpdate = array_merge($entitiesToUpdate, $processedData[self::ENTITIES_TO_UPDATE_KEY]);
foreach ($processedData[self::ATTRIBUTES_TO_SAVE_KEY] as $tableName => $customerAttributes) {
if (!isset($attributesToSave[$tableName])) {
$attributesToSave[$tableName] = [];
}
$attributesToSave[$tableName] = array_diff_key(
$attributesToSave[$tableName],
$customerAttributes
) + $customerAttributes;
}
$this->updateItemsCounterStats($entitiesToCreate, $entitiesToUpdate, $entitiesToDelete);
/**
* Save prepared data
*/
if ($entitiesToCreate || $entitiesToUpdate) {
$this->_saveCustomerEntities($entitiesToCreate, $entitiesToUpdate);
}
if ($attributesToSave) {
$this->_saveCustomerAttributes($attributesToSave);
}
return $entitiesToCreate[0]['entity_id'] ?? $entitiesToUpdate[0]['entity_id'] ?? null;
}
To finish things up, we need to invoke the bin/magento setup:upgrade
and bin/magento setup:di:compile
commands to get our module and custom command working.
Once we run our customer creation script with bin/magento create:customers
, we also need to make sure to re-index the Customer Grid indexer. We can do that by invoking the bin/magento indexer:reindex customer_grid
command.
The End
And that’s it, we should now have a fully functioning module that allows us to programmatically create thousands of customers in a matter of minutes. If you have any questions or issues, please leave a comment below.