basilicom / pimcore-fixtures
Export Pimcore content to version-controlled YAML and load it into any environment. Follows cross-domain references automatically — objects, documents, assets, and system config in one command.
Package info
github.com/basilicom/pimcore-fixtures
Type:pimcore-bundle
pkg:composer/basilicom/pimcore-fixtures
Requires
- php: ^8.3
- laravel/prompts: ^0.3
- pimcore/pimcore: ^12.0
- pimcore/platform-version: ^2025.0
- pimcore/symfony-freeze: ^7.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.89
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.0
This package is auto-updated.
Last update: 2026-06-18 07:46:23 UTC
README
Stop rebuilding test content by hand. Put your Pimcore content in Git and recreate it anywhere with one command.
Every Pimcore team has lost a day to it: a new developer needs a working dataset, a CI run needs known content, a customer bug only reproduces against their data. The usual answer is a stale, oversized database dump full of someone else's test garbage. This bundle replaces that with plain YAML you can commit, diff, and review.
Point it at a folder in your Pimcore tree and it writes everything it finds to YAML — data objects, documents, assets, plus the system config they depend on. Commit those files, and any teammate or environment recreates the exact same Pimcore state with bin/console basilicom:pimcore:fixtures:load.
What sets it apart: it follows cross-domain references automatically. Export /products and the categories, hero images, and landing pages those products point to come along too — including parent folders, so every path resolves on the other side. Most fixture tools dump objects and stop. This one brings the whole graph.
Because fixtures are readable YAML with @key references, a content change shows up as a reviewable Git diff — and --diff previews exactly what a load would change before you run it.
Typical use cases:
- Onboard developers in minutes — clone,
load, done. No begging a colleague for a DB dump. - Deterministic CI —
reset --force && loadgives every test run identical, known content. - Reproducible bug reports — snapshot the affected subtree (refs followed) and debug locally against the real data.
- Ship demo data with your product — bundle a curated dataset; the installer loads it and the demo looks great instantly.
- Config as code — keep sites, roles, glossary, units, and classification stores in version control and promote them env-to-env.
Works with Pimcore 12 / PHP 8.3 / Symfony 7.
Installation
composer require --dev basilicom/pimcore-fixtures
Register the bundle in config/bundles.php:
return [ // ... Basilicom\PimcoreFixtures\PimcoreFixturesBundle::class => ['dev' => true], // ... ];
Optional config in config/packages/basilicom_pimcore_fixtures.yaml:
basilicom_pimcore_fixtures: path: '%kernel.project_dir%/fixtures' # where fixture files live format: yaml # fixture file format: yaml (default) or json ignored_fields: [] # DataObject field names to skip ignored_classes: [] # DataObject classes to skip ignored_paths: [] # Pimcore paths to skip ignored_properties: [] # Pimcore property names to skip on documents/assets/dataObjects ignored_website_settings: [] # Website setting names to skip
Defaults are sensible — path falls back to var/bundles/PimcoreFixtures. Empty values (null, '', []) and inherited values are skipped during generation to keep files clean.
format controls the serialization of every fixture file the bundle writes and reads — data objects, documents, asset metadata, and all pimcore/* config fixtures. With yaml (the default), loading accepts both .yaml and .yml; with json, only .json files are read.
Switching formats does not convert existing fixtures. After changing
format, files in the old format become invisible to both export-merge andfixtures:load. Convert them manually (the data structures are identical) or regenerate from a seeded system, and remove the old files.
ignored_properties matches on Property::getName() and applies wherever Pimcore properties are exported (document YAML, asset _metadata.yaml, and dataObject property holders). ignored_website_settings matches on WebsiteSetting::getName() and filters entries before they are written to pimcore/website_settings.yaml.
How it works
The bundle has two sides:
- Generate — read live Pimcore content and write YAML fixture files.
- Load — read those YAML files and recreate the content in another Pimcore instance.
Loading is safe to re-run: by default an element that already exists at the same path is left
untouched and only missing elements are created. Pass --overwrite to also update existing
elements (see Loading fixtures below). Either way, content is never duplicated.
Below is a tour of the main commands. Run any of them with --help to see all options.
What gets exported
Everything at once
bin/console basilicom:pimcore:fixtures:generate
The umbrella command: exports the complete content set in one run — all data objects, documents, and asset binaries from their tree roots, plus every system-config fixture (sites, units, glossary, website settings, roles, classification store, translations). Use --levels to cap tree depth, --skip-translations to leave translations out, and --force to overwrite instead of merge.
For scoped or interactive exports, use the dedicated per-domain commands below.
Data objects
bin/console basilicom:pimcore:fixtures:generate-objects
Without arguments it opens an interactive picker (filter, ↑/↓, space, enter) to choose one or more folders from your data object tree. Pass --folder=/products to skip the picker, or --all to export everything.
By default it also follows cross-domain references: if a Product points to a Category, a hero image, and a landing page, all four are pulled into the same export — including parent folders so paths resolve cleanly on load. Disable with --no-follow-refs.
Re-running merges new entries into existing fixture files. Pass --force to overwrite from scratch.
Documents
bin/console basilicom:pimcore:fixtures:generate-documents
Same flags as generate-objects. Exports pages, snippets, links, emails, folders, and hardlinks to {path}/documents/. Includes title, description, pretty URL, controller, template, editables, link targets, properties, and the original sibling index so loaded documents keep their Pimcore tree order. Reference following pulls in any data objects and assets the documents link to.
Hardlinks are exported with their source (as an @key reference to the source document) and the childrenFromSource / propertiesFromSource flags. The source document is pulled into the export via reference following, so the link resolves on load.
Assets
bin/console basilicom:pimcore:fixtures:generate-assets
Copies asset binaries into {path}/assets/, mirroring the Pimcore asset tree. On load, files are restored to their original Pimcore paths. Same flag set as the other generators (interactive picker, --folder, --all, --levels, --force, …).
You can also drop replacement files into fixtures/assets/ manually — merge mode leaves existing binaries untouched.
Per-asset state that can't be expressed by the binary itself is written to {path}/assets/_metadata.yaml: own (non-inherited) Pimcore properties and Pimcore asset metadata items (the "Metadata" tab — name/type/data/language, with element-typed values stored as paths). The asset seeder reads this sidecar after creating the binaries and applies both.
System configuration
A few Pimcore-wide settings travel with your fixtures, written as single files under {path}/pimcore/:
| File | Contains |
|---|---|
sites.yaml |
Sites and their root documents |
website_settings.yaml |
Website settings (with element references resolved by path) |
glossary.yaml |
Glossary entries |
units.yaml |
QuantityValue units |
classification_store.yaml |
Stores, groups, keys, collections (the config, not field values) |
roles.yaml |
Roles, role folders, permissions, workspaces |
shared_translations.yaml |
Shared translations (default messages domain) |
admin_translations.yaml |
Admin UI translations (admin domain) |
All of them are emitted when you run generate. Classification stores, roles, and translations also have their own dedicated commands if you want to regenerate just one:
bin/console basilicom:pimcore:fixtures:generate-classification-store bin/console basilicom:pimcore:fixtures:generate-roles bin/console basilicom:pimcore:fixtures:generate-translations
Translation file format — each top-level key is a translation key, each value is a locale → text map:
my.label.headline: de: 'Überschrift' en: 'Headline' my.label.subtitle: de: 'Untertitel' en: 'Subtitle'
On load, missing keys are created; existing keys are left untouched by default and only updated with --overwrite. Locales not configured in Pimcore are skipped with a warning. Pass --skip-translations to generate or load to opt out entirely.
File-based config that already lives in
var/config/— static routes, predefined properties, document types — isn't fixturized. Commit those files directly.
Loading fixtures
bin/console basilicom:pimcore:fixtures:load
Loads everything in the fixtures folder back into Pimcore in the right order: units and classification config first, then assets, document shells, data objects (in two passes so circular relations work), document content, sites, glossary, translations, website settings, and finally roles.
Useful options:
| Option | Effect |
|---|---|
--dry-run |
Preview without writing |
--diff |
Preview what would change in the live data object tree, field by field, without writing. Implies --dry-run. |
--overwrite |
Update elements that already exist at the same path. Without it, existing elements are skipped and only missing ones are created. |
--force |
Required to write in the prod environment. No effect elsewhere. |
--omit-validation |
Skip mandatory-field validation when saving objects |
--skip-translations |
Don't load shared/admin translation fixtures |
Existing elements — skip vs. overwrite. Data objects, documents and assets are matched by their
Pimcore path (ids differ between environments) through a single ExistingElementResolver; config
fixtures are matched by their natural key. The behaviour is one consistent axis across all of them:
- default (no
--overwrite) — an element that already exists at the target path is left completely untouched; only missing elements are created. The safe default for promoting a subtree into a populated environment. --overwrite— the fixture's fields are written onto the existing element. Fields the fixture does not mention keep their live values (generation omits empty/inherited fields, so "unmentioned" means "the fixture has nothing to say about it"). For assets this also replaces the binary.
Neither mode clears fields the fixture omits. A clean snapshot — wiping fields that exist only in
the target — is reset --force followed by load, not a load mode.
--overwrite applies to every domain — data objects, documents, assets, and the system-config
fixtures (sites, website settings, glossary, units, classification-store config, roles, translations).
The config fixtures match existing entries by their natural key (translation key, unit id, role name,
site domain, store/group/key name, …): in the default mode an existing entry is left untouched, with
--overwrite it is updated.
One exception by necessity: the classification-store config still descends into an existing store to create any missing groups/keys even in the default mode (object hydration needs those to exist) — it just doesn't modify the ones already there. Existing groups/keys are only updated with
--overwrite.
Prod safety net. In the prod environment a writing load refuses to run unless --force is
passed. Read-only previews (--dry-run, --diff) are always allowed.
--diff loads nothing — it compares each fixture data object against the live object at the same path and prints the differences:
~ /products/example-shoe (Product)
name: 'Old Shoe' → 'New Shoe'
price: 49.9 → 39.9
+ /products/brand-new (Product) [new]
Diff: 1 new, 1 changed.
Use it to sanity-check a load against a populated environment before committing to it. Field-level detail covers data objects; documents, assets, and system config run as a dry run under --diff.
--diffshows the overwrite view (what--overwritewould change). In the default mode existing elements are skipped, so a plainloadwould not apply those changes — only create the entries marked[new].
Schema drift: if a fixture references a data object field that no longer exists on the current class definition (renamed or removed since the fixture was generated), the load no longer aborts. The field is skipped and surfaced as a warning in the end-of-run summary, so a stale fixture degrades gracefully instead of crashing the whole import.
Resetting
bin/console basilicom:pimcore:fixtures:reset --force
Truncates Pimcore content tables before a fresh load. The --force flag is mandatory in every environment — it is the deliberate gate between you and an empty database, on prod as much as on dev.
Scope it with --types (objects, documents, assets), --classes, --exclude, or --drop-folder. Some shared tables (notes, edit_lock, search_backend_data, recyclebin) are always truncated.
Fixture file format
Fixtures are plain YAML (or JSON, see the format config option) keyed by class and fixture id, with @key references between entries. The structure is identical in both formats — JSON merely escapes the backslashes in class-name keys ("\\App\\Model\\DataObject\\Product").
Data object example:
\App\Model\DataObject\Product: 001_product_example_abc123: key: example-product parentId: 1 published: true name: 'Example product' category: '@002_category_shoes_def456' # cross-fixture reference
Document example:
\Pimcore\Model\Document\Page: 003_page_about_xyz789: key: about-us parent: '@001_folder_content_abc123' title: 'About Us' template: '@AppBundle/Default/default.html.twig' properties: - { name: nav_title, type: text, data: About, inheritable: false }
Classification store field values on a data object are nested by groupName → keyName → language:
classificationAttributes: technicalSpecs: weight: default: '2.5 kg' marketingData: headline: de: Produktüberschrift en: Product headline
Extending
You can teach the bundle about new Pimcore field types or custom load behaviour without forking it.
- Custom field type — add a transformer in
src/Generation/DataTransformer/for export, and a hydrator implementingChainedPropertyHydratorInterfacefor load. Tag the hydrator service withbasilicom_pimcore_fixtures.property_hydratorand a priority. - Custom property hydrator — same tag, used as a fallback when none of the built-in hydrators match.
- Pre/post processors — extend
AbstractProcessorand tag withbasilicom_pimcore_fixtures.pre_processororbasilicom_pimcore_fixtures.post_processorto run code around each object save. - Custom fixture format — implement
FixtureFormatInterfaceand tag the service withbasilicom_pimcore_fixtures.fixture_format. Note that theformatconfig option validates against the built-in names (yaml,json), so a custom format also needs that enum extended via a PR.
See AGENTS.md for architectural details.
Contributing
You don't need a local PHP or Composer install — everything runs in Docker via make.
git clone git@github.com:basilicom/pimcore-fixtures.git
cd pimcore-fixtures
make setup
Common tasks:
| Command | Purpose |
|---|---|
make lint |
PHP-CS-Fixer + PHPStan |
make lint-php-fix |
Auto-fix style issues |
make test-unit |
Run PHPUnit |
make help |
List everything |
Run make lint && make test-unit before opening a PR. Architecture and conventions are documented in AGENTS.md.