# Luigi's Box Bundle Luigi's Box Bundle is a Symfony integration package that provides seamless access to the Luigi's Box search and content management platform. It enables PHP developers to index, update, search, and manage content through Luigi's Box API with type-safe value objects, automatic authentication handling, and comprehensive response parsing. The bundle supports multiple configurations for different environments or accounts, offering features like full and partial content updates, content removal, advanced search with facets and filters, update-by-query batch operations, and product recommendations. All API interactions are handled through dependency-injected services with proper exception handling for rate limiting, bad requests, and service unavailability. ## Installation Install the bundle via Composer. ```bash composer require answear/luigis-box-bundle ``` ## Configuration Configure the bundle with your Luigi's Box API credentials and optional timeout settings. ```yaml # config/packages/answear_luigis_box.yaml answear_luigis_box: default_config: primary configs: primary: host: 'https://live.luigisbox.com' # default publicKey: 'your_public_key' privateKey: 'your_private_key' connectionTimeout: 4.0 # default requestTimeout: 10.0 # default searchTimeout: 6.0 # default searchCacheTtl: 0 # max 300 seconds recommendationsRequestTimeout: 1.0 # default recommendationsConnectionTimeout: 1.0 # default secondary: publicKey: 'another_public_key' privateKey: 'another_private_key' ``` ## Switching Between Configurations Switch between multiple Luigi's Box configurations at runtime using the ConfigProvider service. ```php configProvider->setConfig('secondary'); // Add dynamic configuration at runtime $this->configProvider->addConfig('temporary', new ConfigDTO( publicKey: 'temp_public_key', privateKey: 'temp_private_key', host: 'https://live.luigisbox.com', connectionTimeout: 4.0, requestTimeout: 10.0, searchTimeout: 6.0, searchCacheTtl: 60, recommendationsRequestTimeout: 1.0, recommendationsConnectionTimeout: 1.0, )); // Set custom headers for requests $this->configProvider->setHeader('X-Custom-Header', 'custom-value'); // Reset all custom headers $this->configProvider->resetHeaders(); } } ``` ## Full Content Update Create or replace complete content documents in Luigi's Box index with all fields and nested items. ```php getName(), url: '/products/' . $product->getSlug(), type: 'product', fields: [ 'title' => $product->getName(), 'description' => $product->getDescription(), 'price' => $product->getPrice(), 'brand' => $product->getBrand(), 'category' => $product->getCategory(), 'availability' => $product->isAvailable() ? 1 : 0, 'image_url' => $product->getImageUrl(), ] ); // Add nested variants $nestedVariants = []; foreach ($product->getVariants() as $variant) { $nestedVariants[] = new ContentUpdate( title: $variant->getName(), url: '/products/' . $product->getSlug() . '/variant/' . $variant->getId(), type: 'variant', fields: [ 'size' => $variant->getSize(), 'color' => $variant->getColor(), 'sku' => $variant->getSku(), ] ); } $contentUpdate->setNested($nestedVariants); $updates[] = $contentUpdate; } $collection = new ContentUpdateCollection($updates); try { $response = $this->request->contentUpdate($collection); if ($response->isSuccess()) { echo "All {$response->okCount} documents indexed successfully.\n"; } else { echo "Indexed: {$response->okCount}, Errors: {$response->errorsCount}\n"; foreach ($response->errors as $error) { echo "Failed URL: {$error->url}, Reason: {$error->reason}\n"; } } } catch (TooManyItemsException $e) { echo "Batch too large. Limit: {$e->limit} items.\n"; } catch (TooManyRequestsException $e) { echo "Rate limited. Retry after {$e->retryAfterSeconds} seconds.\n"; } catch (BadRequestException $e) { echo "Bad request: " . $e->response->getBody() . "\n"; } } } ``` ## Partial Content Update Update specific fields of existing documents without replacing the entire document. ```php $newPrice) { $updates[] = new PartialContentUpdate( url: $productUrl, type: 'product', fields: [ 'price' => $newPrice, 'price_updated_at' => (new \DateTime())->format('Y-m-d H:i:s'), ] ); } $collection = new ContentUpdateCollection($updates); $response = $this->request->partialContentUpdate($collection); echo "Updated {$response->okCount} prices, {$response->errorsCount} failures.\n"; } } ``` ## Content Removal Remove documents from the Luigi's Box index by URL and type. ```php request->contentRemoval($collection); if ($response->isSuccess()) { echo "Removed {$response->okCount} products from index.\n"; } else { foreach ($response->errors as $error) { echo "Failed to remove {$error->url}: {$error->reason}\n"; if ($error->causedBy) { print_r($error->causedBy); } } } } } ``` ## Change Availability Quickly enable or disable product availability using a simplified partial update interface. ```php $inStock) { $availabilities[] = new ContentAvailability( url: $productUrl, available: $inStock ); } // Batch update multiple products $collection = new ContentAvailabilityCollection($availabilities); $response = $this->request->changeAvailability($collection); echo "Updated availability for {$response->okCount} products.\n"; } public function disableSingleProduct(string $productUrl): void { // Single product update $availability = new ContentAvailability( url: $productUrl, available: false ); $response = $this->request->changeAvailability($availability); if ($response->isSuccess()) { echo "Product disabled successfully.\n"; } } } ``` ## Search Execute searches with filters, facets, sorting, preferences, and pagination using the SearchUrlBuilder. ```php setQuery($query) ->setSize(20) ->addFilter('type', 'product') ->addFilter('availability', 1) ->setSort('relevance', 'desc') ->setFacets(['brand', 'category', 'color', 'size', 'price']) ->setDynamicFacetsSize(10) ->setHitFields(['title', 'price', 'image_url', 'brand']) ->setQuicksearchTypes(['product', 'category']) ->setFixits(true); // Apply active filters from user selection foreach ($activeFilters as $key => $values) { foreach ((array) $values as $value) { $urlBuilder->addFilter($key, $value); } } // Add brand preference to boost certain brands $urlBuilder->addPrefer('brand', 'Premium Brand'); $urlBuilder->addPrefer('brand', 'Featured Brand'); // Add must filters (required conditions) $urlBuilder->addMustFilter('in_stock', true); // Set user context for personalization $urlBuilder->setUserId('user-123'); $urlBuilder->setClientId('client-abc'); // Add geo-location context $context = new Context(); $context->setGeoLocation(52.2297, 21.0122); // Warsaw coordinates $context->setGeoLocationField('store_location'); $context->setAvailabilityField('local_availability'); $context->setBoostField('popularity_score'); $context->setFreshnessField('created_at'); $urlBuilder->setContext($context); // Enable query understanding (requires userId to be set first) $urlBuilder->enableQueryUnderstanding(); try { $response = $this->searchRequest->search($urlBuilder); return [ 'query' => $response->query, 'corrected_query' => $response->correctedQuery, 'total_hits' => $response->totalHits, 'current_size' => $response->currentSize, 'guid' => $response->guid, 'filters' => $response->filters, 'hits' => array_map(fn($hit) => [ 'url' => $hit->url, 'type' => $hit->type, 'title' => $hit->attributes['title'] ?? '', 'price' => $hit->attributes['price'] ?? 0, 'highlight' => $hit->highlight, 'exact_match' => $hit->exact, 'alternative' => $hit->alternative, 'nested' => $hit->nested, ], $response->hits), 'quicksearch_hits' => array_map(fn($hit) => [ 'url' => $hit->url, 'title' => $hit->attributes['title'] ?? '', ], $response->quickSearchHits), 'facets' => array_map(fn($facet) => [ 'name' => $facet->name, 'type' => $facet->type, 'values' => $facet->values, ], $response->facets), ]; } catch (BadRequestException $e) { throw new \RuntimeException('Search failed: ' . $e->getMessage()); } catch (ServiceUnavailableException $e) { throw new \RuntimeException('Search service unavailable'); } } } ``` ## Update By Query Perform batch updates on documents matching specific search criteria without knowing individual URLs. ```php 'green'] ); // Define the update - change color to multiple values $update = new Update( fields: ['color' => ['olive', 'emerald', 'forest']] ); $updateByQuery = new UpdateByQuery($search, $update); // Start the async update job $response = $this->updateByQueryRequest->update($updateByQuery); $jobId = $response->jobId; echo "Update job started with ID: {$jobId}\n"; // Poll for completion return $this->waitForCompletion($jobId); } private function waitForCompletion(int $jobId): int { $maxAttempts = 30; $attempt = 0; while ($attempt < $maxAttempts) { $statusResponse = $this->updateByQueryRequest->getStatus($jobId); echo "Job {$statusResponse->trackerId}: "; if ($statusResponse->isCompleted()) { echo "Completed!\n"; echo "Updated: {$statusResponse->okCount}, Errors: {$statusResponse->errorsCount}\n"; if ($statusResponse->errors) { foreach ($statusResponse->errors as $error) { echo "Error for {$error->url}: {$error->reason}\n"; } } return $statusResponse->okCount; } echo "Still processing...\n"; sleep(2); $attempt++; } throw new \RuntimeException("Job {$jobId} did not complete in time"); } } ``` ## Recommendations (Experimental) Fetch personalized product recommendations using various recommendation strategies. ```php 'product_detail', 'category' => 'electronics', ], settingsOverride: [ 'diversity' => 0.5, ], markFallbackResults: true, recommenderClientIdentifier: 'web-app-v2', ); $collection = new RecommendationsCollection([$recommendation]); $response = $this->recommendationsRequest->getRecommendations($collection); if ($response->isSuccess()) { return $response->rawResponse; } return []; } } ``` ## Exception Handling Handle all possible API exceptions with appropriate error recovery strategies. ```php request->contentUpdate($collection); if ($response->isSuccess()) { return true; } // Log partial failures but consider it success if some passed foreach ($response->errors as $error) { error_log("Sync error for {$error->url}: {$error->type} - {$error->reason}"); if ($error->causedBy) { error_log("Caused by: " . json_encode($error->causedBy)); } } return $response->okCount > 0; } catch (BadRequestException $e) { // Request is malformed, don't retry error_log("Bad request - check payload: " . $e->request->getBody()); error_log("Response: " . $e->response->getBody()); return false; } catch (TooManyItemsException $e) { // Split the batch and retry error_log("Batch too large. Max items: {$e->limit}"); return $this->syncInBatches($collection, $e->limit); } catch (TooManyRequestsException $e) { // Rate limited - wait and retry $waitTime = $e->retryAfterSeconds; error_log("Rate limited. Waiting {$waitTime} seconds..."); sleep($waitTime); $attempt++; } catch (MalformedResponseException $e) { // API returned unexpected response error_log("Malformed response: " . json_encode($e->response)); $attempt++; sleep(1); } catch (ServiceUnavailableException $e) { // Service is down - exponential backoff $waitTime = pow(2, $attempt); error_log("Service unavailable. Retrying in {$waitTime} seconds..."); sleep($waitTime); $attempt++; } } return false; } private function syncInBatches(ContentUpdateCollection $collection, int $batchSize): bool { // Implementation to split collection into smaller batches return true; } } ``` ## Summary Luigi's Box Bundle provides a complete toolkit for integrating Luigi's Box search and content management into Symfony applications. The primary use cases include e-commerce product indexing with full and partial updates, real-time inventory availability synchronization, advanced product search with faceted filtering and personalization, batch content updates using query-based operations, and AI-powered product recommendations. The bundle handles all authentication, request signing, and response parsing automatically. Integration follows standard Symfony patterns with dependency injection for all services. Configure your API credentials in YAML, inject `RequestInterface` for content operations, `SearchRequestInterface` for search, `UpdateByQueryRequestInterface` for batch updates, and `RecommendationsRequestInterface` for recommendations. The bundle supports multiple configurations for multi-tenant or multi-environment setups, making it suitable for complex e-commerce architectures with separate staging and production Luigi's Box accounts.