# External Entity Mapping für Search System **Suchfunktionalität ohne Entity-Änderungen** - Komplette Integration über externe Konfiguration. ## 1. Basic Mapping Configuration ```php // In einem Service Provider oder Initializer final class SearchMappingProvider { public static function registerMappings(SearchableMappingRegistry $registry): void { // User Entity Mapping $userMapping = SearchableMapping::for(User::class) ->entityType('users') ->idField('id') ->field('name', 'name') ->field('email', 'email') ->field('bio', 'biography') ->field('created', 'createdAt', fn($date) => $date->format('Y-m-d')) ->boost('name', 2.0) ->boost('email', 1.5) ->autoIndex(true) ->build(); $registry->register($userMapping); // Product Entity Mapping $productMapping = SearchableMapping::for(Product::class) ->entityType('products') ->field('title', 'name') ->field('description', 'description') ->field('category', 'category.name') // Nested field access ->field('price', 'price', fn($price) => $price->getCents() / 100) // Transform Money object ->field('tags', 'getTags', fn($tags) => implode(' ', $tags)) // Method call + transform ->boost('title', 3.0) ->boost('description', 1.0) ->build(); $registry->register($productMapping); } } ``` ## 2. Configuration-Based Mapping ```php // config/search_mappings.php return [ User::class => [ 'entity_type' => 'users', 'id_field' => 'id', 'auto_index' => true, 'fields' => [ 'name' => 'name', 'email' => 'email', 'bio' => 'biography', 'created' => [ 'field' => 'createdAt', 'transformer' => fn($date) => $date->format('Y-m-d') ] ], 'boosts' => [ 'name' => 2.0, 'email' => 1.5 ] ], Product::class => [ 'entity_type' => 'products', 'fields' => [ 'title' => 'name', 'description' => 'description', 'category' => 'category.name', // Nested access 'price' => [ 'field' => 'price', 'transformer' => fn($price) => $price->toFloat() ] ], 'boosts' => [ 'title' => 3.0 ] ] ]; // Load configuration $config = require 'config/search_mappings.php'; $registry->registerFromConfig($config); ``` ## 3. Repository Integration (No Entity Changes) ```php final class UserRepository { public function __construct( private EntityManager $entityManager, private SearchIndexingService $searchIndexing ) { } public function create(array $userData): User { $user = new User($userData); $this->entityManager->persist($user); $this->entityManager->flush(); // Auto-index after creation (if enabled) $this->searchIndexing->indexEntity($user); return $user; } public function update(User $user, array $changes): void { // Apply changes to user foreach ($changes as $field => $value) { $user->{"set" . ucfirst($field)}($value); } $this->entityManager->flush(); // Update search index $this->searchIndexing->updateEntity($user); } public function delete(User $user): void { // Remove from search first $this->searchIndexing->removeEntity($user); $this->entityManager->remove($user); $this->entityManager->flush(); } public function reindexAll(): BulkIndexResult { return $this->searchIndexing->reindexEntityType('users', function() { return $this->entityManager->findAll(User::class); }); } } ``` ## 4. Service Usage Without Entity Knowledge ```php final class ProductSearchService { public function __construct( private SearchService $searchService, private SearchableMappingRegistry $mappingRegistry ) { } public function makeProductSearchable(Product $product): bool { // Check if product is configured as searchable if (!$this->mappingRegistry->isSearchable($product)) { return false; } // Create adapter automatically $adapter = $this->mappingRegistry->createAdapter($product); if (!$adapter) { return false; } // Index using the mapped fields return $this->searchService->index( $adapter->getEntityType(), $adapter->getId(), $adapter->toSearchDocument() ); } public function searchProducts(string $query): SearchResult { return $this->searchService ->for('products') ->query($query) ->boost('title', 3.0) // Use configured boosts ->limit(20) ->search(); } } ``` ## 5. Event-Driven Auto-Indexing ```php // Events werden automatisch von Entity Operations gefeuert $eventDispatcher->dispatch(new EntityCreatedEvent($user)); // Auto-indexes $eventDispatcher->dispatch(new EntityUpdatedEvent($product)); // Auto-updates $eventDispatcher->dispatch(new EntityDeletedEvent($order)); // Auto-removes ``` ## 6. Advanced Field Transformations ```php $mapping = SearchableMapping::for(Article::class) ->field('title', 'title') ->field('content', 'body') ->field('author', 'user.name') // Nested object access ->field('tags', 'tags', fn($tags) => implode(' ', array_column($tags, 'name'))) ->field('published_date', 'publishedAt', fn($date) => $date?->format('Y-m-d')) ->field('word_count', 'body', fn($content) => str_word_count(strip_tags($content))) ->field('reading_time', 'body', fn($content) => ceil(str_word_count($content) / 200)) ->build(); ``` ## 7. Conditional Indexing ```php final class ConditionalSearchListener { #[EventListener(event: 'entity.updated')] public function onEntityUpdated(EntityUpdatedEvent $event): void { $entity = $event->getEntity(); // Only index published articles if ($entity instanceof Article && $entity->isPublished()) { $this->indexingService->updateEntity($entity); } elseif ($entity instanceof Article && !$entity->isPublished()) { // Remove from index if unpublished $this->indexingService->removeEntity($entity); } } } ``` ## Vorteile dieses Ansatzes: ✅ **Keine Entity-Änderungen erforderlich** ✅ **Zentrale Konfiguration** aller Mappings ✅ **Flexible Field-Transformationen** ✅ **Automatische Indexierung** über Events ✅ **Repository-Integration** ohne Abhängigkeiten ✅ **Conditional Indexing** möglich ✅ **Backward Compatible** mit bestehenden Entities ## Integration: ```php // Container Registration $container->singleton(SearchableMappingRegistry::class); $container->singleton(SearchIndexingService::class); // Register mappings at startup $registry = $container->get(SearchableMappingRegistry::class); SearchMappingProvider::registerMappings($registry); ``` Dieser Ansatz ermöglicht **vollständige Suchfunktionalität ohne jegliche Änderungen an bestehenden Entity-Klassen**.