Skip to content

Events Reference

The Craft Search with Elastic plugin provides a comprehensive event system that allows developers to extend and customize the plugin's functionality. Events are triggered at key points during search, indexing, and management operations.

Event Registration

Events follow Craft's standard event pattern. Register event listeners in your module, plugin, or bootstrap file:

php
use yii\base\Event;
use pennebaker\searchwithelastic\services\ElasticsearchService;
use pennebaker\searchwithelastic\events\SearchEvent;

Event::on(
    ElasticsearchService::class,
    ElasticsearchService::EVENT_BEFORE_SEARCH,
    function(SearchEvent $event) {
        // Your event handler code
    }
);

Search Events

SearchEvent

Namespace: pennebaker\searchwithelastic\events\SearchEvent

Triggered when search queries are executed against Elasticsearch.

Properties

PropertyTypeDescription
$querystring|arrayThe search query (can be modified by event handlers)
$paramsarrayAdditional search parameters
$siteId?intThe site ID where the search is being performed
$skipDefaultSearchboolWhether the default search execution should be skipped

Events

Event ConstantServiceTrigger Point
EVENT_BEFORE_SEARCHElasticsearchServiceBefore executing a search query
EVENT_AFTER_SEARCHElasticsearchServiceAfter executing a search query

Usage Examples

Modify search query before execution:

php
use yii\base\Event;
use pennebaker\searchwithelastic\services\ElasticsearchService;
use pennebaker\searchwithelastic\events\SearchEvent;

Event::on(
    ElasticsearchService::class,
    ElasticsearchService::EVENT_BEFORE_SEARCH,
    function(SearchEvent $event) {
        // Add site-specific boost
        if ($event->siteId === 1) {
            $event->params['boost'] = 1.5;
        }
        
        // Sanitize query
        $event->query = trim(strip_tags($event->query));
        
        // Add automatic filters
        if (!isset($event->params['filters'])) {
            $event->params['filters'] = [];
        }
        $event->params['filters']['status'] = 'live';
    }
);

Custom search analytics:

php
Event::on(
    ElasticsearchService::class,
    ElasticsearchService::EVENT_AFTER_SEARCH,
    function(SearchEvent $event) {
        // Log search analytics
        $resultCount = count($event->params['results'] ?? []);
        
        \Craft::info("Search performed: '{$event->query}' on site {$event->siteId}, {$resultCount} results", 'search-analytics');
        
        // Store in custom analytics table
        (new \craft\db\Query())
            ->createCommand()
            ->insert('{{%search_analytics}}', [
                'query' => $event->query,
                'siteId' => $event->siteId,
                'resultCount' => $resultCount,
                'dateCreated' => \craft\helpers\Db::prepareDateForDb(new \DateTime())
            ])
            ->execute();
    }
);

ConnectionTestEvent

Namespace: pennebaker\searchwithelastic\events\ConnectionTestEvent

Triggered when testing connections to Elasticsearch.

Properties

PropertyTypeDescription
$endpointstringThe Elasticsearch endpoint being tested
$configarrayConnection configuration (passwords masked)
$result?boolThe test result (null initially, set during test)
$errorMessage?stringError message if test failed
$skipDefaultTestboolWhether to skip the default test

Events

Event ConstantServiceTrigger Point
EVENT_BEFORE_CONNECTION_TESTElasticsearchServiceBefore testing connection
EVENT_AFTER_CONNECTION_TESTElasticsearchServiceAfter testing connection

Usage Examples

Custom connection validation:

php
use pennebaker\searchwithelastic\events\ConnectionTestEvent;

Event::on(
    ElasticsearchService::class,
    ElasticsearchService::EVENT_BEFORE_CONNECTION_TEST,
    function(ConnectionTestEvent $event) {
        // Perform custom validation
        if (strpos($event->endpoint, 'localhost') !== false && \Craft::$app->getConfig()->general->devMode === false) {
            $event->result = false;
            $event->errorMessage = 'Localhost connections not allowed in production';
            $event->skipDefaultTest = true;
        }
    }
);

Indexing Events

IndexElementEvent

Namespace: pennebaker\searchwithelastic\events\IndexElementEvent

Triggered when indexing or removing elements from Elasticsearch.

Properties

PropertyTypeDescription
$elementElementThe element being indexed or removed
$documentDataarrayThe document data that will be indexed (modifiable)
$indexNamestringThe Elasticsearch index name
$isNewboolWhether the element is being newly created
$skipDefaultOperationboolWhether to skip the default operation

Events

Event ConstantServiceTrigger Point
EVENT_BEFORE_INDEX_ELEMENTElementIndexerServiceBefore indexing an element
EVENT_AFTER_INDEX_ELEMENTElementIndexerServiceAfter indexing an element
EVENT_BEFORE_REMOVE_ELEMENTElementIndexerServiceBefore removing an element
EVENT_AFTER_REMOVE_ELEMENTElementIndexerServiceAfter removing an element

