Custom Software11 min read·

Pimcore Classification Store in practice: data modeling, migration, and API integration

How we migrated variant-heavy product data into Pimcore Classification Store: modeling, idempotent imports, and API integration.

Quick answer

Classification Store is the right Pimcore structure when attribute sets differ widely between product families and should be editor-managed. Keep shared, frequently-filtered attributes as plain class fields, stage and validate before importing, write idempotent imports keyed on an external id, and disable versioning during the bulk load.

Migrating a large, variant-heavy product catalogue into Pimcore is where Classification Store earns its place. Picture thousands of products, each with dozens of technical attributes that differ by product family, and values that have to stay traceable for audit and compliance. Plain fields on a Pimcore DataObject would not hold that structure without turning the class into an unmanageable wall of mostly-empty attributes.

Classification Store solved the modeling problem, but it brought its own trade-offs around performance, localization, and import noise. This is a practical account of the decisions that held up, the code patterns we relied on, and one thing we would do differently. It assumes you already know what Pimcore is and are deciding how to model and migrate real product data.

When Classification Store beats bricks and plain attributes

Pimcore gives you three ways to attach attributes to a DataObject. Plain class fields are the simplest and fastest to query, but they live on the class, so every product family carries the same field set. Object bricks let you attach optional blocks of fields, which helps when only some products need a particular group. Classification Store goes further: it stores values in a separate key-value structure organized into groups, and which groups apply can vary per object.

Classification Store earns its complexity when the attribute set is genuinely dynamic. We reached for it in three situations:

  • The attribute set differs widely between product families and would otherwise bloat the class with fields that are empty for most products.
  • The list of attributes changes often and should be editable by data managers rather than shipped in a code deploy.
  • Editors need attributes grouped and ordered in the UI without a developer touching the class definition each time.

The catch: Classification Store values are not first-class class fields. You cannot filter a product listing on a store key with the same ease as a plain column, and every read goes through the store API. If an attribute is central to querying and shared by every product, keep it as a plain field. Classification Store is for the long tail of family-specific, editor-managed attributes.

We plan and build Pimcore data models, migrations, and integrations end to end.

Pimcore development services →

Modeling groups and keys for variant-heavy data

The two core structures in Classification Store are groups and keys. A key is a single attribute definition: its name, data type, and whether it is localized. A group is a named collection of keys. You assign groups to an object, and the object then holds values for the keys in those groups. For variant-heavy data we settled on one group per product family, plus a shared group for attributes common to every family.

That kept editors focused. A product in one family only saw the groups that applied to it instead of scrolling past hundreds of irrelevant keys. It also made the import mapping explicit, because each source attribute mapped to exactly one group and one key.

php
<?php
use Pimcore\Model\DataObject\Product;

$product = Product::getById($id);
$store   = $product->getAttributes(); // Classificationstore instance

// Group and key ids come from the Classification Store definition.
$store->setLocalizedKeyValue($groupId, $keyId, '316L stainless steel', 'en');

$product->save();

Values are addressed by numeric group and key ids, not by name. Resolve those ids once from the definition and cache the mapping. Looking them up per row during a large import is a common and avoidable performance mistake.

Migration approach: stage, validate, import idempotently

A migration into a regulated PIM is not a one-shot script. We ran it in three passes: stage the raw source into an intermediate schema, validate against the target model, then import with an idempotent writer. Each pass is independently repeatable, which matters when the first full run surfaces data problems you have to fix at the source.

  • Staging: load the source exports as-is into a staging schema with the source primary key preserved. No transformation yet, so you can always trace a value back to its origin.
  • Validation: check every row against the target model (required attributes present, enum values known, units parseable) and write a report. Nothing imports until that report is clean enough to accept.
  • Import: upsert by a stable external key so a re-run never creates duplicates.

Idempotency is the part teams skip and regret. Product imports get re-run: a source fix, a mapping change, a partial failure halfway through. If the writer keys on an external id and updates in place, a re-run is safe. If it blindly creates objects, a re-run doubles your catalogue.

php
<?php
use Pimcore\Model\DataObject\Product;
use Pimcore\Model\Element\Service;

$existing = Product::getByExternalId($externalId, 1);
$product  = $existing instanceof Product ? $existing : new Product();

