Terug naar blog
pimcoregraphqlheadlessapidatahub

Pimcore Headless met GraphQL: een complete gids

30 januari 202614 min leestijd

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