PHPackages                             setono/sylius-completeness-plugin - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. setono/sylius-completeness-plugin

ActiveSylius-plugin[Utility &amp; Helpers](/categories/utility)

setono/sylius-completeness-plugin
=================================

Sylius plugin that computes a weighted, per-channel/per-locale enrichment completeness percentage for products.

00PHPCI passing

Since Jul 4Pushed 1mo agoCompare

[ Source](https://github.com/Setono/sylius-completeness-plugin)[ Packagist](https://packagist.org/packages/setono/sylius-completeness-plugin)[ RSS](/packages/setono-sylius-completeness-plugin/feed)WikiDiscussions master Synced 1w ago

READMEChangelogDependenciesVersions (1)Used By (0)

Setono Sylius Completeness Plugin
=================================

[](#setono-sylius-completeness-plugin)

[![Latest Version](https://camo.githubusercontent.com/291a2ce97d474b7f3d7b8d6b205f43eade63527e0f718c9fc2cffeeaea7c750a/68747470733a2f2f706f7365722e707567782e6f72672f7365746f6e6f2f73796c6975732d636f6d706c6574656e6573732d706c7567696e2f762f737461626c65)](https://packagist.org/packages/setono/sylius-completeness-plugin)[![Software License](https://camo.githubusercontent.com/26510f7cec9298e2371d78c81e4809096c17b98fac9e78722ce35a3b20485d46/68747470733a2f2f706f7365722e707567782e6f72672f7365746f6e6f2f73796c6975732d636f6d706c6574656e6573732d706c7567696e2f6c6963656e7365)](LICENSE)[![Build Status](https://github.com/Setono/sylius-completeness-plugin/actions/workflows/build.yaml/badge.svg)](https://github.com/Setono/sylius-completeness-plugin/actions/workflows/build.yaml)[![Code Coverage](https://camo.githubusercontent.com/d45b1a7d1e9185aabe2cd41978a952b35901f4e40da3f6a207eeede44b936ea6/68747470733a2f2f636f6465636f762e696f2f67682f5365746f6e6f2f73796c6975732d636f6d706c6574656e6573732d706c7567696e2f6272616e63682f6d61737465722f67726170682f62616467652e737667)](https://codecov.io/gh/Setono/sylius-completeness-plugin)[![Mutation testing badge](https://camo.githubusercontent.com/cc387504394439e4c6c45495b21c809532ec214618c1c5e536f729fce8a68af6/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f7374796c653d666c61742675726c3d687474707325334125324625324662616467652d6170692e737472796b65722d6d757461746f722e696f2532466769746875622e636f6d2532465365746f6e6f25324673796c6975732d636f6d706c6574656e6573732d706c7567696e2532466d6173746572)](https://dashboard.stryker-mutator.io/reports/github.com/Setono/sylius-completeness-plugin/master)

Compute a **weighted, per-channel/per-locale enrichment completeness percentage** for your Sylius products, persist it, roll it up to a single global score on the product and surface it across the admin: a **Completeness dashboard** (catalog-wide figures, score distribution and the products most in need of work) reached from a single admin menu item, a grid column (threshold color-coded, stale-aware) with a numeric range filter, a channel × locale breakdown panel on the product show and edit pages, a rule CRUD and a "test against a product" preview with a live expression scratchpad.

Scoring rules are **database-backed and admin-managed**, with three tiers of flexibility:

1. **Curated checkers** — discoverable built-ins (`has_image`, `has_price`, `has_minimum_images`, …).
2. **Developer checkers** — implement an interface, tag the service, done.
3. **ExpressionLanguage rules** — authored entirely in the UI, with a rich helper library.

Installation
------------

[](#installation)

### 1. Require the plugin

[](#1-require-the-plugin)

```
composer require setono/sylius-completeness-plugin
```

### 2. Register the bundle

[](#2-register-the-bundle)

```
# config/bundles.php

return [
    // ...
    Setono\SyliusCompletenessPlugin\SetonoSyliusCompletenessPlugin::class => ['all' => true],
];
```

### 3. Import the configuration and routing

[](#3-import-the-configuration-and-routing)

```
# config/packages/setono_sylius_completeness.yaml
imports:
    - { resource: "@SetonoSyliusCompletenessPlugin/Resources/config/app/config.yaml" }

setono_sylius_completeness: ~
```

```
# config/routes/setono_sylius_completeness.yaml
setono_sylius_completeness_admin:
    resource: "@SetonoSyliusCompletenessPlugin/Resources/config/routes/admin.yaml"
    prefix: /admin
```

### 4. Make your `Product` completeness-aware

[](#4-make-your-product-completeness-aware)

Apply the shipped interface and trait to your product entity:

```
# src/Entity/Product/Product.php
namespace App\Entity\Product;

use Doctrine\ORM\Mapping as ORM;
use Setono\SyliusCompletenessPlugin\Model\ProductCompletenessAwareInterface;
use Setono\SyliusCompletenessPlugin\Model\ProductCompletenessAwareTrait;
use Sylius\Component\Core\Model\Product as BaseProduct;

#[ORM\Entity]
#[ORM\Table(name: 'sylius_product')]
class Product extends BaseProduct implements ProductCompletenessAwareInterface
{
    use ProductCompletenessAwareTrait;
}
```

The plugin ships **XML** Doctrine mappings, so add the matching mapping fragment for the fields the trait introduces (or use PHP attributes as above and only map the association + scalar columns). Example XML fragment:

```

```

Point your host application to the completeness `Product` model:

```
# config/packages/_sylius.yaml
sylius_product:
    resources:
        product:
            classes:
                model: App\Entity\Product\Product
```

### 5. Update the database

[](#5-update-the-database)

Generate and run a migration (the plugin does not ship migrations because the target `sylius_product`table is host-owned):

```
bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate
```

This creates the `setono_sylius_completeness__*` tables and adds `completeness_ratio` (indexed), `completeness_rubric_version` and `completeness_dirty_at` (indexed) to `sylius_product`.

### 6. Schedule the drain (required)

[](#6-schedule-the-drain-required)

Most recalculation happens in the background: changes mark the affected products *dirty* and a drain command recalculates them. Run it on a cron every few minutes:

```
*/5 * * * * cd /path/to/app && bin/console setono:completeness:process
```

It is safe to overlap (a leased lock guarantees a single run) and to run as often as you like. See [How recalculation is triggered](#how-recalculation-is-triggered) for the full picture.

### 7. Optional: starter ruleset + initial calculation

[](#7-optional-starter-ruleset--initial-calculation)

```
bin/console sylius:fixtures:load setono_sylius_completeness   # a sensible starter rubric
bin/console setono:completeness:recalculate --all             # score the whole catalog
```

Concepts
--------

[](#concepts)

- **Rule** — a persisted, admin-managed record binding a checker `type` (+ `configuration`) to a **weight tier** (`low`/`medium`/`high`/`critical`), an optional **scope** (channels/locales/taxons) and an optional ExpressionLanguage **condition** gate. The `expression` checker's `configuration` holds the ExpressionLanguage **expression** that is the check itself. The set of enabled rules is the scoring rubric.
- **Weight vs score** — a rule's *weight* is "how much it matters" (from the tier); a checker's *score* is "how met it is" (0.0–1.0). Binary checkers return 1.0/0.0; graded checkers grant partial credit (e.g. `has_minimum_images` with 3 of 5 ⇒ 0.6).
- **Context** — a `(channel, locale)` pair. Each context is scored independently; translatable fields resolve to exactly that locale (a missing translation reads as empty, never the default-locale text).
- **N/A** — a context with no applicable rules is **not scored** (rendered as "—"), distinct from a measured 0%. N/A contexts are excluded from the global rollup.
- **Context settings** — an optional per-`(channel, locale)` record holding a "ready" **threshold** (for color-coding) and a **rollup weight** (0 = excluded from the global score). A missing row means defaults, so an empty table reproduces flat-average, single-threshold behavior.
- **Rollup** — the per-context ratios collapse into the single `completenessRatio` via a configurable strategy (`weighted_average` default, `minimum`, `default_channel`), after dropping N/A and excluded contexts.
- **Staleness** — a monotonic rubric version is bumped on every rule change and stamped on products at calc time. The grid and panel show a "recalculating…" marker for products whose stamped version is behind (a rule changed) or that are flagged `completeness_dirty_at` (their own data changed), until the [drain](#how-recalculation-is-triggered) catches up.

How recalculation is triggered
------------------------------

[](#how-recalculation-is-triggered)

Scores are kept up to date through three lanes, so an interactive edit is instant while bulk changes never block a request:

ChangeMechanismWhen it recalculatesA product/variant is **saved in the admin**Sylius resource events (`sylius.product.post_*`, `sylius.product_variant.post_*`)**Immediately &amp; synchronously** — the fresh score is on the page you land onAny other product change — **API, imports, programmatic writes**, changes to related entitiesa Doctrine `onFlush` listener sets `completeness_dirty_at` on the affected product(s)on the next **drain**A **rule** changesthe rubric version is bumped (every product becomes stale)on the next **drain**A **context** changesa rollup-only refresh is dispatched over Messengerwhen that message is handled**Manual** — the dashboard/grid "Recalculate" buttons, or `setono:completeness:recalculate`Messenger / directon demandThe **drain** (`setono:completeness:process`, step 6) is the workhorse: every few minutes it recalculates the products that are dirty or stale, in id-keyset chunks, under a Symfony Lock (a `DoctrineDbalStore` on your default connection) so runs never overlap and a crashed run self-heals. It debounces bursts (a 10k-product import is *one* drain, not 10k recalculations) and needs no message worker. The `completeness_dirty_at` flag is cleared only if it hasn't changed since the product was picked up, so an edit that lands mid-run is retried rather than lost.

> The lock store lazily creates a `lock_keys` table on first use. In a multi-server production setup you may prefer to create it up front — see the [Symfony Lock docs](https://symfony.com/doc/current/lock.html).

The Doctrine `onFlush` marker never calls `flush()` or dispatches — it writes the flag as part of the same flush via `recomputeSingleEntityChangeSet`. New products are not flagged: their null rubric version already makes them drain candidates. To watch an additional entity, register an [`AffectedProductsResolverInterface`](#extension-points) — both the marker and the immediate lane use it.

Set `recalculate_on_doctrine_flush: false` to disable the dirty-marking entirely (then only the manual lane and a periodic `recalculate --all` keep scores fresh).

Expression authoring
--------------------

[](#expression-authoring)

Conditions and expressions use the Symfony ExpressionLanguage. A **condition** decides *if* a rule applies; an **expression** *is* the check for `expression`-type rules (a boolean means met/not met, a number between 0 and 1 grants partial credit).

The condition and expression fields (and the preview scratchpad) are enhanced with a [CodeMirror](https://codemirror.net/) editor that adds syntax highlighting and autocompletion of the in-scope variables and the registered functions (host-added functions included). CodeMirror is loaded from a versioned, SRI-pinned CDN, so the plugin needs no asset build; if it is unavailable the fields degrade gracefully to plain textareas.

Variables in scope: `product`, `channel`, `locale`, `channelCode`, `localeCode`.

Translatable fields are read through the product getters and always resolve to the scored locale:

```
word_count(product.getDescription()) >= 200

```

Use native operators — arithmetic `+ - * / %`, comparison `== != < > =`, logical `and or not`, membership `in` / `not in`, **regex `matches`**, concat `~`, ternary `?:` — plus the helper library (`word_count`, `char_count`, `has_attribute`, `attribute_value`, `image_count`, `in_taxon`, `has_price`, `price`, `min`, `max`, `between`, …). The full catalog is rendered inline in the rule form and on the preview screen.

**"Required-when" rules** use both slots — e.g. *"if `type` is beer, `beer_type` must be set"*:

- condition: `attribute_value(product, 'type') == 'beer'`
- expression: `has_attribute(product, 'beer_type')`

The rule then vanishes for non-beer products (counting toward neither numerator nor denominator).

Caveats:

- **Regex ReDoS**: author-supplied `matches` patterns run unsandboxed; a catastrophic pattern can hang a calculation. Keep rule administration to trusted users.
- **Select-attribute values are codes**: `attribute_value(product, code)` returns the stored option **code**, not the display label.
- The case-insensitive contains helper is named **`icontains`** (`contains` is a reserved EL operator).

Extension points
----------------

[](#extension-points)

Everything is a tagged service. All of these are supported and documented:

Tag / interfacePurpose`setono_sylius_completeness.checker` (`CompletenessCheckerInterface`)Add a checker. If two share a `type`, the **last registered wins** — that's how you override a built-in.`setono_sylius_completeness.checker_configuration_form_type`Register a checker's configuration form.`setono_sylius_completeness.expression_function_provider`Add expression helper functions (a Symfony `ExpressionFunctionProviderInterface`).`setono_sylius_completeness.affected_products_resolver` (`AffectedProductsResolverInterface`)Make changes to your own entities trigger recalculation — no core change.`setono_sylius_completeness.rollup_strategy` (`RollupStrategyInterface`)Add a rollup strategy.The public API is `Setono\SyliusCompletenessPlugin\Calculator\CompletenessCalculatorInterface` (a pure dry-run that returns the full breakdown) and `Setono\SyliusCompletenessPlugin\Updater\ProductCompletenessUpdaterInterface`(calculate + persist). After each persisted calculation a `ProductCompletenessCalculated` event is dispatched (with a `bulk` flag). Notice that context changes trigger a rollup-only refresh, which recomputes the global ratio from existing rows and does **not** dispatch that event.

Configuration reference
-----------------------

[](#configuration-reference)

```
setono_sylius_completeness:
    rollup_strategy: weighted_average   # weighted_average | minimum | default_channel |
    default_channel_code: ~             # channel used by the default_channel strategy
    default_ready_threshold: 80         # green/"ready" line when a context has no override
    amber_band: 20                      # width of the amber zone below the threshold (0 disables amber)
    weight_tiers:
        low: 1
        medium: 3
        high: 6
        critical: 10
    enable_custom_weight: false         # exposes the advanced per-rule float override
    recalculate_on_doctrine_flush: true # a flush marks affected products dirty for the drain
    recalculation_lock_ttl: 900         # lease (s) of the drain's lock; refreshed every chunk
```

There is intentionally **no** `watched_entities` key: the set of watched classes is derived from the registered `AffectedProductsResolverInterface` services. To watch an additional entity, register a resolver.

Console
-------

[](#console)

```
# The background drain: recalculate dirty/stale products (schedule this on a cron, see step 6)
bin/console setono:completeness:process

# Recalculate the whole catalog synchronously (good after install, or as a periodic safety net)
bin/console setono:completeness:recalculate --all

# Recalculate specific products
bin/console setono:completeness:recalculate --product=SKU-1 --product=SKU-2
```

Translations
------------

[](#translations)

`en` is the authoritative source of truth. The plugin ships admin translations for **`da`, `sv`, `no`, `fi`, `de`, `fr`, `es`, `it`, `nl`, `pl`, `pt`, `cs`, `hu`, `ro` and `uk`**; any untranslated key falls back to English via the Symfony translator. (Norwegian uses `no`; if your shop runs `nb`, copy the catalog under that code.)

###  Health Score

19

—

LowBetter than 9% of packages

Maintenance60

Regular maintenance activity

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/2412177?v=4)[Joachim Løvgaard](/maintainers/loevgaard)[@loevgaard](https://github.com/loevgaard)

---

Top Contributors

[![loevgaard](https://avatars.githubusercontent.com/u/2412177?v=4)](https://github.com/loevgaard "loevgaard (47 commits)")

---

Tags

data-qualityecommercephppimproduct-completenessproduct-enrichmentsyliussylius-pluginsymfony

### Embed Badge

![Health badge](/badges/setono-sylius-completeness-plugin/health.svg)

```
[![Health](https://phpackages.com/badges/setono-sylius-completeness-plugin/health.svg)](https://phpackages.com/packages/setono-sylius-completeness-plugin)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
