Skip to content

Customer Segments - Developer Guide v25.9+

This guide covers the technical implementation of Maho's Customer Segmentation system, including architecture, database schema, condition types, code examples, and extension points.

Architecture Overview

Customer Segments in Maho use a rule-based condition system that generates optimized SQL queries for matching customers against complex criteria.

┌─────────────────────────┐
│   Segment Definition    │  (Conditions, rules, config)
└───────────┬─────────────┘
┌─────────────────────────┐
│  Condition Tree Builder │  (Nested AND/OR logic)
└───────────┬─────────────┘
┌─────────────────────────┐
│   SQL Query Generator   │  (Optimized SELECT statements)
└───────────┬─────────────┘
┌─────────────────────────┐
│  Membership Cache       │  (customer_segment_customer table)
└─────────────────────────┘

Key Components

Component Location Purpose
Segment Model Maho_CustomerSegmentation_Model_Segment Core segment logic and orchestration
Condition Base Maho_CustomerSegmentation_Model_Segment_Condition_Abstract Base class for all condition types
Condition Combine Maho_CustomerSegmentation_Model_Segment_Condition_Combine Logical AND/OR/NOT combinations
Condition Types Maho_CustomerSegmentation_Model_Segment_Condition_* 13 specialized condition implementations
Resource Model Maho_CustomerSegmentation_Model_Resource_Segment Database operations and SQL generation
Cron Model Maho_CustomerSegmentation_Model_Cron Scheduled segment refresh

Database Schema

customer_segment

Primary segment configuration table:

CREATE TABLE customer_segment (
    segment_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    is_active SMALLINT UNSIGNED NOT NULL DEFAULT 1,
    conditions_serialized MEDIUMTEXT,              -- Serialized condition tree
    website_ids TEXT,                               -- Comma-separated website IDs
    customer_group_ids TEXT,                        -- Comma-separated group IDs
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    matched_customers_count INT UNSIGNED DEFAULT 0,
    last_refresh_at TIMESTAMP NULL,
    refresh_status VARCHAR(20) DEFAULT 'pending',  -- 'pending', 'processing', 'completed', 'error'
    refresh_mode VARCHAR(20) DEFAULT 'auto',       -- 'auto', 'manual'
    priority INT UNSIGNED DEFAULT 0,
    auto_email_trigger VARCHAR(10) NOT NULL DEFAULT 'none',
    auto_email_active SMALLINT NOT NULL DEFAULT 0,
    INDEX idx_is_active (is_active),
    INDEX idx_refresh_status (refresh_status),
    INDEX idx_priority (priority)
);

Field Descriptions: - conditions_serialized: PHP serialized array representing the condition tree - refresh_status: Tracks current refresh state - refresh_mode: Auto (cron) or manual refresh - priority: Higher priority segments evaluated first in sales rules

customer_segment_customer

Materialized segment membership cache:

CREATE TABLE customer_segment_customer (
    segment_id INT UNSIGNED NOT NULL,
    customer_id INT UNSIGNED NOT NULL,
    website_id SMALLINT UNSIGNED NOT NULL,
    added_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (segment_id, customer_id, website_id),
    INDEX idx_customer_id (customer_id),
    INDEX idx_website_id (website_id),
    FOREIGN KEY (segment_id) REFERENCES customer_segment(segment_id) ON DELETE CASCADE,
    FOREIGN KEY (customer_id) REFERENCES customer_entity(entity_id) ON DELETE CASCADE
);

Purpose: Fast membership lookup without re-evaluating conditions

Query Pattern:

-- Check if customer is in segment
SELECT 1 FROM customer_segment_customer
WHERE segment_id = ? AND customer_id = ? AND website_id = ?
LIMIT 1;

Condition System Architecture

Condition Tree Structure

Segments use a tree-based condition structure:

Combine (AND)
  ├── Customer Attributes
  │     └── Lifetime Sales > $1000
  ├── Order Attributes
  │     └── Days Since Last Order < 30
  └── Combine (OR)
        ├── Newsletter: Subscribed
        └── Customer Group: VIP

Serialized Format:

[
    'type' => 'customersegmentation/segment_condition_combine',
    'aggregator' => 'all',  // 'all' (AND), 'any' (OR)
    'value' => '1',
    'conditions' => [
        [
            'type' => 'customersegmentation/segment_condition_customer_clv',
            'attribute' => 'lifetime_sales',
            'operator' => '>',
            'value' => '1000'
        ],
        // ... more conditions
    ]
]

Condition Base Class

