Lifecycle Callbacks
Master ObjectQuel's lifecycle callback system to execute custom business logic at specific points in an entity's persistence lifecycle, from automatic timestamps to complex validation and external system integration.
Understanding Lifecycle Callbacks
Lifecycle callbacks allow you to hook into the entity persistence process and execute custom logic at specific moments. ObjectQuel provides six lifecycle events that cover the complete entity lifecycle from creation to deletion:
<?php
namespace App\Entity;
use Quellabs\ObjectQuel\Annotations\Orm;
/**
* @Orm\Table(name="products")
* @Orm\LifecycleAware
*/
class ProductEntity {
/**
* @Orm\Column(name="created_at", type="datetime", nullable=true)
*/
private ?\DateTime $createdAt = null;
/**
* @Orm\Column(name="updated_at", type="datetime", nullable=true)
*/
private ?\DateTime $updatedAt = null;
/**
* Called before inserting a new entity
* @Orm\PrePersist
*/
public function onPrePersist(): void {
$this->createdAt = new \DateTime();
$this->updatedAt = new \DateTime();
}
/**
* Called after successfully inserting a new entity
* @Orm\PostPersist
*/
public function onPostPersist(): void {
// Send notification, trigger events, etc.
$this->notifyProductCreated();
}
/**
* Called before updating an existing entity
* @Orm\PreUpdate
*/
public function onPreUpdate(): void {
$this->updatedAt = new \DateTime();
}
/**
* Called after successfully updating an entity
* @Orm\PostUpdate
*/
public function onPostUpdate(): void {
// Clear caches, sync with external systems, etc.
$this->invalidateProductCache();
}
/**
* Called before deleting an entity
* @Orm\PreDelete
*/
public function onPreDelete(): void {
// Backup data, validate deletion constraints
$this->backupProductData();
}
/**
* Called after successfully deleting an entity
* @Orm\PostDelete
*/
public function onPostDelete(): void {
// Clean up related data, notify systems
$this->cleanupProductFiles();
}
}
Key Requirements:
- Entity must be annotated with
@Orm\LifecycleAware - Callback methods must be annotated with the appropriate lifecycle annotation
- Callback methods should not require parameters
- Callback methods should not return values
Available Lifecycle Events
@Orm\PrePersist - Before Entity Creation
Executed before inserting a new entity into the database. Perfect for setting default values, generating UUIDs, or performing validation:
/**
* @Orm\Table(name="users")
* @Orm\LifecycleAware
*/
class UserEntity {
/**
* @Orm\Column(name="user_id", type="string", limit=36, primary_key=true)
*/
private ?string $userId = null;
/**
* @Orm\Column(name="email", type="string", limit=255)
*/
private string $email;
/**
* @Orm\Column(name="status", type="string", limit=20)
*/
private string $status = 'pending';
/**
* @Orm\Column(name="email_verification_token", type="string", limit=64, nullable=true)
*/
private ?string $emailVerificationToken = null;
/**
* @Orm\PrePersist
*/
public function generateDefaults(): void {
// Generate UUID for new users
if ($this->userId === null) {
$this->userId = $this->generateUuid();
}
// Generate email verification token
$this->emailVerificationToken = bin2hex(random_bytes(32));
// Normalize email
$this->email = strtolower(trim($this->email));
}
/**
* @Orm\PrePersist
*/
public function validateNewUser(): void {
// Custom validation before persistence
if (!filter_var($this->email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Invalid email address');
}
if (strlen($this->email) > 255) {
throw new \InvalidArgumentException('Email too long');
}
}
}
@Orm\PostPersist - After Entity Creation
Executed after successfully inserting a new entity. Ideal for sending notifications, triggering external processes, or logging:
/**
* @Orm\Table(name="orders")
* @Orm\LifecycleAware
*/
class OrderEntity {
/**
* @Orm\Column(name="order_number", type="string", limit=20)
*/
private string $orderNumber;
/**
* @Orm\Column(name="customer_email", type="string", limit=255)
*/
private string $customerEmail;
/**
* @Orm\PostPersist
*/
public function sendOrderConfirmation(): void {
// Send confirmation email to customer
$emailService = new EmailService();
$emailService->sendOrderConfirmation($this->customerEmail, $this->orderNumber);
}
/**
* @Orm\PostPersist
*/
public function logOrderCreation(): void {
// Log order creation for analytics
$logger = new OrderLogger();
$logger->logOrderCreated($this->orderNumber, $this->customerEmail);
}
/**
* @Orm\PostPersist
*/
public function triggerInventoryUpdate(): void {
// Trigger inventory system update
$inventoryService = new InventoryService();
$inventoryService->reserveItems($this);
}
}
@Orm\PreUpdate - Before Entity Modification
Executed before updating an existing entity. Useful for updating timestamps, performing change validation, or preparing data:
/**
* @Orm\Table(name="articles")
* @Orm\LifecycleAware
*/
class ArticleEntity {
/**
* @Orm\Column(name="title", type="string", limit=255)
*/
private string $title;
/**
* @Orm\Column(name="slug", type="string", limit=255)
*/
private string $slug;
/**
* @Orm\Column(name="updated_at", type="datetime", nullable=true)
*/
private ?\DateTime $updatedAt = null;
/**
* @Orm\Column(name="version", type="integer", default=1)
*/
private int $version = 1;
/**
* @Orm\PreUpdate
*/
public function updateModificationInfo(): void {
// Update timestamp
$this->updatedAt = new \DateTime();
// Increment version for optimistic locking
$this->version++;
}
/**
* @Orm\PreUpdate
*/
public function updateSlugIfNeeded(): void {
// Regenerate slug if title changed
$newSlug = $this->generateSlug($this->title);
if ($this->slug !== $newSlug) {
$this->slug = $newSlug;
}
}
/**
* @Orm\PreUpdate
*/
public function validateChanges(): void {
// Validate business rules before update
if (empty(trim($this->title))) {
throw new \InvalidArgumentException('Article title cannot be empty');
}
if (strlen($this->title) > 255) {
throw new \InvalidArgumentException('Article title too long');
}
}
}
@Orm\PostUpdate - After Entity Modification
Executed after successfully updating an entity. Perfect for cache invalidation, external system synchronization, or change tracking:
/**
* @Orm\Table(name="products")
* @Orm\LifecycleAware
*/
class ProductEntity {
/**
* @Orm\Column(name="name", type="string", limit=255)
*/
private string $name;
/**
* @Orm\Column(name="price", type="decimal", limit="10,2")
*/
private float $price;
/**
* @Orm\Column(name="status", type="string", limit=20)
*/
private string $status;
/**
* @Orm\PostUpdate
*/
public function invalidateCaches(): void {
// Clear product cache
$cacheService = new CacheService();
$cacheService->invalidate("product_{$this->getProductId()}");
$cacheService->invalidate("product_list");
}
/**
* @Orm\PostUpdate
*/
public function syncWithSearchIndex(): void {
// Update search index
$searchService = new SearchIndexService();
$searchService->updateProduct($this);
}
/**
* @Orm\PostUpdate
*/
public function trackPriceChanges(): void {
// Log price changes for historical tracking
$changeTracker = new PriceChangeTracker();
$changeTracker->trackPriceChange($this);
}
/**
* @Orm\PostUpdate
*/
public function notifySubscribers(): void {
// Notify customers watching this product
if ($this->status === 'in_stock') {
$notificationService = new NotificationService();
$notificationService->notifyStockAvailable($this);
}
}
}
@Orm\PreDelete - Before Entity Removal
Executed before deleting an entity. Essential for validation, backup creation, or dependency checking:
/**
* @Orm\Table(name="customers")
* @Orm\LifecycleAware
*/
class CustomerEntity {
/**
* @Orm\Column(name="customer_id", type="integer", primary_key=true)
*/
private ?int $customerId = null;
/**
* @Orm\OneToMany(targetEntity="OrderEntity", mappedBy="customerId")
*/
public $orders;
/**
* @Orm\PreDelete
*/
public function validateDeletion(): void {
// Prevent deletion if customer has active orders
foreach ($this->orders as $order) {
if ($order->getStatus() === 'pending' || $order->getStatus() === 'processing') {
throw new \RuntimeException('Cannot delete customer with active orders');
}
}
}
/**
* @Orm\PreDelete
*/
public function createBackup(): void {
// Create backup before deletion
$backupService = new CustomerBackupService();
$backupService->backupCustomer($this);
}
/**
* @Orm\PreDelete
*/
public function checkDependencies(): void {
// Check for critical dependencies
$dependencyChecker = new DependencyChecker();
if ($dependencyChecker->hasActiveSupportTickets($this->customerId)) {
throw new \RuntimeException('Cannot delete customer with active support tickets');
}
}
}
@Orm\PostDelete - After Entity Removal
Executed after successfully deleting an entity. Used for cleanup, logging, or external system notifications:
/**
* @Orm\Table(name="files")
* @Orm\LifecycleAware
*/
class FileEntity {
/**
* @Orm\Column(name="filename", type="string", limit=255)
*/
private string $filename;
/**
* @Orm\Column(name="file_path", type="string", limit=500)
*/
private string $filePath;
/**
* @Orm\Column(name="file_size", type="bigint")
*/
private int $fileSize;
/**
* @Orm\PostDelete
*/
public function deletePhysicalFile(): void {
// Remove file from filesystem
if (file_exists($this->filePath)) {
unlink($this->filePath);
}
}
/**
* @Orm\PostDelete
*/
public function updateStorageMetrics(): void {
// Update storage usage statistics
$storageService = new StorageService();
$storageService->decreaseUsage($this->fileSize);
}
/**
* @Orm\PostDelete
*/
public function logFileDeletion(): void {
// Log file deletion for audit trail
$auditLogger = new AuditLogger();
$auditLogger->logFileDeletion($this->filename, $this->filePath);
}
/**
* @Orm\PostDelete
*/
public function notifyFileSystem(): void {
// Notify external file management system
$fileSystemNotifier = new FileSystemNotifier();
$fileSystemNotifier->notifyFileDeletion($this->filename);
}
}
Multiple Callbacks per Event
You can define multiple callback methods for the same lifecycle event. They will be executed in the order they are defined in the class:
/**
* @Orm\Table(name="blog_posts")
* @Orm\LifecycleAware
*/
class BlogPostEntity {
/**
* @Orm\Column(name="title", type="string", limit=255)
*/
private string $title;
/**
* @Orm\Column(name="content", type="text")
*/
private string $content;
/**
* First callback - data preparation
* @Orm\PrePersist
*/
public function prepareData(): void {
$this->title = trim($this->title);
$this->content = trim($this->content);
}
/**
* Second callback - validation
* @Orm\PrePersist
*/
public function validateContent(): void {
if (empty($this->title)) {
throw new \InvalidArgumentException('Title is required');
}
if (strlen($this->content) < 100) {
throw new \InvalidArgumentException('Content must be at least 100 characters');
}
}
/**
* Third callback - additional processing
* @Orm\PrePersist
*/
public function generateMetadata(): void {
$this->slug = $this->generateSlug($this->title);
$this->readingTime = $this->calculateReadingTime($this->content);
$this->wordCount = str_word_count($this->content);
}
}
Advanced Lifecycle Callback Patterns
Conditional Logic in Callbacks
/**
* @Orm\Table(name="users")
* @Orm\LifecycleAware
*/
class UserEntity {
/**
* @Orm\Column(name="email", type="string", limit=255)
*/
private string $email;
/**
* @Orm\Column(name="status", type="string", limit=20)
*/
private string $status;
/**
* @Orm\Column(name="last_login", type="datetime", nullable=true)
*/
private ?\DateTime $lastLogin = null;
private string $previousEmail = '';
/**
* @Orm\PreUpdate
*/
public function trackEmailChanges(): void {
// Store previous email if it's changing
if ($this->email !== $this->previousEmail && !empty($this->previousEmail)) {
$emailHistoryService = new EmailHistoryService();
$emailHistoryService->recordEmailChange($this->getUserId(), $this->previousEmail, $this->email);
}
}
/**
* @Orm\PostUpdate
*/
public function handleStatusChanges(): void {
// Send welcome email only when status changes to 'active'
if ($this->status === 'active' && $this->isStatusChanged()) {
$emailService = new EmailService();
$emailService->sendWelcomeEmail($this->email);
}
// Send suspension notice when status changes to 'suspended'
if ($this->status === 'suspended' && $this->isStatusChanged()) {
$emailService = new EmailService();
$emailService->sendSuspensionNotice($this->email);
}
}
private function isStatusChanged(): bool {
// Implementation to check if status actually changed
// This could use UnitOfWork to compare with original data
return true; // Simplified for example
}
}
Service Integration in Callbacks
/**
* @Orm\Table(name="orders")
* @Orm\LifecycleAware
*/
class OrderEntity {
/**
* @Orm\Column(name="status", type="string", limit=20)
*/
private string $status;
/**
* @Orm\Column(name="total_amount", type="decimal", limit="10,2")
*/
private float $totalAmount;
/**
* @Orm\PostPersist
*/
public function processNewOrder(): void {
// Use dependency injection or service locator
$serviceContainer = ServiceContainer::getInstance();
// Process payment
$paymentService = $serviceContainer->get(PaymentService::class);
$paymentService->processOrderPayment($this);
// Update inventory
$inventoryService = $serviceContainer->get(InventoryService::class);
$inventoryService->reserveOrderItems($this);
// Send notifications
$notificationService = $serviceContainer->get(NotificationService::class);
$notificationService->sendOrderCreatedNotifications($this);
}
/**
* @Orm\PostUpdate
*/
public function handleStatusUpdates(): void {
$serviceContainer = ServiceContainer::getInstance();
switch ($this->status) {
case 'shipped':
$shippingService = $serviceContainer->get(ShippingService::class);
$shippingService->generateTrackingInfo($this);
break;
case 'delivered':
$loyaltyService = $serviceContainer->get(LoyaltyService::class);
$loyaltyService->awardLoyaltyPoints($this);
break;
case 'cancelled':
$inventoryService = $serviceContainer->get(InventoryService::class);
$inventoryService->releaseOrderItems($this);
break;
}
}
}
Error Handling in Lifecycle Callbacks
Exception Handling Strategy
/**
* @Orm\Table(name="products")
* @Orm\LifecycleAware
*/
class ProductEntity {
/**
* @Orm\PostUpdate
*/
public function updateExternalSystems(): void {
try {
// Critical operation - should fail the transaction
$inventoryService = new InventoryService();
$inventoryService->updateStock($this);
} catch (InventoryException $e) {
// Re-throw critical exceptions to fail the transaction
throw new \RuntimeException('Failed to update inventory: ' . $e->getMessage(), 0, $e);
}
try {
// Non-critical operation - should not fail the transaction
$searchIndexService = new SearchIndexService();
$searchIndexService->updateProduct($this);
} catch (SearchIndexException $e) {
// Log error but don't fail the transaction
$logger = new Logger();
$logger->error('Failed to update search index for product ' . $this->getProductId(), [
'exception' => $e,
'product_id' => $this->getProductId()
]);
}
}
/**
* @Orm\PreDelete
*/
public function validateDeletionSafety(): void {
try {
// Check if product can be safely deleted
$dependencyChecker = new ProductDependencyChecker();
$dependencyChecker->checkCanDelete($this);
} catch (DependencyException $e) {
// Prevent deletion by throwing exception
throw new \RuntimeException('Cannot delete product: ' . $e->getMessage(), 0, $e);
}
}
}
Performance Considerations
Efficient Callback Implementation
/**
* @Orm\Table(name="analytics_events")
* @Orm\LifecycleAware
*/
class AnalyticsEventEntity {
/**
* @Orm\PostPersist
*/
public function optimizedLogging(): void {
// Good: Lightweight operation
$this->logEventQuickly();
}
/**
* @Orm\PostPersist
*/
public function asyncProcessing(): void {
// Good: Queue heavy operations for background processing
$queueService = new QueueService();
$queueService->queueAnalyticsProcessing($this->getEventId());
}
/**
* Don't do this in callbacks - too slow
*/
public function badCallback(): void {
// Bad: Expensive operations that slow down persistence
// $this->generateComplexReport(); // Takes 2+ seconds
// $this->sendMultipleEmails(); // Network I/O
// $this->processLargeDataSet(); // Memory intensive
// $this->callSlowExternalAPI(); // Unreliable network
}
private function logEventQuickly(): void {
// Fast, local logging
error_log("Event {$this->getEventId()} created");
}
}
Batch Processing Optimization
/**
* @Orm\Table(name="orders")
* @Orm\LifecycleAware
*/
class OrderEntity {
private static array $pendingNotifications = [];
/**
* @Orm\PostPersist
*/
public function queueNotification(): void {
// Collect notifications for batch processing
self::$pendingNotifications[] = $this->getOrderId();
}
/**
* Process all queued notifications (called after flush)
*/
public static function processPendingNotifications(): void {
if (!empty(self::$pendingNotifications)) {
$notificationService = new NotificationService();
$notificationService->sendBatchNotifications(self::$pendingNotifications);
self::$pendingNotifications = [];
}
}
}
// In your service layer after flush():
$entityManager->flush();
OrderEntity::processPendingNotifications();
Lifecycle Callbacks Best Practices
Do:
- Keep callbacks lightweight and fast
- Use callbacks for data validation and integrity
- Handle timestamps and auto-generated values
- Queue heavy operations for background processing
- Log important events for auditing
- Validate business rules consistently
Don't:
- Perform expensive I/O operations in callbacks
- Make unreliable external API calls
- Execute long-running computations
- Access uninitialized related entities
- Ignore exceptions that should fail transactions
- Create circular dependencies between entities
ObjectQuel's lifecycle callback system provides a powerful way to implement cross-cutting concerns like auditing, validation, and system integration while maintaining clean separation between business logic and persistence concerns.