Search by

kerimkaralic / pimcore-gatekeeper-bundle

solo.ferroviere

Completeness gate for Pimcore DataObjects: per-class required fields, profiles and languages, scored on save, with reports and a publish gate.

Package info

github.com/Kera95/pimcore-gatekeeper-bundle

Type:pimcore-bundle

pkg:composer/kerimkaralic/pimcore-gatekeeper-bundle

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-09-11 19:29 UTC

README

Codeception Quality

A completeness gate for Pimcore DataObjects, for Pimcore 11.x, 12.x and 2026.x. You declare per class which fields have to be filled, optionally per language and per profile (channel, market, print, ...). The bundle scores every object on save, keeps the result in its own table, shows it as a report in the admin UI and on the console, can export it into the asset tree, and can warn or block when an incomplete object is published.

  • Composer package: kerimkaralic/pimcore-gatekeeper-bundle
  • Namespace: Tsf\GatekeeperBundle
  • Bundle class: Tsf\GatekeeperBundle\TsfGatekeeperBundle
  • Config alias: tsf_gatekeeper
  • License: MIT

Pimcore's own Data Quality bundle is an Enterprise feature; this is the Community Edition answer to "which products are not ready to go out, and what exactly is missing".

Requirements

PHP >=8.1, <8.6
Pimcore ^11.0, ^12.0, or ^2026.1
Symfony ^6.2 or ^7.3

The bundle uses Pimcore model events and its own database table only. It works with the Classic Admin and with Pimcore Studio; the admin report needs PimcoreCustomReportsBundle, which ships with Pimcore since 11 and only has to be enabled.

Installation

composer require kerimkaralic/pimcore-gatekeeper-bundle

Register the bundle in config/bundles.php:

return [
    // ...
    Tsf\GatekeeperBundle\TsfGatekeeperBundle::class => ['all' => true],
];

Install it (creates the table tsf_gatekeeper_result) and clear the cache:

bin/console pimcore:bundle:install TsfGatekeeperBundle
bin/console cache:clear

Configuration

Create config/packages/tsf_gatekeeper.yaml. A complete, commented example is in docs/configuration.example.yaml.

tsf_gatekeeper:
    classes:
        Product:
            required: [sku, name, title, description, main_image, price]
            languages: [en, de]          # optional, default: all valid system languages
            threshold: 100               # optional, default 100
            gate: warn                   # off | warn | block, default warn
            score_field: completeness    # optional Numeric field on the class
            profiles:                    # optional additional rule sets
                print:
                    required: [name, ean, long_description]
                    languages: [de]
                    threshold: 80
    report:
        studio: true
        asset:
            enabled: false
            folder: /reports/completeness
            formats: [csv, md]
            on_save: false

Check the configuration against the installed classes, fields and languages at any time:

bin/console tsf:gatekeeper:validate

Reference

Key Default Meaning
enabled true Master switch for the save listener. Commands work regardless.
classes.<Class>.required [] Field names that must be filled: top-level fields, children of localizedfields by their plain name, or container.Type.field for object brick and field collection fields. Becomes the profile named default.
classes.<Class>.languages [] Languages evaluated for localized fields. Empty means every valid system language.
classes.<Class>.threshold 100 Score from which an object counts as complete, 0-100.
classes.<Class>.gate warn off, warn or block, see below.
classes.<Class>.score_field null Numeric field that receives the aggregate score on every save.
classes.<Class>.enabled true Disable a class without deleting its rules.
classes.<Class>.profiles.<name>.required required Fields of this profile.
classes.<Class>.profiles.<name>.languages class value Override the class languages for this profile.
classes.<Class>.profiles.<name>.threshold class value Override the class threshold for this profile.
report.studio true Register the two Custom Reports when PimcoreCustomReportsBundle is enabled.
report.asset.enabled false Allow tsf:gatekeeper:report --asset to write into the asset tree.
report.asset.folder /reports/completeness Target asset folder, created on demand.
report.asset.formats [csv] csv (one file per class, every row) and/or md (one summary.md).
report.asset.on_save false Also rewrite the files after every save of a tracked object.