All conditions extend Maho_CustomerSegmentation_Model_Segment_Condition_Abstract:

abstract class Maho_CustomerSegmentation_Model_Segment_Condition_Abstract
    extends Mage_Rule_Model_Condition_Abstract
{
    /**
     * Generate SQL WHERE clause for this condition
     *
     * @param AdapterInterface $adapter Database adapter
     * @param int|null $websiteId Scope to specific website
     * @return string|false SQL condition or false if not applicable
     */
    abstract public function getConditionsSql(
        \Maho\Db\Adapter\AdapterInterface $adapter,
        ?int $websiteId = null
    ): string|false;

    /**
     * Get available attributes for this condition type
     */
    abstract public function loadAttributeOptions(): self;

    /**
     * Get selectable options for attribute values
     */
    public function getValueSelectOptions(): array;

    /**
     * Get HTML input type for value field
     */
    public function getInputType(): string;

    /**
     * Get available operators for this condition
     */
    public function getOperatorSelectOptions(): array;
}

SQL Generation Process

  1. Condition Tree Traversal: Recursively process condition tree
  2. SQL Fragment Generation: Each condition generates WHERE clause fragment
  3. Combination: Fragments joined with AND/OR based on aggregator
  4. Subquery Assembly: Complex conditions use subqueries
  5. Final Query: Complete SELECT with JOINs and WHERE clauses

Example Generated SQL:

SELECT DISTINCT e.entity_id
FROM customer_entity e
WHERE e.entity_id IN (
    -- Subquery for lifetime sales condition
    SELECT customer_id FROM (
        SELECT c.entity_id as customer_id,
               COALESCE(SUM(o.grand_total), 0) as total
        FROM customer_entity c
        LEFT JOIN sales_flat_order o ON c.entity_id = o.customer_id
            AND o.state NOT IN ('canceled', 'closed')
        GROUP BY c.entity_id
    ) AS clv
    WHERE clv.total > 1000
)
AND e.website_id IN (1)
AND e.group_id IN (1, 2, 3);

Condition Types Reference

1. Customer Attributes

Class: Maho_CustomerSegmentation_Model_Segment_Condition_Customer_Attributes

Available Attributes:

[
    'email' => 'Email',
    'firstname' => 'First Name',
    'lastname' => 'Last Name',
    'gender' => 'Gender',
    'dob' => 'Date of Birth',
    'created_at' => 'Customer Since',
    'group_id' => 'Customer Group',
    'store_id' => 'Account Created In Store',
    'days_since_registration' => 'Days Since Registration',
    'days_until_birthday' => 'Days Until Birthday',
]

SQL Pattern:

// For direct fields
WHERE e.email = '[email protected]'

// For EAV attributes
WHERE e.entity_id IN (
    SELECT entity_id FROM customer_entity_varchar
    WHERE attribute_id = ? AND value = ?
)

// For calculated fields
WHERE DATEDIFF(NOW(), e.created_at) > 30  -- Days since registration


2. Customer Address

Class: Maho_CustomerSegmentation_Model_Segment_Condition_Customer_Address

Available Attributes:

[
    'country_id' => 'Country',
    'region_id' => 'State/Province',
    'city' => 'City',
    'postcode' => 'Postal Code',
    'street' => 'Street Address',
]

SQL Pattern:

WHERE e.entity_id IN (
    SELECT parent_id FROM customer_address_entity_varchar
    WHERE attribute_id = ? AND value = ?
)


3. Customer Lifetime Value (CLV)

Class: Maho_CustomerSegmentation_Model_Segment_Condition_Customer_Clv

Available Metrics:

[
    'lifetime_sales' => 'Lifetime Sales Amount',
    'number_of_orders' => 'Number of Orders',
    'average_order_value' => 'Average Order Value',
    'lifetime_profit' => 'Lifetime Profit (Sales - Refunds)',
    'lifetime_refunds' => 'Lifetime Refunds Amount',
]

SQL Pattern:

// Lifetime Sales
WHERE e.entity_id IN (
    SELECT customer_id FROM (
        SELECT c.entity_id as customer_id,
               COALESCE(SUM(o.grand_total), 0) as total
        FROM customer_entity c
        LEFT JOIN sales_flat_order o
            ON c.entity_id = o.customer_id
            AND o.state NOT IN ('canceled', 'closed')
        GROUP BY c.entity_id
    ) AS clv
    WHERE clv.total > 1000
)


4. Newsletter Subscription

Class: Maho_CustomerSegmentation_Model_Segment_Condition_Customer_Newsletter

Available Statuses:

