How to programmatically create customers in Magento 2.3.x

How to programmatically create customers in Magento 2.3.x

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
 
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::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 Inchoo\CustomerCreation\Console\Command;
 
use Exception;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\Console\Cli;
use Magento\Framework\Filesystem;
use Magento\Framework\App\State;
use Magento\Framework\App\Area;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Inchoo\CustomerCreation\Model\Customer;
 
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="Magento\Framework\Console\CommandList">
<arguments>
<argument name="commands" xsi:type="array">
<item name="CreateCustomers" xsi:type="object">Inchoo\CustomerCreation\Console\Command\CreateCustomers</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 Inchoo\CustomerCreation\Model;
 
use Exception;
use Generator;
use Magento\Framework\Filesystem\Io\File;
use Magento\Store\Model\StoreManagerInterface;
use Inchoo\CustomerCreation\Model\Import\CustomerImport;
use Symfony\Component\Console\Output\OutputInterface;
 
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 Inchoo\CustomerCreation\Model\Import;
 
use Magento\CustomerImportExport\Model\Import\Customer;
 
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.

You made it all the way down here so you must have enjoyed this post! You may also like:

Development environment for Magento 2 using Docker Tomas Novoselic
, | 14

Development environment for Magento 2 using Docker

GDPR compliant Magento 2 database dump Deni Pesic
Deni Pesic, | 3

GDPR compliant Magento 2 database dump

Magento Coding Standards Damir Korpar
Damir Korpar, | 5

Magento Coding Standards

2 comments

  1. Hello Deni Pesic,

    It’s a great guide to import customers as `customer main file` but customer address doesn’t import.
    Is there any way to import customer address in same command?

    Thanks

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <blockquote cite=""> <code> <del datetime=""> <em> <s> <strike> <strong>. You may use following syntax for source code: <pre><code>$current = "Inchoo";</code></pre>.

Tell us about your project

Drop us a line. We'd love to know more about your project.