- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
256 lines
7.4 KiB
Markdown
256 lines
7.4 KiB
Markdown
# 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**. |