[
    Mage_Newsletter_Model_Subscriber::STATUS_SUBSCRIBED => 'Subscribed',
    Mage_Newsletter_Model_Subscriber::STATUS_NOT_ACTIVE => 'Not Activated',
    Mage_Newsletter_Model_Subscriber::STATUS_UNSUBSCRIBED => 'Unsubscribed',
    Mage_Newsletter_Model_Subscriber::STATUS_UNCONFIRMED => 'Unconfirmed',
]

SQL Pattern:

WHERE e.entity_id IN (
    SELECT customer_id FROM newsletter_subscriber
    WHERE subscriber_status = 1
)


5. Shopping Cart Attributes

Class: Maho_CustomerSegmentation_Model_Segment_Condition_Cart_Attributes

Available Attributes:

[
    'is_active' => 'Cart Status',
    'items_count' => 'Cart Items Count',
    'items_qty' => 'Cart Items Quantity',
    'subtotal' => 'Cart Subtotal',
    'grand_total' => 'Cart Grand Total',
    'tax_amount' => 'Cart Tax Amount',
    'discount_amount' => 'Cart Discount Amount',
    'updated_at' => 'Cart Updated Date',
    'days_since_update' => 'Days Since Cart Update',
]

SQL Pattern:

// Active cart with grand total > 100
WHERE e.entity_id IN (
    SELECT customer_id FROM sales_flat_quote
    WHERE is_active = 1
    AND grand_total > 100
    AND store_id IN (1,2,3)  -- Website stores
)


6. Shopping Cart Items

Class: Maho_CustomerSegmentation_Model_Segment_Condition_Cart_Items

Condition on Products: Match based on products in active cart

SQL Pattern:

WHERE e.entity_id IN (
    SELECT q.customer_id
    FROM sales_flat_quote q
    JOIN sales_flat_quote_item qi ON q.entity_id = qi.quote_id
    WHERE q.is_active = 1
    AND qi.product_id = ?
)


7. Order Attributes

Class: Maho_CustomerSegmentation_Model_Segment_Condition_Order_Attributes

Available Attributes:

[
    'total_qty' => 'Total Items Quantity',
    'total_amount' => 'Total Amount',
    'subtotal' => 'Subtotal',
    'tax_amount' => 'Tax Amount',
    'shipping_amount' => 'Shipping Amount',
    'discount_amount' => 'Discount Amount',
    'grand_total' => 'Grand Total',
    'status' => 'Order Status',
    'created_at' => 'Purchase Date',
    'updated_at' => 'Last Modified',
    'store_id' => 'Store',
    'currency_code' => 'Currency',
    'payment_method' => 'Payment Method',
    'shipping_method' => 'Shipping Method',
    'coupon_code' => 'Coupon Code',
    'days_since_last_order' => 'Days Since Last Order',
    'average_order_amount' => 'Average Order Amount',
    'total_ordered_amount' => 'Total Ordered Amount',
]

SQL Pattern:

// Days since last order
WHERE e.entity_id IN (
    SELECT o.customer_id
    FROM sales_flat_order o
    WHERE DATEDIFF(NOW(), o.created_at) > 90
    GROUP BY o.customer_id
    HAVING MAX(o.created_at) = o.created_at
)


8. Order Items

Class: Maho_CustomerSegmentation_Model_Segment_Condition_Order_Items

Condition on Products: Match based on purchased products

SQL Pattern:

WHERE e.entity_id IN (
    SELECT o.customer_id
    FROM sales_flat_order o
    JOIN sales_flat_order_item oi ON o.entity_id = oi.order_id
    WHERE oi.product_id = ?
    AND o.state NOT IN ('canceled', 'closed')
)


9. Product Viewed

Class: Maho_CustomerSegmentation_Model_Segment_Condition_Product_Viewed

Requires: report_viewed_product_index table (product view tracking)

SQL Pattern:

WHERE e.entity_id IN (
    SELECT customer_id FROM report_viewed_product_index
    WHERE product_id = ?
    AND added_at > DATE_SUB(NOW(), INTERVAL 7 DAY)
)


10. Product in Wishlist

Class: Maho_CustomerSegmentation_Model_Segment_Condition_Product_Wishlist

SQL Pattern:

WHERE e.entity_id IN (
    SELECT w.customer_id
    FROM wishlist w
    JOIN wishlist_item wi ON w.wishlist_id = wi.wishlist_id
    WHERE wi.product_id = ?
)


11. Combine Conditions

Class: Maho_CustomerSegmentation_Model_Segment_Condition_Combine