Usage Examples

Add custom fields to index:

php
use pennebaker\searchwithelastic\services\ElementIndexerService;
use pennebaker\searchwithelastic\events\IndexElementEvent;

Event::on(
    ElementIndexerService::class,
    ElementIndexerService::EVENT_BEFORE_INDEX_ELEMENT,
    function(IndexElementEvent $event) {
        $element = $event->element;
        
        // Add custom metadata
        $event->documentData['customCategory'] = $element->getFieldValue('category')?->title ?? 'Uncategorized';
        $event->documentData['authorName'] = $element->getAuthor()?->fullName ?? 'Anonymous';
        $event->documentData['wordCount'] = str_word_count(strip_tags($event->documentData['content'] ?? ''));
        
        // Add computed fields
        if ($element instanceof \craft\elements\Entry) {
            $event->documentData['entryAge'] = time() - $element->postDate->getTimestamp();
            $event->documentData['isRecent'] = $event->documentData['entryAge'] < (30 * 24 * 60 * 60); // 30 days
        }
        
        // Add tags from related elements
        $tags = [];
        foreach ($element->getFieldValue('relatedTags')->all() as $tag) {
            $tags[] = $tag->title;
        }
        $event->documentData['tags'] = $tags;
    }
);

Custom indexing logic:

php
Event::on(
    ElementIndexerService::class,
    ElementIndexerService::EVENT_BEFORE_INDEX_ELEMENT,
    function(IndexElementEvent $event) {
        // Skip indexing draft entries in production
        if (!Craft::$app->getConfig()->general->devMode && $event->element->getIsDraft()) {
            $event->skipDefaultOperation = true;
            return;
        }
        
        // Custom privacy handling
        if ($event->element->getFieldValue('isPrivate')) {
            $event->documentData['content'] = '[Private content]';
            $event->documentData['searchable'] = false;
        }
    }
);

Post-indexing operations:

php
Event::on(
    ElementIndexerService::class,
    ElementIndexerService::EVENT_AFTER_INDEX_ELEMENT,
    function(IndexElementEvent $event) {
        // Invalidate related caches
        \Craft::$app->getCache()->delete("search_cache_site_{$event->element->siteId}");
        
        // Update related counters
        if ($event->element instanceof \craft\elements\Entry) {
            $this->updateCategoryCounter($event->element->getFieldValue('category'));
        }
        
        // Send webhook notification
        $this->sendWebhookNotification('element_indexed', [
            'elementId' => $event->element->id,
            'elementType' => get_class($event->element),
            'siteId' => $event->element->siteId,
            'indexName' => $event->indexName
        ]);
    }
);

ContentExtractionEvent

Namespace: pennebaker\searchwithelastic\events\ContentExtractionEvent

Triggered when extracting indexable content from HTML.

Properties

PropertyTypeDescription
$rawContentstringThe raw HTML content to extract from
$extractedContentstringThe extracted content (modifiable)
$element?ElementThe element being indexed (optional context)
$skipDefaultExtractionboolWhether to skip the default extraction

Events

Event ConstantServiceTrigger Point
EVENT_CONTENT_EXTRACTIONElementIndexerServiceDuring content extraction from HTML

Usage Examples

Custom content extraction:

php
use pennebaker\searchwithelastic\services\ElementIndexerService;
use pennebaker\searchwithelastic\events\ContentExtractionEvent;

Event::on(
    ElementIndexerService::class,
    ElementIndexerService::EVENT_CONTENT_EXTRACTION,
    function(ContentExtractionEvent $event) {
        // Custom extraction for specific element types
        if ($event->element instanceof \craft\elements\Entry && $event->element->section->handle === 'products') {
            // Extract product-specific information
            $dom = new \DOMDocument();
            @$dom->loadHTML($event->rawContent);
            
            $xpath = new \DOMXPath($dom);
            
            // Extract product specifications
            $specs = $xpath->query('//div[@class="product-specs"]//text()');
            $specText = '';
            foreach ($specs as $spec) {
                $specText .= trim($spec->nodeValue) . ' ';
            }
            
            // Extract reviews
            $reviews = $xpath->query('//div[@class="reviews"]//p/text()');
            $reviewText = '';
            foreach ($reviews as $review) {
                $reviewText .= trim($review->nodeValue) . ' ';
            }
            
            $event->extractedContent = trim($specText . ' ' . $reviewText);
            $event->skipDefaultExtraction = true;
        }
    }
);

Content filtering and enhancement:

php
Event::on(
    ElementIndexerService::class,
    ElementIndexerService::EVENT_CONTENT_EXTRACTION,
    function(ContentExtractionEvent $event) {
        // Remove unwanted content
        $event->extractedContent = preg_replace('/\bPassword:\s*\S+/i', '[Password Removed]', $event->extractedContent);
        $event->extractedContent = preg_replace('/\b\d{4}[-\s]\d{4}[-\s]\d{4}[-\s]\d{4}\b/', '[Card Number Removed]', $event->extractedContent);
        
        // Enhance content with metadata
        if ($event->element) {
            $author = $event->element->getAuthor();
            if ($author) {
                $event->extractedContent .= " Author: {$author->fullName}";
            }
            
            // Add field values to searchable content
            $category = $event->element->getFieldValue('category');
            if ($category) {
                $event->extractedContent .= " Category: {$category->title}";
            }
        }
    }
);

Management Events

IndexManagementEvent

Namespace: pennebaker\searchwithelastic\events\IndexManagementEvent

Triggered during index management operations.

Properties

PropertyTypeDescription
$siteIdintThe site ID
$indexNamestringThe Elasticsearch index name
$indexConfigarrayThe index configuration (modifiable)
$operationstringThe operation type ('create', 'delete', 'recreate')
$indexExistedboolWhether the index existed before the operation
$skipDefaultOperationboolWhether to skip the default operation

Events

Event ConstantServiceTrigger Point
EVENT_BEFORE_CREATE_INDEXIndexManagementServiceBefore creating an index
EVENT_AFTER_CREATE_INDEXIndexManagementServiceAfter creating an index
EVENT_BEFORE_DELETE_INDEXIndexManagementServiceBefore deleting an index
EVENT_AFTER_DELETE_INDEXIndexManagementServiceAfter deleting an index
EVENT_BEFORE_RECREATE_INDEXIndexManagementServiceBefore recreating an index
EVENT_AFTER_RECREATE_INDEXIndexManagementServiceAfter recreating an index

Usage Examples

Custom index configuration:

php
use pennebaker\searchwithelastic\services\IndexManagementService;
use pennebaker\searchwithelastic\events\IndexManagementEvent;

Event::on(
    IndexManagementService::class,
    IndexManagementService::EVENT_BEFORE_CREATE_INDEX,
    function(IndexManagementEvent $event) {
        // Add custom analyzers
        $event->indexConfig['settings']['analysis']['analyzer']['custom_html'] = [
            'type' => 'custom',
            'tokenizer' => 'standard',
            'char_filter' => ['html_strip'],
            'filter' => ['lowercase', 'stop']
        ];
        
        // Add custom field mappings
        $event->indexConfig['mappings']['properties']['customField'] = [
            'type' => 'text',
            'analyzer' => 'custom_html'
        ];
        
        // Increase shards for high-traffic sites
        if ($event->siteId === 1) {
            $event->indexConfig['settings']['number_of_shards'] = 3;
            $event->indexConfig['settings']['number_of_replicas'] = 1;
        }
    }
);

Index lifecycle hooks:

php
Event::on(
    IndexManagementService::class,
    IndexManagementService::EVENT_AFTER_CREATE_INDEX,
    function(IndexManagementEvent $event) {
        // Set up index aliases
        $connection = \pennebaker\searchwithelastic\SearchWithElastic::getConnection();
        $aliasName = "craft-site-{$event->siteId}";
        
        $connection->post(['_aliases'], [], json_encode([
            'actions' => [
                ['add' => ['index' => $event->indexName, 'alias' => $aliasName]]
            ]
        ]));
        
        // Log index creation
        \Craft::info("Created index {$event->indexName} for site {$event->siteId}", 'elasticsearch');
    }
);

Queue Events

QueueEvent

Namespace: pennebaker\searchwithelastic\events\QueueEvent

Triggered during queue management operations.

Properties

PropertyTypeDescription
$elementModelsarrayArray of IndexableElementModel instances
$operationstringThe operation type ('enqueue', 'clear')
$jobIds?arrayArray of job IDs (set after enqueue)
$skipDefaultOperationboolWhether to skip the default operation

Events

Event ConstantServiceTrigger Point
EVENT_BEFORE_ENQUEUE_JOBSReindexQueueManagementServiceBefore enqueuing jobs
EVENT_AFTER_ENQUEUE_JOBSReindexQueueManagementServiceAfter enqueuing jobs
EVENT_BEFORE_CLEAR_QUEUEReindexQueueManagementServiceBefore clearing queue
EVENT_AFTER_CLEAR_QUEUEReindexQueueManagementServiceAfter clearing queue

Usage Examples

Custom queue management:

php
use pennebaker\searchwithelastic\services\ReindexQueueManagementService;
use pennebaker\searchwithelastic\events\QueueEvent;