A class needs a non-empty required list or at least one profile. default is reserved as a profile name. Thresholds, gate values, language codes and duplicate field names are validated when the container is built; classes, fields and system languages are validated by tsf:gatekeeper:validate because they only exist at runtime.

How the score works

  • Every profile is scored separately: score = round(100 × filled / required).
  • If a profile contains at least one localized field it produces one row per language; non-localized fields count the same in every language row. Profiles without localized fields produce a single row with an empty language.
  • Values are read without language fallback, so a German title that only exists through the English fallback is missing in German.
  • A row passes when its score reaches the profile threshold.
  • The aggregate score of an object is the lowest row score. That is what goes into the score_field and what the gate looks at.
  • Variants are evaluated like any other object. Folders are ignored.
  • Fields inside object bricks and field collections are listed as container.Type.field, e.g. bricks.Dimensions.width or features.Feature.label. A brick field is filled when the brick is set and the field is filled; a field collection field is filled when any item of that type has it filled. Localized fields inside bricks and collections work the same way as on the class. Listing the container itself (bricks, features) means "at least one brick / item". Classification store keys are not supported.

What counts as empty

The bundle starts from Pimcore's own isEmpty() per data type and corrects the cases where that is wrong for completeness:

Type Filled when
Input, Textarea, Email, Password not only whitespace ("0" is a value)
WYSIWYG text remains after stripping tags and &nbsp;
Numeric, Slider a number, including 0
Checkbox true or false (null is empty)
QuantityValue, InputQuantityValue a value is set, 0 included; a unit alone is empty
Hotspot image an image is assigned
Video the video data (asset or external id) is set
Link a direct URL or an internal target exists
Table at least one non-blank cell
Field collection at least one item
Object bricks at least one brick set and not marked for deletion
Everything else Pimcore's isEmpty() (relations: at least one element, select: non-empty value, ...)

To change or extend this, implement Tsf\GatekeeperBundle\Service\Emptiness\EmptinessResolverInterface in a service; it is tagged tsf_gatekeeper.emptiness_resolver automatically and the first resolver that supports() a field definition wins.

The gate

The gate only concerns published objects; unpublished saves and drafts always go through.

gate Behaviour when a published object is saved with a failing row
off Nothing.
warn A warning is logged: Completeness gate: Product "ABC-123" is incomplete. default/de (71% < 100%): title, short_description
block The save is rejected with a ValidationException carrying the same message. Studio shows it as the error of the save (HTTP 422), the Classic Admin as its validation dialog.

Reports

Admin UI

With PimcoreCustomReportsBundle enabled the bundle registers two reports in the group Completeness (Studio: Reporting; Classic: Marketing > Custom Reports):

  • Completeness: objects — one row per object, profile and language with score, threshold, passed, missing count and the missing field names. Sortable and filterable, the ID column opens the object, CSV export included.
  • Completeness: summary — per class, profile and language: number of objects, average score, complete and failing counts.

Console

bin/console tsf:gatekeeper:report                    # every class, grouped per class and profile
bin/console tsf:gatekeeper:report -c Product -f      # only failing rows of one class
bin/console tsf:gatekeeper:report --below 50         # rows scoring below 50
bin/console tsf:gatekeeper:report -p print --language de
bin/console tsf:gatekeeper:report --format=csv > completeness.csv
bin/console tsf:gatekeeper:report --format=md        # Markdown tables, paste into a ticket
bin/console tsf:gatekeeper:report --summary          # one line per class, profile and language
 Product · profile default · 4 objects · avg 64 % · 1 complete · 3 failing (threshold 100)
id key         lang pub score  missing
2  ABC-123     de   yes  100 %
6  PCSA-001    de   yes ! 71 % title,short_description
6  PCSA-001    en   yes  100 %
8  HPHC-001    en   yes ! 57 % short_description,price,categories

Asset tree

With report.asset.enabled: true, tsf:gatekeeper:report --asset writes the configured formats into report.asset.folder:

  • <Class>.csv per class with every row (object_id, key, path, class, published, profile, language, score, threshold, passed, missing_count, missing_fields, calculated_at)
  • summary.md with totals per class/profile/language, the ten most frequently missing fields and the failing objects