Aggregators: - all: AND logic (all conditions must match) - any: OR logic (at least one condition must match)

SQL Pattern:

// AND: (condition1 AND condition2 AND condition3)
WHERE (sql_fragment_1) AND (sql_fragment_2) AND (sql_fragment_3)

// OR: (condition1 OR condition2 OR condition3)
WHERE (sql_fragment_1) OR (sql_fragment_2) OR (sql_fragment_3)


12. Time-based Conditions

Class: Maho_CustomerSegmentation_Model_Segment_Condition_Customer_Timebased

Special calculated attributes: - Days since registration - Days until birthday - Days since last order

SQL Patterns:

// Days since registration
WHERE DATEDIFF(NOW(), e.created_at) > 30

// Days until birthday (handles year rollover)
WHERE DATEDIFF(
    DATE_ADD(
        DATE(e.dob),
        INTERVAL (YEAR(NOW()) - YEAR(e.dob) +
            IF(DAYOFYEAR(NOW()) > DAYOFYEAR(e.dob), 1, 0)
        ) YEAR
    ),
    NOW()
) = 0  -- Birthday is today

Code Examples

Creating a Segment Programmatically

$segment = Mage::getModel('customersegmentation/segment');

// Basic configuration
$segment->setName('VIP Customers - $1000+ Lifetime')
    ->setDescription('High-value customers for exclusive offers')
    ->setIsActive(1)
    ->setWebsiteIds('1,2,3')  // Comma-separated
    ->setCustomerGroupIds('1,2')  // General, Wholesale
    ->setRefreshMode(Maho_CustomerSegmentation_Model_Segment::MODE_AUTO)
    ->setPriority(10);

// Build condition tree
$conditions = [
    'type' => 'customersegmentation/segment_condition_combine',
    'aggregator' => 'all',  // AND
    'value' => '1',
    'conditions' => [
        [
            'type' => 'customersegmentation/segment_condition_customer_clv',
            'attribute' => 'lifetime_sales',
            'operator' => '>',
            'value' => '1000',
        ],
        [
            'type' => 'customersegmentation/segment_condition_order_attributes',
            'attribute' => 'number_of_orders',
            'operator' => '>',
            'value' => '5',
        ],
        [
            'type' => 'customersegmentation/segment_condition_customer_newsletter',
            'attribute' => 'subscriber_status',
            'operator' => '==',
            'value' => Mage_Newsletter_Model_Subscriber::STATUS_SUBSCRIBED,
        ],
    ],
];

// Set conditions
$segment->getConditions()->loadArray($conditions);
$segment->setConditionsSerialized(serialize($conditions));

// Save
$segment->save();

// Initial refresh
$segment->refreshCustomers();

Checking Customer Segment Membership

// Method 1: Using cached membership (fast)
$segmentId = 1;
$customerId = 123;
$websiteId = 1;

$resource = Mage::getResourceModel('customersegmentation/segment');
$isInSegment = $resource->isCustomerInSegment($segmentId, $customerId, $websiteId);

if ($isInSegment) {
    echo "Customer is in segment";
}
// Method 2: Real-time validation (slower, but always accurate)
$segment = Mage::getModel('customersegmentation/segment')->load($segmentId);
$customer = Mage::getModel('customer/customer')->load($customerId);

if ($segment->validateCustomer($customer, $websiteId)) {
    echo "Customer matches segment conditions";
}

Getting All Customers in a Segment

$segment = Mage::getModel('customersegmentation/segment')->load($segmentId);

// Get customer IDs (from cache)
$customerIds = $segment->getMatchingCustomerIds();

// Load full customer objects
$customers = Mage::getResourceModel('customer/customer_collection')
    ->addFieldToFilter('entity_id', ['in' => $customerIds])
    ->addNameToSelect()
    ->addAttributeToSelect('email');

foreach ($customers as $customer) {
    echo "{$customer->getName()} - {$customer->getEmail()}\n";
}

Manually Refreshing a Segment

$segment = Mage::getModel('customersegmentation/segment')->load($segmentId);

try {
    $segment->refreshCustomers();
    echo "Segment refreshed: {$segment->getMatchedCustomersCount()} customers matched\n";
} catch (Exception $e) {
    echo "Refresh failed: " . $e->getMessage() . "\n";
}

Batch Processing Segments

$collection = Mage::getResourceModel('customersegmentation/segment_collection')
    ->addIsActiveFilter()
    ->addAutoRefreshFilter()
    ->addNeedsRefreshFilter(24);  // 24 hours since last refresh