if (!$existing) {
    $product->setExternalId($externalId);
    $product->setParent($familyFolder);
    $product->setKey(Service::getValidKey($sku, 'object'));
}

$product->setPublished(false); // reviewed publish happens in a later pass
applyClassificationValues($product, $row);
$product->save();

We kept objects unpublished until a reviewer signed off, so a bad import never reached the live channel. In a regulated setting that review gate is not optional, and building it into the import from day one is cheaper than bolting it on after go-live.

Not sure whether your catalogue even needs Pimcore, or how to structure PIM and DAM together? That is the conversation we start with.

PIM and DAM consulting →

Integrating through the REST and Data Hub API

Once the data was in, downstream systems needed to read it. Pimcore exposes two main integration surfaces: the web-service REST API and the Data Hub GraphQL endpoint. For read-heavy integrations that pull specific fields, the GraphQL endpoint was the better fit, because a consumer asks for exactly the attributes it needs and nothing more.

graphql
query Product {
  getProduct(id: 417) {
    id
    sku
    material
    sterilizationMethod
  }
}

The same query runs over plain HTTP against the Data Hub endpoint, which makes it easy to wire into any consumer that speaks JSON.

json
{
  "query": "query { getProduct(id: 417) { id sku material } }"
}

Two things to plan for. Data Hub exposes only the fields you enable in its configuration, so a new attribute is invisible to consumers until the endpoint is updated. And Classification Store keys surface in GraphQL under the names you configure, so the API contract and the store definition have to stay in sync. We generated the Data Hub field list from the same mapping that drove the import, so the two could not drift apart.

Pitfalls we hit

Performance with large key sets. Classification Store reads are cheap for a handful of keys and get expensive when an object carries hundreds. Loading a product listing and touching every store value pulled far more rows than we expected. The fix was to read only the groups a given view needed and to cache the group and key id mapping instead of resolving names on each access.

Localization of attribute values. Each key is either localized or not, and getting that flag wrong is painful to reverse. A value stored as non-localized cannot simply be split per language later without a data migration. We erred toward localized for anything a human writes, such as labels and descriptions, and non-localized for pure measurements and enums. Deciding this per key up front saved us a second migration.

Versioning noise during bulk import. Every save creates a version. A first import of thousands of objects, each saved at least once, generated a version history that buried the meaningful editorial changes and inflated the database. For the bulk load we disabled versioning on the import path and turned it back on before handing the system to editors.

php
<?php
use Pimcore\Model\Version;

// Silence version + notification noise during a bulk import.
Version::disable();
try {
    foreach ($rows as $row) {
        importProduct($row);
    }
} finally {
    Version::enable();
}

What we would do differently

One honest correction. We modeled a handful of borderline attributes as Classification Store keys that, in hindsight, belonged on the class as plain fields. They were used in almost every listing filter, and routing those reads through the store cost query performance we then had to engineer around. If an attribute is queried often and shared by nearly all products, the convenience of editor-managed keys is not worth the read cost. Next time we would draw that line earlier and keep the store for the genuinely family-specific long tail.

Planning a Pimcore migration or a Classification Store model? We help teams get the data model right before the import, so the migration runs once. Tell us about your catalogue and the systems it has to feed.

Talk to us about a Pimcore migration →

Frequently asked questions

Does Pimcore have a REST API for integrations?

Yes. Pimcore ships web-service REST endpoints, and the Data Hub adds a configurable GraphQL endpoint. For read-heavy integrations the GraphQL endpoint is usually the better fit, because a consumer requests exactly the fields it needs. Data Hub only exposes the fields you enable, so a new attribute stays invisible until the endpoint configuration is updated.

When should you use Classification Store instead of object bricks?

Use object bricks when a fixed, developer-defined block of fields applies to some objects. Use Classification Store when the attribute set differs widely between product families, changes often, and should be managed by data editors without a code deploy. Attributes that every product shares and that you filter listings on belong on the class itself, not in the store.

How long does a Pimcore data migration take?

It depends far more on source data quality than on record count. A clean catalogue of a few thousand products can be modeled and migrated in a few weeks. Messy source data, unclear attribute ownership, and localization decisions are what stretch the timeline, which is why we stage and validate before importing anything.