Files are overwritten in place, so the asset versions keep the history. --timestamp appends -YYYYmmdd-HHMM to the filenames instead. This works on every supported Pimcore line and without the Custom Reports bundle, and gives people without admin access a file they can open or download.

Commands

Command Purpose
tsf:gatekeeper:validate Checks every class rule against the installed classes, fields, score field type and system languages. Exit code 1 on problems.
tsf:gatekeeper:recalculate [-c Class] [--limit N] [--dry-run] [--save] Evaluates all objects of the configured classes and rewrites the result table. Objects are not saved unless --save is given, which also refreshes the score field, versions and the search index (slow).
tsf:gatekeeper:report [...] Prints the object rows, or with --summary the totals per class, profile and language; --format table|csv|md; --asset writes the files.
tsf:gatekeeper:add-score-field <Class> [--name=completeness] [--panel=<layout name>] Adds a Numeric 0-100 field to a class so score_field can be used.

Run recalculate once after installing or after changing the rules; from then on every save keeps the table current.

The score field

score_field is optional. The result table is always the source of truth; the field is a mirror of the aggregate score written through the generated setter during the same save, so it is versioned and indexed like any other value and can be used in grids, listings and code:

$product->getCompleteness(); // 71

If the configured field is missing or not Numeric the bundle logs one warning per class, skips the mirror and never blocks the save; tsf:gatekeeper:validate reports the exact problem.

How it works

  • pimcore.dataobject.preAdd / preUpdate: the object is evaluated, the score field is set, the gate is applied. An exception in the evaluation is logged and never blocks the save.
  • pimcore.dataobject.postAdd / postUpdate: the rows are written to tsf_gatekeeper_result (one per profile and language; rows of removed profiles are deleted).
  • pimcore.dataobject.postDelete: the rows of the object are removed.
  • Nothing is saved twice, no flags, no queue. Studio autosave and "save version" do not dispatch preUpdate, so drafts are scored on the next real save.

Testing

Two Codeception suites:

composer install

# unit: no database, no Pimcore kernel
vendor/bin/codecept run Unit

# functional: boots a minimal Pimcore kernel against a real database
PIMCORE_TEST_DB_DSN=mysql://root:root@127.0.0.1:3306/tsf_gatekeeper_test vendor/bin/codecept run Functional

The unit suite covers the configuration, rule normalisation, the emptiness rules per data type, the evaluator, the listener with every gate mode, the report rendering and the report definitions.

The functional suite runs the bundle inside a throwaway Pimcore project (tests/Support/App, Pimcore core plus the Custom Reports bundle) and covers what only a real save can show: the installer, the gate refusing a publish, the score field and the result rows written during a save, the delete hook, every console command, the asset export and the Custom Reports definitions. The database named in PIMCORE_TEST_DB_DSN is dropped and recreated on every run, so point it at a dedicated one. No Pimcore product key is needed; the test project boots with the "needs install" marker that skips the registration check.

From inside a Pimcore project that consumes the bundle as a path package, run the suites with the project's vendor directory, e.g. with the skeleton's test compose profile:

docker compose exec -T php sh -c 'cd bundles/Tsf/GatekeeperBundle && ../../../vendor/bin/codecept run Unit'
docker compose run --rm test-php sh -c 'cd bundles/Tsf/GatekeeperBundle && ../../../vendor/bin/codecept run Functional'

Static analysis and code style

composer phpstan     # PHPStan level 6 over src/ and tests/
composer cs:check    # PHP-CS-Fixer, report only
composer cs:fix      # PHP-CS-Fixer, write the changes

PHPStan is configured for the whole supported PHP range (8.1 to 8.5), so a call that only exists in a newer version is reported even when the analysis runs on the newest one. Both tools run in CI on every push.

Contributing

Branching model, pull request rules and how to run the suite: CONTRIBUTING.md. Changes are tracked in CHANGELOG.md.

License

MIT, see LICENSE.