foreach ($collection as $segment) {
    try {
        $startTime = microtime(true);
        $segment->refreshCustomers();
        $duration = microtime(true) - $startTime;

        Mage::log(sprintf(
            'Refreshed segment %s (%d): %d customers in %.2f seconds',
            $segment->getName(),
            $segment->getId(),
            $segment->getMatchedCustomersCount(),
            $duration
        ));
    } catch (Exception $e) {
        Mage::logException($e);
    }
}

Creating Custom Condition Type

class My_Module_Model_Segment_Condition_Custom
    extends Maho_CustomerSegmentation_Model_Segment_Condition_Abstract
{
    protected $_inputType = 'string';

    public function __construct()
    {
        parent::__construct();
        $this->setType('mymodule/segment_condition_custom');
        $this->setValue(null);
    }

    public function getNewChildSelectOptions(): array
    {
        return [
            'value' => $this->getType(),
            'label' => Mage::helper('mymodule')->__('Custom Condition'),
        ];
    }

    public function loadAttributeOptions(): self
    {
        $attributes = [
            'my_custom_field' => Mage::helper('mymodule')->__('My Custom Field'),
            'another_field' => Mage::helper('mymodule')->__('Another Field'),
        ];

        $this->setAttributeOption($attributes);
        return $this;
    }

    public function getConditionsSql(
        \Maho\Db\Adapter\AdapterInterface $adapter,
        ?int $websiteId = null
    ): string|false {
        $attribute = $this->getAttribute();
        $operator = $this->getMappedSqlOperator();
        $value = $this->getValue();

        // Your custom SQL logic
        $resource = Mage::getSingleton('core/resource');
        $customTable = $resource->getTableName('my_custom_table');

        return sprintf(
            "e.entity_id IN (SELECT customer_id FROM %s WHERE %s %s %s)",
            $adapter->quoteIdentifier($customTable),
            $adapter->quoteIdentifier($attribute),
            $operator,
            $adapter->quote($value)
        );
    }
}

Register in config.xml:

<customersegmentation>
    <segment>
        <conditions>
            <mymodule_custom>
                <class>mymodule/segment_condition_custom</class>
            </mymodule_custom>
        </conditions>
    </segment>
</customersegmentation>

Observing Segment Refresh Events

class My_Module_Model_Observer
{
    public function onSegmentRefreshBefore(Varien_Event_Observer $observer)
    {
        $segment = $observer->getSegment();
        Mage::log("Starting refresh for segment: " . $segment->getName());
    }

    public function onSegmentRefreshAfter(Varien_Event_Observer $observer)
    {
        $segment = $observer->getSegment();
        $matchedCustomers = $observer->getMatchedCustomers();

        Mage::log(sprintf(
            "Segment %s refreshed: %d customers matched",
            $segment->getName(),
            count($matchedCustomers)
        ));

        // Sync to external system
        $this->syncToExternalCrm($segment, $matchedCustomers);
    }
}

config.xml:

<events>
    <customer_segment_refresh_before>
        <observers>
            <mymodule_segment_refresh>
                <class>mymodule/observer</class>
                <method>onSegmentRefreshBefore</method>
            </mymodule_segment_refresh>
        </observers>
    </customer_segment_refresh_before>

    <customer_segment_refresh_after>
        <observers>
            <mymodule_segment_refresh_after>
                <class>mymodule/observer</class>
                <method>onSegmentRefreshAfter</method>
            </mymodule_segment_refresh_after>
        </observers>
    </customer_segment_refresh_after>
</events>

Testing

Unit Testing Segment Conditions

class Maho_CustomerSegmentation_Test_Model_SegmentTest extends PHPUnit\Framework\TestCase
{
    public function testVipSegmentMatching()
    {
        // Create test customer
        $customer = $this->createCustomerWithOrders(3, 1500);  // 3 orders, $1500 total

        // Create VIP segment
        $segment = $this->createVipSegment();

        // Test matching
        $this->assertTrue(
            $segment->validateCustomer($customer, 1),
            'Customer with $1500 lifetime sales should match VIP segment'
        );
    }

    public function testCartAbandonmentSegment()
    {
        // Create customer with abandoned cart
        $customer = $this->createCustomer();
        $quote = $this->createAbandonedCart($customer, 250, 2);  // $250 cart, 2 hours old

        // Create segment
        $segment = $this->createCartAbandonmentSegment(100, 1);  // $100+ cart, 1+ hour

        // Refresh and check
        $segment->refreshCustomers();

        $this->assertTrue(
            $this->isCustomerInSegment($segment, $customer),
            'Customer with abandoned cart should be in segment'
        );
    }

