Skip to main content
Developer Docs

Annotated Code & Style Reference

Master template for all Magento Core documentation. Includes annotated CustomerRepositoryInterface code and a complete component library for consistent styling.

Master Template Code Examples Component Library

The Primary Service Contract

CustomerRepositoryInterface is the main CRUD interface for customer data persistence in Magento

Architectural Position

Layer Hierarchy

Controllers / API Endpoints / Commands → Call this interface
↓
CustomerRepositoryInterface (Service Contract - You are here!)
↓
Model\ResourceModel\CustomerRepository (Implementation via DI)
↓
Database (customer_entity + EAV tables)

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

sortOrder: -1

TransactionWrapper

  • Opens database transaction BEFORE save/delete
  • Commits AFTER successful operation
  • Rolls back on ANY exception
  • Ensures customer + addresses + EAV saved atomically
webapi_rest

UpdateCustomer

REST API specific: Merges request body into customer DTO

API areas

CustomerAuthorization

  • Validates API token customer matches requested customer ID
  • Prevents customer A from accessing customer B's data
Third-party

Custom Plugins

Data enrichment, validation, logging/audit, integration with external systems

Method: save()

Signature: save(CustomerInterface $customer, $passwordHash = null): CustomerInterface

PHP
/**
 * 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

PHP
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))

PHP
/**
 * 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

PHP
// 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

PHP
/**
 * 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

PHP
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.

HTML - Complete Head Template
<!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.

1

Step Title

Step description with single paragraph.

2

Step With List Items

  • → First action item
  • → Second action item
  • → Third action item
3

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.

Bad Code Example Bad
// WRONG: Direct model manipulation
$customer = $this->customerFactory->create();
$customer->load($customerId);
$customer->save();
Good Code Example Good
// 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.

42
Metric Label
128
Another Metric
99%
Success Rate
15ms
Response Time

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.

Plugin/Card Grid

Use for listing plugins, observers, or related components with metadata.

sortOrder: -1

Plugin Name

  • What this plugin does
  • When it executes
  • Important behavior
webapi_rest

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.

Primary Tag Secondary Tag Gold Tag