Product recommendations are a critical driver of average order value and customer retention in modern e-commerce. While Shopware offers robust default plugins for cross-selling and "People also bought" functionality, businesses often require deeper personalization. Whether you need to integrate external AI models, implement proprietary loyalty logic, or dynamically adjust weights based on real-time inventory, building a custom engine provides the necessary flexibility.
In this guide, we'll walk through architecting a custom recommendation plugin for Shopware 6.7+. We'll focus on decoupled service design, leveraging the Strategy pattern, exposing data via the Store API, and adhering to Shopware 6.7 best practices for performance and type safety.
Why Build Custom?
Default plugins rely on static rules defined in the administration. A custom engine allows you to:
- Integrate External Intelligence: Connect with Python microservices running TensorFlow or PyTorch models without polluting the PHP stack.
- Dynamic Weighting: Adjust recommendation weights based on marketing campaigns, seasonal trends, or user segments.
- Complex Business Rules: Implement logic that spans multiple entities (e.g., subscription tiers, B2B pricing groups) which default plugins cannot easily access.
Shopware 6.7 enhances plugin development with stricter type safety, improved dependency injection patterns, and better event handling. Our approach leverages these features to create a system that is maintainable, testable, and scalable.
Step 1: Architecture and Service Design
To ensure your engine remains flexible, avoid hardcoding algorithms. Instead, use the Strategy pattern. This allows you to switch recommendation types (e.g., collaborative filtering vs. category-based) at runtime based on configuration or context.
Start by defining the core service in your plugin's src/ directory. Shopware 6.7 encourages readonly classes and strict typing to ensure immutability where possible.
<?php declare(strict_types=1);
namespace Swag\CustomRecommendation\Service;
use Shopware\Core\Checkout\Product\SalesChannel\SalesChannelContext;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
final readonly class ProductRecommendationEngine
{
public function __construct(
private EntityRepositoryInterface $productRepository,
private RecommendationStrategyResolver $strategyResolver
) {
}
/**
* Main entry point for fetching recommendations.
*/
public function getRecommendations(string $seedProductId, SalesChannelContext $context): array
{
// 1. Resolve the active strategy based on context or config
$strategy = $this->strategyResolver->resolve($context);
// 2. Get candidate IDs from the strategy
$candidateIds = $strategy->getCandidates($seedProductId, $context);
if (empty($candidateIds)) {
return [];
}
// 3. Enrich candidates with context-aware data
return $this->enrichCandidates($candidateIds, $seedProductId, $context);
}
private function enrichCandidates(array $ids, string $excludeId, SalesChannelContext $context): array
{
$criteria = new Criteria($ids);
$criteria->setTitle('custom-recommendation-enrichment');
// Ensure standard associations for storefront rendering
$criteria->addAssociation('cover')->addAssociation('categories');
// Exclude the seed product to prevent self-recommendation
$criteria->getFilterList()->add(
new MultiFilter(MultiFilter::CONNECTION_OR, [
new EqualsFilter('id', '!=', $excludeId)
])
);
// Fetch entities using the SalesChannelContext.
// CRITICAL: In Shopware 6.7, always pass context to ensure pricing groups,
// tax rules, and currency conversions are applied correctly.
$results = $this->productRepository->search($criteria, $context->getContext());
return $results->getEntities()->getElements();
}
}
Shopware 6.7 Note: The readonly keyword on the class is a modern PHP feature supported in 6.7 that signals the service's statelessness. This improves performance slightly and documents intent clearly for other developers.
Step 2: Strategy Pattern Implementation
Hardcoding logic makes your engine brittle. Define an interface that strategies must implement:
interface RecommendationStrategyInterface
{
public function getCandidates(string $seedId, SalesChannelContext $context): array;
}
// Example: Rule-based strategy using product categories
final class CategoryBasedStrategy implements RecommendationStrategyInterface
{
public function __construct(
private readonly EntityRepositoryInterface $productRepository
) {
}
public function getCandidates(string $seedId, SalesChannelContext $context): array
{
// Fetch seed product to get categories
$criteria = new Criteria([$seedId]);
$criteria->addAssociation('categories');
$seedProduct = $this->productRepository->search($criteria, $context->getContext())->get($seedId);
if (!$seedProduct) {
return [];
}
// Get parent category IDs to find siblings
$categoryIds = $seedProduct->getCategories()->map(fn($cat) => $cat->getId());
// Find products in same categories, excluding current product
$criteria = new Criteria();
$criteria->addFilter(
new MultiFilter(MultiFilter::CONNECTION_AND, [
new EqualsAnyFilter('categories.id', $categoryIds),
new EqualsFilter('id', '!=', $seedId)
])
);
// Limit results for performance
$criteria->setLimit(10);
return $this->productRepository->searchIds($criteria, $context->getContext())->getIds();
}
}
This modularity allows you to A/B test algorithms or switch strategies via plugin configuration without modifying the core engine code.
Step 3: Exposing Data via Store API
The storefront should not call internal services directly. Create a custom plugin route to expose recommendations safely and consistently.
<?php declare(strict_types=1);
namespace Swag\CustomRecommendation\Routing;
use Shopware\Core\Framework\Api\Context\AdminApiSource;
use Shopware\Core\Framework\DataAbstractionLayer\Cache\EntitySchemaCacheKeyGenerator;
use Shopware\Core\Framework\Plugin\PluginEntity;
use Shopware\Core\Framework\Struct\ArrayEntity;
use Shopware\Core\System\SalesChannel\SalesChannelContextFactoryInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Swag\CustomRecommendation\Service\ProductRecommendationEngine;
#[RouteScope(scopes: ['store-api'])]
class RecommendationController extends StorefrontController
{
public function __construct(
private readonly ProductRecommendationEngine $engine,
private readonly EntitySchemaCacheKeyGenerator $schemaCache
) {
}
#[Route('/store-api/recommendations/{productId}', name: 'store-api.custom.recommendation', methods: ['GET'])]
public function getRecommendations(string $productId, Request $request): Response
{
// Shopware 6.7 ensures context propagation is robust.
// The sales-channel-context is automatically resolved from the session/token.
$context = $request->attributes->get('sales-channel-context');
if (!$context) {
return new JsonResponse(['status' => 'error', 'message' => 'Context missing'], Response::HTTP_BAD_REQUEST);
}
$recommendations = $this->engine->getRecommendations($productId, $context);
// Return data in a format friendly to your frontend state management
return new JsonResponse([
'products' => $recommendations,
'seoInformation' => null,
'sw-version' => \Shopware\Core\Framework\Adapter\Kernel\SystemVersion::getVersion(),
]);
}
}
Step 4: Integration and Performance Best Practices
Frontend Consumption
To utilize this endpoint, your storefront component can fetch data asynchronously. This keeps initial page loads fast and allows recommendations to update dynamically based on user clicks.
<template>
<div class="recommendation-block" v-if="products.length">
<h3>You Might Also Like</h3>
<product-card-list :items="products" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import ProductCardList from './ProductCardList.vue';
const props = defineProps({ productId: String });
const products = ref([]);
onMounted(async () => {
if (!props.productId) return;
// Use Shopware's axios instance for automatic token management
const response = await window.storeApi.apiClient.get(`/recommendations/${props.productId}`);
products.value = response.products;
});
</script>
Performance Considerations
- Caching: Recommendation calculations can be expensive. Implement a cache layer using Redis. Cache results by the tuple
(productId, salesChannelId, languageId). Invalidate or TTL the cache based on product updates. Shopware 6.7's caching infrastructure integrates well with PSR-6 compatible backends. - Criteria Optimization: In
enrichCandidates, always request only the fields you need usingaddSelectif possible, thoughsearchIdsfollowed by a batch search is standard. Avoid N+1 queries by ensuring associations are loaded in the criteria. - Fallbacks: Always implement fallback logic. If no candidates are found, return best-sellers or featured products to maintain UX quality.
Shopware 6.7 Specific Enhancements
When building for 6.7+, keep these nuances in mind:
- Deprecation Warnings: The platform enforces stricter deprecation handling. Ensure you aren't relying on removed APIs. Use the
shopware/deprecationchecker during development. - PHP 8.2/8.3 Compatibility: Leverage modern features like Enums for strategy types to replace magic strings, enhancing type safety across your recommendation layers.
- Plugin Manifests: Ensure your
plugin.jsoncorrectly defines dependencies onstore-apiandframeworkversions compatible with 6.7.
Conclusion
Building a custom recommendation engine in Shopware 6.7 leverages the framework's strong plugin architecture, dependency injection, and API-first design. By decoupling algorithm logic via strategies, exposing data through a clean Store API, and respecting context propagation for pricing and availability, you create a system that is both powerful and maintainable.
Whether you're implementing simple rule-based suggestions or integrating complex machine learning pipelines, Shopware 6.7 provides the stability and modern PHP features needed to build a high-performance personalization engine that drives revenue and enhances customer experience. Start by defining your strategy interface, implement your first engine service, and gradually evolve towards more sophisticated personalization patterns as your business data grows.