    protected function createVipSegment()
    {
        $segment = Mage::getModel('customersegmentation/segment');
        $conditions = [
            'type' => 'customersegmentation/segment_condition_combine',
            'aggregator' => 'all',
            'value' => '1',
            'conditions' => [
                [
                    'type' => 'customersegmentation/segment_condition_customer_clv',
                    'attribute' => 'lifetime_sales',
                    'operator' => '>',
                    'value' => '1000',
                ],
            ],
        ];

        $segment->setName('Test VIP Segment')
               ->setIsActive(1)
               ->setWebsiteIds('1')
               ->getConditions()->loadArray($conditions);

        return $segment->save();
    }
}

Integration Testing

public function testSegmentRefreshPerformance()
{
    // Create segment with complex conditions
    $segment = $this->createComplexSegment();

    // Create 1000 test customers
    $customers = $this->createTestCustomers(1000);

    // Measure refresh time
    $startTime = microtime(true);
    $startMemory = memory_get_usage();

    $segment->refreshCustomers();

    $duration = microtime(true) - $startTime;
    $memoryUsed = memory_get_usage() - $startMemory;

    // Assert performance constraints
    $this->assertLessThan(5.0, $duration, 'Segment refresh should complete in <5 seconds');
    $this->assertLessThan(50 * 1024 * 1024, $memoryUsed, 'Memory usage should be <50MB');
}

Performance Optimization

SQL Query Optimization

Use EXPLAIN to analyze generated queries:

$segment = Mage::getModel('customersegmentation/segment')->load($segmentId);
$websiteId = 1;

// Get the generated SQL
$adapter = Mage::getSingleton('core/resource')->getConnection('core_read');
$sql = $segment->getResource()->createSelect($segment, $websiteId);

// Analyze
$explain = $adapter->fetchAll("EXPLAIN " . $sql);
print_r($explain);

Optimization Techniques:

  1. Add Indexes:

    -- Customer table
    CREATE INDEX idx_created_at ON customer_entity(created_at);
    CREATE INDEX idx_email ON customer_entity(email);
    CREATE INDEX idx_group_id ON customer_entity(group_id);
    
    -- Order table
    CREATE INDEX idx_customer_created ON sales_flat_order(customer_id, created_at);
    CREATE INDEX idx_customer_total ON sales_flat_order(customer_id, grand_total);
    
    -- Quote table
    CREATE INDEX idx_customer_active ON sales_flat_quote(customer_id, is_active);
    CREATE INDEX idx_customer_updated ON sales_flat_quote(customer_id, updated_at);
    

  2. Avoid N+1 Queries:

    // Bad: Loads each customer individually
    foreach ($customerIds as $customerId) {
        $customer = Mage::getModel('customer/customer')->load($customerId);
        // Process...
    }
    
    // Good: Batch load
    $customers = Mage::getResourceModel('customer/customer_collection')
        ->addFieldToFilter('entity_id', ['in' => $customerIds])
        ->addAttributeToSelect('*');
    
    foreach ($customers as $customer) {
        // Process...
    }
    

  3. Cache Segment Results:

    // Segment membership is automatically cached in customer_segment_customer
    // Refresh only when needed
    $segment->setRefreshMode(Maho_CustomerSegmentation_Model_Segment::MODE_MANUAL);
    

Memory Optimization

For large segments (100k+ customers):

public function processLargeSegment($segmentId)
{
    $segment = Mage::getModel('customersegmentation/segment')->load($segmentId);
    $customerIds = $segment->getMatchingCustomerIds();

    // Process in batches
    $batchSize = 1000;
    $batches = array_chunk($customerIds, $batchSize);

    foreach ($batches as $batch) {
        $this->processBatch($batch);
        gc_collect_cycles();  // Free memory
    }
}

Caching Strategies

// Cache segment collection
$cacheKey = "segments_active_website_" . $websiteId;
$segments = Mage::app()->getCache()->load($cacheKey);

if (!$segments) {
    $collection = Mage::getResourceModel('customersegmentation/segment_collection')
        ->addIsActiveFilter()
        ->addWebsiteFilter($websiteId);

    $segments = serialize($collection->getAllIds());

    Mage::app()->getCache()->save(
        $segments,
        $cacheKey,
        [Maho_CustomerSegmentation_Model_Segment::CACHE_TAG],
        3600  // 1 hour
    );
}

$segmentIds = unserialize($segments);

Debugging

Enable Detailed Logging

