Pimcore Headless met GraphQL: een complete gids
Headless architectuur is de standaard geworden voor moderne e-commerce en content platforms. Pimcore's DataHub biedt een krachtige GraphQL API die je PIM en DAM data beschikbaar maakt voor elke frontend. In dit artikel behandelen we production-grade implementaties: van DataHub configuratie tot type-safe frontend integratie met caching strategieën.
DataHub configuratie en schema design
DataHub is Pimcore's GraphQL engine. Een goed schema design bepaalt de performance en bruikbaarheid van je API:
pimcore_data_hub:
configurations:
product-api:
general:
active: true
name: 'Product API'
description: 'GraphQL API for product data'
sqlObjectCondition: "o_published = 1"
security:
method: 'datahub_apikey'
apikey:
- name: 'frontend-key'
permissions:
- 'read'
- name: 'admin-key'
permissions:
- 'read'
- 'update'
schema:
queryEntities:
Product:
id: true
name: true
columnConfig:
columns:
- name: 'sku'
label: 'SKU'
- name: 'price'
label: 'Price'
- name: 'categories'
label: 'Categories'
fieldHelper: 'relation'
- name: 'images'
label: 'Images'
fieldHelper: 'asset'namespace Ten50\Bundle\DataHub\GraphQL\Resolver;
use Pimcore\Bundle\DataHubBundle\GraphQL\Resolver\Base;
use Pimcore\Model\DataObject\Product;
class ProductResolver extends Base
{
public function resolveAvailability(Product $product, array $args): array
{
$stock = $product->getStock();
$threshold = $args['lowStockThreshold'] ?? 10;
return [
'inStock' => $stock > 0,
'quantity' => $stock,
'lowStock' => $stock > 0 && $stock <= $threshold,
'availableFrom' => $product->getAvailableFrom()?->format('c'),
];
}
public function resolvePricing(Product $product, array $args): array
{
$currency = $args['currency'] ?? 'EUR';
$customerGroup = $args['customerGroup'] ?? 'default';
return [
'basePrice' => $product->getPrice(),
'finalPrice' => $this->priceCalculator->calculate($product, $customerGroup),
'currency' => $currency,
'vatRate' => $product->getVatRate() ?? 21,
'priceWithVat' => $this->priceCalculator->calculateWithVat($product, $customerGroup),
];
}
}Custom resolvers geven je volledige controle over de response structuur en business logic.
Complexe queries en filtering
GraphQL's kracht zit in het ophalen van precies de data die je nodig hebt. Hier zijn patterns voor productie-scenario's:
query GetProductCatalog(
$locale: String!
$categoryId: Int
$minPrice: Float
$maxPrice: Float
$inStock: Boolean
$first: Int = 20
$after: String
) {
getProductListing(
filter: {
categories: { $contains: $categoryId }
price: { $gte: $minPrice, $lte: $maxPrice }
stock: { $gt: 0 } @include(if: $inStock)
}
sortBy: "price"
sortOrder: "ASC"
first: $first
after: $after
) {
pageInfo {
hasNextPage
endCursor
totalCount
}
edges {
cursor
node {
id
sku
name(language: $locale)
description(language: $locale)
price
availability {
inStock
quantity
lowStock
}
images {
id
filename
thumbnail(config: "product_listing")
fullsize: thumbnail(config: "product_detail")
}
categories {
id
name(language: $locale)
path
}
relatedProducts(first: 4) {
edges {
node {
id
sku
name(language: $locale)
thumbnail: images(first: 1) {
thumbnail(config: "product_thumb")
}
}
}
}
}
}
}
}Gebruik fragments voor herbruikbare field sets en cursor-based pagination voor grote datasets.
Authenticatie en rate limiting
Security is cruciaal voor publieke APIs. Implementeer meerdere lagen van bescherming:
namespace Ten50\Bundle\DataHub\Security;
use Pimcore\Bundle\DataHubBundle\GraphQL\Service\RequestServiceInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Cache\Adapter\RedisAdapter;
class ApiKeyAuthenticator
{
private const RATE_LIMIT_WINDOW = 60;
private const RATE_LIMIT_MAX_REQUESTS = 100;
public function __construct(
private RedisAdapter $cache,
private RequestServiceInterface $requestService
) {}
public function authenticate(Request $request): AuthResult
{
$apiKey = $request->headers->get('X-API-Key')
?? $request->query->get('apikey');
if (!$apiKey) {
return AuthResult::failed('API key required');
}
$keyConfig = $this->validateApiKey($apiKey);
if (!$keyConfig) {
return AuthResult::failed('Invalid API key');
}
if (!$this->checkRateLimit($apiKey)) {
return AuthResult::rateLimited(
'Rate limit exceeded',
$this->getRetryAfter($apiKey)
);
}
if (!$this->checkQueryComplexity($request, $keyConfig)) {
return AuthResult::failed('Query complexity limit exceeded');
}
return AuthResult::success($keyConfig['permissions']);
}
private function checkRateLimit(string $apiKey): bool
{
$cacheKey = 'rate_limit_' . hash('xxh3', $apiKey);
$item = $this->cache->getItem($cacheKey);
$requests = $item->isHit() ? $item->get() : 0;
if ($requests >= self::RATE_LIMIT_MAX_REQUESTS) {
return false;
}
$item->set($requests + 1);
$item->expiresAfter(self::RATE_LIMIT_WINDOW);
$this->cache->save($item);
return true;
}
private function checkQueryComplexity(Request $request, array $keyConfig): bool
{
$query = $request->getContent();
$maxDepth = $keyConfig['maxQueryDepth'] ?? 10;
$maxComplexity = $keyConfig['maxQueryComplexity'] ?? 500;
$depth = $this->calculateQueryDepth($query);
$complexity = $this->calculateQueryComplexity($query);
return $depth <= $maxDepth && $complexity <= $maxComplexity;
}
}Rate limiting per API key voorkomt misbruik. Query complexity limits beschermen tegen denial-of-service via diepe nested queries.
Frontend integratie met Apollo Client
Type-safe GraphQL integratie met automatische code generatie zorgt voor maintainable frontends:
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: 'https://pimcore.example.com/pimcore-datahub-webservices/product-api',
documents: ['src/**/*.graphql'],
generates: {
'./src/generated/graphql.ts': {
plugins: [
'typescript',
'typescript-operations',
'typescript-react-apollo'
],
config: {
withHooks: true,
withHOC: false,
withComponent: false
}
}
}
};
export default config;import { useGetProductCatalogQuery } from '@/generated/graphql';
import { useLocale } from 'next-intl';
export function useProductCatalog(options: {
categoryId?: number;
minPrice?: number;
maxPrice?: number;
inStock?: boolean;
pageSize?: number;
}) {
const locale = useLocale();
const { data, loading, error, fetchMore } = useGetProductCatalogQuery({
variables: {
locale,
categoryId: options.categoryId,
minPrice: options.minPrice,
maxPrice: options.maxPrice,
inStock: options.inStock ?? true,
first: options.pageSize ?? 20,
},
fetchPolicy: 'cache-and-network',
nextFetchPolicy: 'cache-first',
});
const loadMore = () => {
if (!data?.getProductListing?.pageInfo.hasNextPage) return;
fetchMore({
variables: {
after: data.getProductListing.pageInfo.endCursor,
},
});
};
return {
products: data?.getProductListing?.edges.map(e => e.node) ?? [],
pageInfo: data?.getProductListing?.pageInfo,
loading,
error,
loadMore,
};
}Met GraphQL Codegen zijn alle types automatisch gesynchroniseerd met je Pimcore schema. Apollo's normalized cache zorgt voor efficiënt data management zonder duplicatie.
Headless Pimcore implementatie?
Wij bouwen schaalbare headless architecturen met Pimcore als backend voor uw moderne frontends.
Neem contact op