The Primary Service Contract
CustomerRepositoryInterface is the main CRUD interface for customer data persistence in Magento
Architectural Position
Layer Hierarchy
Why Service Contracts?
- Backward Compatibility: @api interfaces cannot change signatures
- Decoupling: Controllers don't depend on implementation details
- Plugin Intercept Points: Plugins can only intercept interfaces
- API Exposure: REST/SOAP APIs auto-generate from service contracts
- Testability: Easy to mock in unit tests
Plugins Intercepting This Interface
TransactionWrapper
- Opens database transaction BEFORE save/delete
- Commits AFTER successful operation
- Rolls back on ANY exception
- Ensures customer + addresses + EAV saved atomically
UpdateCustomer
REST API specific: Merges request body into customer DTO
CustomerAuthorization
- Validates API token customer matches requested customer ID
- Prevents customer A from accessing customer B's data
Custom Plugins
Data enrichment, validation, logging/audit, integration with external systems
Method: save()
Signature: save(CustomerInterface $customer, $passwordHash = null): CustomerInterface
/**
* Create or update a customer.
*
* EXECUTION FLOW:
* 1. TransactionWrapper::beforeSave() - Opens database transaction
* 2. Validation (email format, required fields, uniqueness)
* 3. Convert DTO to Model (CustomerInterface → Customer)
* 4. Set password_hash if provided
* 5. Database persistence (INSERT/UPDATE customer_entity + EAV)
* 6. Save addresses if included
* 7. EVENT: customer_save_after_data_object
* - Triggers email sync observers
* 8. TransactionWrapper::afterSave() - Commits transaction
* 9. Return saved CustomerInterface DTO
*
* TRANSACTION SAFETY:
* The TransactionWrapper plugin ensures:
* - Customer + addresses saved atomically
* - Email sync observers run in same transaction
* - Validation failures rollback everything
*
* PASSWORD HANDLING:
* CREATE with password:
* $hash = $encryptor->getHash($plainPassword, true);
* $repository->save($customer, $hash);
*
* UPDATE without changing password:
* $repository->save($customer); // Don't pass hash
*
* @param CustomerInterface $customer Customer data object
* @param string|null $passwordHash Hashed password (optional)
* @return CustomerInterface Saved customer with generated ID
*/
public function save(
\Magento\Customer\Api\Data\CustomerInterface $customer,
$passwordHash = null
);
Critical Side Effect: Email Synchronization
When customer email changes, observers automatically update:
- All historical orders get new email (UpgradeOrderCustomerEmailObserver)
- Active quote gets new email (UpgradeQuoteCustomerEmailObserver)
- This happens in the SAME transaction as customer save
UPDATE sales_order SET customer_email = ? WHERE customer_id = ?
UPDATE quote SET customer_email = ? WHERE customer_id = ? AND is_active = 1
Error Handling
try {
$customer = $this->customerRepository->save($customer, $passwordHash);
} catch (\Magento\Framework\Exception\InputException $e) {
// Validation error (invalid email, missing required fields)
foreach ($e->getErrors() as $error) {
$this->messageManager->addErrorMessage($error->getMessage());
}
} catch (\Magento\Framework\Exception\State\InputMismatchException $e) {
// Email already exists (unique constraint violation)
$this->messageManager->addErrorMessage($e->getMessage());
} catch (\Magento\Framework\Exception\LocalizedException $e) {
// Business logic error
$this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
// Unexpected error
$this->logger->critical($e);
$this->messageManager->addErrorMessage(__('Unable to save customer.'));
}
Method: getById()
Signature: getById(int $customerId): CustomerInterface
Performance: This is the FASTEST way to load a customer (primary key lookup O(1))
/**
* Get customer by Customer ID.
*
* PREFERRED LOOKUP METHOD - Uses primary key (entity_id)
*
* Database query: SELECT * FROM customer_entity WHERE entity_id = ?
* Performance: O(1) - direct primary key lookup
*
* This method loads:
* - customer_entity row (main table)
* - All EAV attributes (joins to customer_entity_varchar, _int, etc.)
* - Custom attributes (if defined)
* - Extension attributes (if defined)
*
* Does NOT load:
* - Addresses (must call AddressRepositoryInterface separately)
*
* WHERE TO GET CUSTOMER ID:
* - Customer session: $this->session->getCustomerId()
* - Order: $order->getCustomerId()
* - Quote: $quote->getCustomerId()
* - URL parameter: $this->getRequest()->getParam('customer_id')
*
* @param int $customerId Customer ID (entity_id)
* @return CustomerInterface Customer data object
* @throws NoSuchEntityException If customer doesn't exist
*/
public function getById($customerId);
Usage Example
// Fast: Primary key lookup
$customerId = $this->session->getCustomerId();
$customer = $this->customerRepository->getById($customerId);
// Slow: Secondary index lookup (email + website_id)
$customer = $this->customerRepository->get('john@example.com', $websiteId);
// Always prefer getById() when you have the ID!
Method: getList()
Signature: getList(SearchCriteriaInterface $searchCriteria): CustomerSearchResultsInterface
/**
* Retrieve customers which match specified criteria.
*
* Example usage:
*/
$searchCriteria = $this->searchCriteriaBuilder
->addFilter('email', '%@example.com', 'like')
->addFilter('group_id', [1, 2], 'in')
->addFilter('website_id', $websiteId)
->setPageSize(50)
->setCurrentPage(1)
->addSortOrder(
$this->sortOrderBuilder
->setField('created_at')
->setDirection('DESC')
->create()
)
->create();
$results = $this->customerRepository->getList($searchCriteria);
echo "Total customers: " . $results->getTotalCount();
foreach ($results->getItems() as $customer) {
echo $customer->getEmail() . "\n";
}
Filter Conditions
- 'eq' - equals (default)
- 'neq' - not equals
- 'like' - SQL LIKE with %
- 'in' - IN array
- 'gt' - greater than
- 'lt' - less than
- 'null' - IS NULL
Performance Tips
- Always use pagination - setPageSize()
- Filter on indexed fields (email, website_id)
- Avoid multiple EAV filters (adds JOINs)
- Use specific filters to reduce result set
Complete Usage Example
namespace Vendor\Module\Model;
use Magento\Customer\Api\CustomerInterfaceFactory;
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Framework\Encryption\EncryptorInterface;
use Magento\Store\Model\StoreManagerInterface;
class CustomerService
{
private $customerFactory;
private $customerRepository;
private $encryptor;
private $storeManager;
public function __construct(
CustomerInterfaceFactory $customerFactory,
CustomerRepositoryInterface $customerRepository,
EncryptorInterface $encryptor,
StoreManagerInterface $storeManager
) {
$this->customerFactory = $customerFactory;
$this->customerRepository = $customerRepository;
$this->encryptor = $encryptor;
$this->storeManager = $storeManager;
}
/**
* Create new customer account
*/
public function createCustomer($email, $firstname, $lastname, $password)
{
$websiteId = $this->storeManager->getWebsite()->getId();
$storeId = $this->storeManager->getStore()->getId();
// Create customer DTO
$customer = $this->customerFactory->create();
$customer->setWebsiteId($websiteId);
$customer->setStoreId($storeId);
$customer->setEmail($email);
$customer->setFirstname($firstname);
$customer->setLastname($lastname);
$customer->setGroupId(1); // General group
// Hash password
$passwordHash = $this->encryptor->getHash($password, true);
try {
// Save customer (TransactionWrapper ensures atomicity)
$savedCustomer = $this->customerRepository->save($customer, $passwordHash);
return $savedCustomer;
} catch (\Magento\Framework\Exception\InputException $e) {
throw new \Exception('Invalid customer data: ' . $e->getMessage());
} catch (\Magento\Framework\Exception\State\InputMismatchException $e) {
throw new \Exception('Email already exists');
}
}
/**
* Update customer information
*/
public function updateCustomer($customerId, $firstname, $lastname)
{
try {
// Load existing customer (fast primary key lookup)
$customer = $this->customerRepository->getById($customerId);
// Update fields
$customer->setFirstname($firstname);
$customer->setLastname($lastname);
// Save (no password hash = password unchanged)
$savedCustomer = $this->customerRepository->save($customer);
return $savedCustomer;
} catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
throw new \Exception('Customer not found');
}
}
}
Style Reference - Component Library
This section documents all styling patterns used across the Magento Core documentation. Use these as templates when building new documentation pages.
Tailwind Config & Head Template
Copy this complete head section for new documentation pages. Includes Tailwind config, fonts, Alpine.js, and highlight.js.
<!DOCTYPE html>
<html lang="en" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page Title | Magento Core Documentation</title>
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'magento-orange': {
DEFAULT: '#f26423',
50: '#fef5ee', 100: '#fee9d6', 200: '#fbd0ad', 300: '#f9ae78',
400: '#f58242', 500: '#f26423', 600: '#e34613', 700: '#bc3312',
800: '#962a16', 900: '#792515', 950: '#411009'
},
'magento-gold': {
DEFAULT: '#f1bc1b',
50: '#fffdeb', 100: '#fdf9c8', 200: '#fbf38c', 300: '#f8e651',
400: '#f7d728', 500: '#f1bc1b', 600: '#d5900a', 700: '#b1670c',
800: '#8f5111', 900: '#764311', 950: '#442204'
},
'magento-charcoal': {
DEFAULT: '#2c2c2c',
50: '#f1f1f1', 100: '#d9d9d9', 200: '#b6b6b6', 300: '#818181',
400: '#474747', 500: '#2c2c2c', 600: '#262626', 700: '#202020',
800: '#1c1c1c', 900: '#191919', 950: '#121212'
},
},
fontFamily: {
sans: ['Inter Tight', 'system-ui', 'sans-serif'],
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', 'monospace'],
},
}
}
}
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700&display=swap');
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
<!-- Syntax Highlighting (include if page has code blocks) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
<style>
[x-cloak] { display: none !important; }
/* VS Code Dark+ Theme */
.hljs { background: transparent; }
.hljs-comment { color: #6a9955; }
.hljs-keyword { color: #569cd6; }
.hljs-function .hljs-title, .hljs-title.function_ { color: #dcdcaa; }
.hljs-variable, .hljs-variable.language_ { color: #9cdcfe; }
.hljs-string { color: #ce9178; }
.hljs-number { color: #b5cea8; }
.hljs-literal, .hljs-built_in { color: #569cd6; }
.hljs-type, .hljs-title.class_ { color: #4ec9b0; }
.hljs-tag, .hljs-name { color: #569cd6; }
</style>
</head>
Brand Color Reference
magento-orange
Primary accent, CTAs, borders
#f26423
magento-gold
Callouts, tips, highlights
#f1bc1b
magento-charcoal
Text, headers, code blocks
#2c2c2c
Important: Only use these three brand colors. Never use vanilla Tailwind colors like blue-500, purple-600, or green-500.
Execution Flow Steps
Use for step-by-step processes, execution sequences, or numbered workflows.
Step Title
Step description with single paragraph.
Step With List Items
- → First action item
- → Second action item
- → Third action item
Step With Code Reference
Call service contract: ServiceInterface::method($param)
Key Points Callout
Use at the end of execution flows or sections to highlight important takeaways.
Key Points
Point Title
Detailed explanation of this key point with additional context.
Another Key Point
Second important takeaway from this section.
Entry Point Section
Use to document API endpoints, controllers, or service entry points.
Entry Point
Controller: Magento\Module\Controller\Action::execute()
Route: POST /module/action/endpoint
Area: frontend
Warning & Error Callouts
Use for highlighting problems, anti-patterns, or critical warnings.
Why It's Bad
- → First reason this approach is problematic
- → Second consequence of this anti-pattern
- → Third issue that will arise
CRITICAL SEVERITY
Success & Solution Callouts
Use for highlighting correct approaches, best practices, or solutions.
Why It's Better
- ✓ First benefit of this approach
- ✓ Second advantage
- ✓ Third positive outcome
Bad/Good Code Comparison
Use for anti-pattern documentation showing incorrect vs correct implementations.
// WRONG: Direct model manipulation
$customer = $this->customerFactory->create();
$customer->load($customerId);
$customer->save();
// CORRECT: Use repository service contract
$customer = $this->customerRepository->getById($customerId);
$this->customerRepository->save($customer);
Statistics Grid
Use for displaying key metrics, issue counts, or summary statistics.
Data Table
Use for structured data, issue metadata, or comparison tables.
| Property | Value or description |
| Link Property | #12345 |
| Status | Active |
| Last Property | Final value (no bottom border) |
Two Column Grid
Use for side-by-side content, related information, or comparison layouts.
Left Column Title
- First item in list
- Second item in list
- Third item in list
Right Column Title
- Bold item: with description
- Another bold: with more info
Table of Contents
Use at the top of long pages for navigation.
Category One
Category Two
Plugin/Card Grid
Use for listing plugins, observers, or related components with metadata.
Plugin Name
- What this plugin does
- When it executes
- Important behavior
Another Plugin
Single line description of this plugin's purpose.
Section Headers
Various header styles for different section types.
Page Section Header:
Major Section Title
Content Card Header:
Card Section Title
Subsection Header:
Subsection Title
Header Tags/Badges
Use in page headers to show metadata, categories, or status.