// In your condition class
public function getConditionsSql(
    \Maho\Db\Adapter\AdapterInterface $adapter,
    ?int $websiteId = null
): string|false {
    $sql = $this->buildSqlCondition($adapter, $field, $operator, $value);

    // Log generated SQL
    Mage::log(
        sprintf('Condition SQL [%s %s %s]: %s',
            $this->getAttribute(),
            $this->getOperator(),
            $this->getValue(),
            $sql
        ),
        Mage::LOG_DEBUG,
        'segment_debug.log'
    );

    return $sql;
}

Debug Segment Matching

$segment = Mage::getModel('customersegmentation/segment')->load($segmentId);
$customer = Mage::getModel('customer/customer')->load($customerId);

// Get condition tree
$conditions = $segment->getConditions();
print_r($conditions->asArray());

// Test individual conditions
$result = $conditions->validate($customer);
echo "Validation result: " . ($result ? 'MATCH' : 'NO MATCH') . "\n";

// Get generated SQL
$sql = $segment->getResource()->createSelect($segment, $websiteId);
echo "Generated SQL:\n" . $sql . "\n";

// Execute and see results
$adapter = Mage::getSingleton('core/resource')->getConnection('core_read');
$matchedIds = $adapter->fetchCol($sql);
echo "Matched " . count($matchedIds) . " customers\n";

Common Debug Queries

-- Check segment membership
SELECT s.name, COUNT(*) as customer_count
FROM customer_segment s
LEFT JOIN customer_segment_customer sc ON s.segment_id = sc.segment_id
WHERE s.is_active = 1
GROUP BY s.segment_id;

-- Find segments a customer is in
SELECT s.segment_id, s.name, sc.added_at
FROM customer_segment s
JOIN customer_segment_customer sc ON s.segment_id = sc.segment_id
WHERE sc.customer_id = 123;

-- Segments needing refresh
SELECT segment_id, name, last_refresh_at
FROM customer_segment
WHERE is_active = 1
AND (last_refresh_at IS NULL
     OR last_refresh_at < DATE_SUB(NOW(), INTERVAL 24 HOUR));

-- Segment refresh history
SELECT segment_id, refresh_status,
       matched_customers_count, last_refresh_at
FROM customer_segment
ORDER BY last_refresh_at DESC;

API Reference

Model Methods

Maho_CustomerSegmentation_Model_Segment

// Segment management
public function refreshCustomers(): self
public function getMatchingCustomerIds(): array
public function validateCustomer(Mage_Customer_Model_Customer $customer, int $websiteId): bool
public function isCustomerInSegment(int $customerId, ?int $websiteId = null): bool

// Condition management
public function getConditions(): Maho_CustomerSegmentation_Model_Segment_Condition_Combine
public function setConditions($conditions): self

// Email automation (see Email Automation dev guide)
public function hasEmailAutomation(): bool
public function startEmailSequence(int $customerId, string $triggerType): void
public function getEmailSequences(): Maho_CustomerSegmentation_Model_Resource_EmailSequence_Collection

// Status & modes
public const STATUS_PENDING    = 'pending'
public const STATUS_PROCESSING = 'processing'
public const STATUS_COMPLETED  = 'completed'
public const STATUS_ERROR      = 'error'

public const MODE_AUTO   = 'auto'
public const MODE_MANUAL = 'manual'

public const EMAIL_TRIGGER_NONE  = 'none'
public const EMAIL_TRIGGER_ENTER = 'enter'
public const EMAIL_TRIGGER_EXIT  = 'exit'

Maho_CustomerSegmentation_Model_Resource_Segment

// Membership operations
public function isCustomerInSegment(int $segmentId, int $customerId, int $websiteId): bool
public function updateCustomerMembership(Maho_CustomerSegmentation_Model_Segment $segment, array $customerIds): int

// SQL generation
public function createSelect(Maho_CustomerSegmentation_Model_Segment $segment, int $websiteId): \Maho\Db\Select

// Batch operations
public function deleteCustomerFromSegment(int $customerId, int $segmentId): int
public function deleteSegmentCustomers(int $segmentId): int

Maho_CustomerSegmentation_Model_Segment_Condition_Abstract

// Condition definition
abstract public function getConditionsSql(\Maho\Db\Adapter\AdapterInterface $adapter, ?int $websiteId = null): string|false
abstract public function loadAttributeOptions(): self

// Value handling
public function getValueElementType(): string
public function getValueSelectOptions(): array
public function getInputType(): string

// Operators
public function getOperatorSelectOptions(): array
public function getMappedSqlOperator(): string