Event::on(
    ReindexQueueManagementService::class,
    ReindexQueueManagementService::EVENT_BEFORE_ENQUEUE_JOBS,
    function(QueueEvent $event) {
        // Filter out certain element types during peak hours
        if (date('G') >= 9 && date('G') <= 17) { // Business hours
            $event->elementModels = array_filter($event->elementModels, function($model) {
                return $model->type !== \craft\elements\Asset::class;
            });
        }
        
        // Prioritize certain element types
        usort($event->elementModels, function($a, $b) {
            $priority = [
                \craft\elements\Entry::class => 1,
                \craft\elements\Category::class => 2,
                \craft\elements\Asset::class => 3,
            ];
            
            return ($priority[$a->type] ?? 999) <=> ($priority[$b->type] ?? 999);
        });
    }
);

Model Events

ModelEvent

Namespace: pennebaker\searchwithelastic\events\ModelEvent

Triggered during model creation operations.

Properties

PropertyTypeDescription
$elementElementThe Craft element
$siteIdintThe site ID
$modelIndexableElementModelThe indexable element model (modifiable)
$skipDefaultCreationboolWhether to skip default model creation

Events

Event ConstantServiceTrigger Point
EVENT_BEFORE_CREATE_MODELModelServiceBefore creating a model
EVENT_AFTER_CREATE_MODELModelServiceAfter creating a model

Record Events

RecordEvent

Namespace: pennebaker\searchwithelastic\events\RecordEvent

Triggered during record operations.

Properties

PropertyTypeDescription
$elementElementThe element
$documentId?stringThe document ID
$operationstringThe operation type ('save', 'delete')
$resultboolThe operation result
$skipDefaultOperationboolWhether to skip the default operation

Events

Event ConstantServiceTrigger Point
EVENT_BEFORE_SAVE_RECORDRecordServiceBefore saving a record
EVENT_AFTER_SAVE_RECORDRecordServiceAfter saving a record
EVENT_BEFORE_DELETE_RECORDRecordServiceBefore deleting a record
EVENT_AFTER_DELETE_RECORDRecordServiceAfter deleting a record

Query Events

QueryEvent

Namespace: pennebaker\searchwithelastic\events\QueryEvent

Triggered during query building operations.

Properties

PropertyTypeDescription
$siteIdintThe site ID
$elementTypestringThe element type class name
$querymixedThe query builder instance (modifiable)

Events

Event ConstantServiceTrigger Point
EVENT_BEFORE_BUILD_QUERYQueryServiceBefore building a query
EVENT_AFTER_BUILD_QUERYQueryServiceAfter building a query

Usage Example

Custom query modifications:

php
use pennebaker\searchwithelastic\services\QueryService;
use pennebaker\searchwithelastic\events\QueryEvent;

Event::on(
    QueryService::class,
    QueryService::EVENT_BEFORE_BUILD_QUERY,
    function(QueryEvent $event) {
        // Add custom filtering for specific sites
        if ($event->siteId === 2 && $event->elementType === \craft\elements\Entry::class) {
            $event->query->section('news'); // Only news section for site 2
        }
        
        // Add date-based filtering
        if ($event->elementType === \craft\elements\Entry::class) {
            $event->query->postDate('>= ' . (new \DateTime('-1 year'))->format('Y-m-d'));
        }
    }
);

Best Practices

Event Handler Performance

Keep event handlers lightweight and fast:

php
// Good: Quick operations
Event::on(Service::class, Service::EVENT_NAME, function($event) {
    $event->data['customField'] = 'value';
});

// Avoid: Heavy operations in events
Event::on(Service::class, Service::EVENT_NAME, function($event) {
    // Don't do this - it will slow down indexing
    $heavyData = $this->performExpensiveApiCall();
    $event->data['heavyData'] = $heavyData;
});

Error Handling

Always include error handling in event listeners:

php
Event::on(Service::class, Service::EVENT_NAME, function($event) {
    try {
        // Your event handling code
        $event->data['processed'] = $this->processData($event->data);
    } catch (\Exception $e) {
        \Craft::error("Event handler failed: " . $e->getMessage(), 'search-plugin');
        // Don't re-throw - let the operation continue
    }
});

Conditional Processing

Use conditional logic to avoid unnecessary processing:

php
Event::on(Service::class, Service::EVENT_NAME, function($event) {
    // Skip processing for certain element types
    if (!$event->element instanceof \craft\elements\Entry) {
        return;
    }
    
    // Skip processing for disabled sites
    if (!$event->element->getSite()->enabled) {
        return;
    }
    
    // Your processing code here
});

Memory Management

For events that process many elements, be mindful of memory usage:

php
Event::on(Service::class, Service::EVENT_NAME, function($event) {
    // Process data
    $processedData = $this->processElement($event->element);
    $event->data = array_merge($event->data, $processedData);
    
    // Clear references to prevent memory leaks
    unset($processedData);
});