// SQL helpers
protected function buildSqlCondition(\Maho\Db\Adapter\AdapterInterface $adapter, string $field, string $operator, mixed $value): string
protected function buildDaysSinceCondition(\Maho\Db\Adapter\AdapterInterface $adapter, string $dateField, string $operator, int $days): string

Collection Methods

Maho_CustomerSegmentation_Model_Resource_Segment_Collection

// Filters
public function addIsActiveFilter(): self
public function addWebsiteFilter($websiteId): self
public function addAutoRefreshFilter(): self
public function addNeedsRefreshFilter(int $hoursOld = 24): self

// Sorting
public function addPriorityOrder(string $direction = 'DESC'): self

Configuration Reference

System Configuration

// Segment refresh cron schedule
customer/customer_segments/cron_schedule  // Default: "0 5 * * *" (5 AM daily)

// Email automation
customer_segmentation/email_automation/enabled  // 1 = enabled, 0 = disabled

Cron Configuration

<crontab>
    <jobs>
        <customersegmentation_refresh_segments>
            <schedule>
                <config_path>customer/customer_segments/cron_schedule</config_path>
            </schedule>
            <run>
                <model>customersegmentation/cron::refreshSegments</model>
            </run>
        </customersegmentation_refresh_segments>
    </jobs>
</crontab>

Migration & Upgrades

Database Migration Script

// In sql/maho_customersegmentation_setup/upgrade-X.X.X-Y.Y.Y.php

$installer = $this;
$installer->startSetup();

// Add new condition-specific table
$table = $installer->getConnection()
    ->newTable($installer->getTable('my_condition_data'))
    ->addColumn('id', Maho\Db\Ddl\Table::TYPE_INTEGER, null, [
        'identity' => true,
        'unsigned' => true,
        'nullable' => false,
        'primary'  => true,
    ], 'ID')
    ->addColumn('customer_id', Maho\Db\Ddl\Table::TYPE_INTEGER, null, [
        'unsigned' => true,
        'nullable' => false,
    ], 'Customer ID')
    ->addColumn('custom_value', Maho\Db\Ddl\Table::TYPE_DECIMAL, '12,4', [
        'nullable' => false,
    ], 'Custom Value')
    ->addIndex(
        $installer->getIdxName('my_condition_data', ['customer_id']),
        ['customer_id']
    )
    ->addForeignKey(
        $installer->getFkName('my_condition_data', 'customer_id', 'customer_entity', 'entity_id'),
        'customer_id',
        $installer->getTable('customer_entity'),
        'entity_id',
        Maho\Db\Ddl\Table::ACTION_CASCADE
    )
    ->setComment('Custom Condition Data');

$installer->getConnection()->createTable($table);

$installer->endSetup();

Best Practices for Developers

1. Condition Design

Do: - Use indexed fields for WHERE clauses - Minimize subquery nesting - Use EXISTS instead of IN for large result sets - Cache expensive calculations

Don't: - Use LIKE '%pattern%' (kills indexes) - Join large tables without indexes - Create Cartesian products

2. Error Handling

try {
    $segment->refreshCustomers();
} catch (Exception $e) {
    Mage::logException($e);
    Mage::log(
        "Segment refresh failed: " . $e->getMessage(),
        Mage::LOG_ERROR,
        'customer_segmentation.log'
    );

    // Update segment status
    $segment->setRefreshStatus(Maho_CustomerSegmentation_Model_Segment::STATUS_ERROR);
    $segment->save();
}

3. Type Safety

// Use strict types
declare(strict_types=1);

// Type hint parameters and return values
public function getConditionsSql(
    \Maho\Db\Adapter\AdapterInterface $adapter,
    ?int $websiteId = null
): string|false {
    // Implementation
}

4. Code Documentation

/**
 * Get SQL condition for lifetime sales amount
 *
 * Generates a subquery that calculates total sales per customer
 * from sales_flat_order table, excluding canceled and closed orders.
 *
 * Example SQL:
 * WHERE e.entity_id IN (
 *     SELECT customer_id FROM (...)
 *     WHERE total > 1000
 * )
 *
 * @param AdapterInterface $adapter Database adapter
 * @param int|null $websiteId Limit to specific website stores
 * @return string|false SQL WHERE clause or false on error
 */
public function getConditionsSql(
    \Maho\Db\Adapter\AdapterInterface $adapter,
    ?int $websiteId = null
): string|false {
    // Implementation
}

Further Reading

Support

Questions or found a bug?


Build powerful customer segmentation with Maho! 🎯