Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- [ ] Tests pass (`vendor/bin/phpunit`, `vendor/bin/behat --strict`)
- [ ] PHPStan and ECS are green
- [ ] If the schema changed: the migration is updated, and `sh tests/check-schema-drift.sh` prints nothing (MySQL or PostgreSQL)
- [ ] If the schema changed: a new migration is added, and `sh tests/check-schema-drift.sh` prints nothing (MySQL or PostgreSQL)
- [ ] `UPGRADE.md` is up to date if the public contract changes (traits and interfaces, Twig hooks and functions, grid query)
- [ ] A decision taken or reversed has its record in `docs/adr/`
- [ ] No customer data, shop secrets or pre-production URLs appear in the code, the tests or this description
43 changes: 30 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,34 +30,48 @@ The plugin targets Sylius 2.1, 2.2 and 2.3, Symfony 7.4 and 8 (8 with Sylius 2.3

## Invariants

Each rule is here in full, with the names the code uses; its reasons are in the decision record it
links ([`docs/adr/`](docs/adr/README.md)). Read the record before changing a rule, and write a new
record rather than editing an accepted one - the records hold no path nor figure, this file does.

- **Never replace a Sylius model.** The application owns `Taxon`, `Channel` and `TaxonImage`; the
plugin only ships traits (mapping as attributes) and interfaces. Relations target the Sylius
interfaces (`TaxonInterface`, `ProductInterface`, `TaxonImageInterface`), resolved by
`resolve_target_entities`. Code tests `instanceof AdvancedTaxonInterface`, never a class.
`resolve_target_entities`. Code tests `instanceof AdvancedTaxonInterface`, never a class
([0001](docs/adr/0001-never-replace-a-sylius-model.md)).
- **The taxon listing is the Sylius grid.** `ShopProductListQueryBuilder` calls
`createShopListQueryBuilder()` and adds the children products and the advanced filters: no
parallel product query in templates. Facet counts come from `createBaseProductsQb()`, which must
select exactly what the grid lists.
select exactly what the grid lists ([0006](docs/adr/0006-taxon-listing-is-the-sylius-grid.md),
[0007](docs/adr/0007-facet-counts-start-from-the-grid-query.md)).
- **Conditional taxons fail closed.** A stored condition that is not applicable (wrong operator,
missing reference or value) makes the taxon match nothing, never the whole catalog.
missing reference or value) makes the taxon match nothing, never the whole catalog
([0015](docs/adr/0015-non-applicable-condition-matches-nothing.md)).
- **No LIKE on JSON columns** (not portable to PostgreSQL): select attribute values are resolved in
PHP, then filtered by identifier.
PHP, then filtered by identifier ([0009](docs/adr/0009-no-like-on-json-columns.md)).
- **Services are explicit**: one file per area in `config/services/`, ids prefixed with
`cyllene_digital_sylius_advanced_taxon.` (the controller keeps its class as id), no autowiring nor
autoconfiguration (Symfony bundle best practice). A class implementing `ResetInterface` needs the
`kernel.reset` tag by hand.
autoconfiguration. A class implementing `ResetInterface` needs the
`kernel.reset` tag by hand ([0028](docs/adr/0028-explicit-services-no-autowiring.md),
[0029](docs/adr/0029-per-request-memory-services-are-reset.md)).
- **Template icons are local**: `tabler:` when Sylius ships the icon, otherwise a Tabler SVG added to
`assets/icons/` and used as `at:<name>`. A unit test fails on any other prefix (Iconify).
`assets/icons/` and used as `at:<name>` (Sylius ships 82 Tabler icons only, any other is fetched
from Iconify at render time). A unit test fails on an icon written in a template that no local set
ships
([0021](docs/adr/0021-template-icons-are-local.md)).
- **Twig hooks are imported, not prepended**: `config/config.yaml` must be loaded after the Sylius
configuration, since a prepended hook loses against the Sylius core hooks it overrides.
configuration, since a prepended hook loses against the Sylius core hooks it overrides
([0004](docs/adr/0004-twig-hooks-imported-not-prepended.md)).
- **Pictograms are Sylius images** (`image:<path>` in the icon field, Sylius image uploader and
storage, Liip filter `cyllene_advanced_taxon_icon`). Values rendered in `style` or `href`
attributes are sanitized on write (`AdvancedTaxonValueSanitizer`).
attributes are sanitized on write (`AdvancedTaxonValueSanitizer`)
([0019](docs/adr/0019-pictograms-are-sylius-images.md), [0020](docs/adr/0020-values-sanitized-on-write.md)).

- **A schema change is a new migration** in `src/Migrations/` (DBAL `Schema` API): `v1.0.0` shipped
`Version20260828173000`, which is never edited again. Free of drift on MySQL, MariaDB and
PostgreSQL (CI runs `down()` / `up()` then `tests/check-schema-drift.sh`, which lists the columns
added to the Sylius tables: keep it in step with the migrations).
added to the Sylius tables: keep it in step with the migrations)
([0031](docs/adr/0031-schema-change-is-a-new-migration.md)).
- The traits, interfaces, Twig hooks and functions, bundle configuration, message, command, admin
route and schema are the public contract ([`docs/architecture/public-contract.md`](docs/architecture/public-contract.md)):
they only break in a major version, with an entry in `UPGRADE.md`.
Expand Down Expand Up @@ -98,7 +112,8 @@ The QA commands are in [`CONTRIBUTING.md`](CONTRIBUTING.md). What it does not sa
(`database_setup: schema`), and the `down()` / `up()` replay is their only run of the plugin
migration.
- **Sylius 2.1 mappings fail `doctrine/orm` 3.7 validation** (`ShipmentUnit#shipment`): CI caps the
ORM below 3.7 before Sylius 2.3, the plugin's own constraint stays open.
ORM below 3.7 before Sylius 2.3, the plugin's own constraint stays open
([0032](docs/adr/0032-no-dependency-cap-for-an-upstream-bug.md)).
- **Behat**: the Sylius suites are not imported (2.1 / 2.2 ship them in YAML, 2.3 in PHP); the plugin
suites only use the Sylius contexts, which are services. The plugin contexts use PHP attributes (`#[Given]`, `#[AfterScenario]`):
Behat 4, the one Symfony 8 needs, no longer reads annotations; the CI keeps Behat 3 before Sylius 2.3,
Expand All @@ -113,12 +128,12 @@ The QA commands are in [`CONTRIBUTING.md`](CONTRIBUTING.md). What it does not sa
- **The taxon form template renders `form._token` itself** (`form_with_tabs.html.twig`): with
`render_rest` off, nothing else does on Sylius 2.1.
- `sylius:debug:twig-hooks` exists from `sylius/twig-hooks` 0.12, Sylius 2.3 only.
- **Template icons must be local**: Sylius ships 82 Tabler icons only; another one goes to
`assets/icons/` as `at:<name>`, or it is fetched from Iconify at render time.
- **`symfony/ux-twig-component` is kept below 3.5 in `require-dev`**: 3.5 lets an explicit `null`
override a `{% props %}` default, and the Sylius grid delete action passes a null label and icon
to `sylius_admin:delete_modal` (empty delete buttons in every admin grid). Drop the constraint
once Sylius passes defaults itself ([Sylius#19278](https://github.com/Sylius/Sylius/issues/19278)).
An upstream workaround never goes into the plugin's own constraints
([0032](docs/adr/0032-no-dependency-cap-for-an-upstream-bug.md)).
- `sylius_test_html_attribute()` attributes (`data-test-*`) are what the Behat pages target: keep
them when reworking a shop template.

Expand All @@ -134,6 +149,8 @@ The same facts live in several places; change one, change them all:
- the Twig hooks: `config/twig_hooks/*.yaml` and `docs/integration/theming.md`;
- the Twig functions: `src/Twig/*.php`, `docs/shop/twig-functions.md` and `docs/architecture/public-contract.md`;
- the Behat features: `features/`, `tests/Behat/Resources/suites.php` and `docs/testing/tests.md`;
- a decision taken or reversed: a new record in `docs/adr/`, its line in `docs/adr/README.md`, and
the link from the rule here;
- what a screen looks like: the screenshots in `docs/assets/` after a visible change.

## What does not belong here
Expand Down
9 changes: 7 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,10 @@ Sylius plugins, do not add personal rules to it.
integration tests on a real database; prefer a Behat scenario when the bug is visible in the shop
or the back office.
- The plugin never replaces a Sylius model: new fields on `Taxon`, `Channel` or `TaxonImage` go into
the matching trait and interface, and the migration.
the matching trait and interface, and a new migration
([why](docs/adr/0001-never-replace-a-sylius-model.md), [migrations](docs/adr/0031-schema-change-is-a-new-migration.md)).
- Services are defined explicitly in `config/services/`, without autowiring
([code map](docs/development/code-map.md#wiring)).
([code map](docs/development/code-map.md#wiring), [why](docs/adr/0028-explicit-services-no-autowiring.md)).
- Update **`UPGRADE.md`** when the public contract changes; the change itself is described in the
pull request, which feeds the release notes. Public contract items are listed in
[docs/architecture/public-contract.md](docs/architecture/public-contract.md); they only break in
Expand All @@ -246,6 +247,10 @@ Sylius plugins, do not add personal rules to it.
- The codebase is written in **English** (code, comments, commits).
- A map of the code is in [docs/development/code-map.md](docs/development/code-map.md); the test
strategy in [docs/testing/tests.md](docs/testing/tests.md).
- Why the plugin is built the way it is, and what was left as it is on purpose, is in the
[decision records](docs/adr/README.md): read the one a change touches first. A pull request
that takes a decision, or reverses one, adds a record - an accepted record is superseded, not
edited.

## What does not belong here

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ full list and the support policy:
- [Back-office guide](https://github.com/CylleneDigital/SyliusAdvancedTaxonPlugin/blob/main/docs/admin/user-guide.md): for merchants
- [Theming and Twig hooks](https://github.com/CylleneDigital/SyliusAdvancedTaxonPlugin/blob/main/docs/integration/theming.md): for integrators
- [Technical documentation](https://github.com/CylleneDigital/SyliusAdvancedTaxonPlugin/blob/main/docs/README.md): every page, by audience
- [Decision records](https://github.com/CylleneDigital/SyliusAdvancedTaxonPlugin/blob/main/docs/adr/README.md): why the plugin is built the way it is, one decision per file

## Status

Expand Down
9 changes: 0 additions & 9 deletions assets/shop/js/mobile-menu-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@ export default class extends Controller {
document.removeEventListener('keydown', this._keyHandler);
}

// ------------------------------------------------------------------ //
// Public actions (callable via data-action)
// ------------------------------------------------------------------ //

open() {
this.element.classList.add('at-mn-drawer--open');
this.element.setAttribute('aria-hidden', 'false');
Expand Down Expand Up @@ -97,7 +93,6 @@ export default class extends Controller {
this._focusActiveScreen();
}

/** Navigate back to the previous screen. */
goBack() {
if (this._stack.length <= 1) { this.close(); return; }
const currentId = this._stack.pop();
Expand All @@ -110,10 +105,6 @@ export default class extends Controller {
this._focusActiveScreen();
}

// ------------------------------------------------------------------ //
// Private helpers
// ------------------------------------------------------------------ //

_handleBurgerClick(event) {
// Prevent Bootstrap (or any other handler) from processing this click.
event.stopImmediatePropagation();
Expand Down
6 changes: 0 additions & 6 deletions assets/shop/scss/mega-menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
border-bottom: 1px solid #dee2e6;
}

// Horizontal navigation bar.
.at-mn-list {
display: flex;
list-style: none;
Expand Down Expand Up @@ -73,7 +72,6 @@
display: block;
}

// Centred inner container.
.at-mn-panel-inner {
display: flex;
align-items: flex-start;
Expand Down Expand Up @@ -116,7 +114,6 @@
&:hover { opacity: 0.8; }
}

// Regular columns.
.at-mn-cols {
display: flex;
flex-wrap: wrap;
Expand Down Expand Up @@ -229,7 +226,6 @@
}
}

// Semi-transparent backdrop.
.at-mn-drawer-backdrop {
position: absolute;
inset: 0;
Expand Down Expand Up @@ -272,7 +268,6 @@
&[data-mn-state="right"] { transform: translateX(100%); pointer-events: none; }
}

// Screen header bar.
.at-mn-screen-head {
display: flex;
align-items: center;
Expand Down Expand Up @@ -331,7 +326,6 @@
&:hover { text-decoration: underline; }
}

// List of navigation items.
.at-mn-screen-list {
list-style: none;
padding: 0;
Expand Down
3 changes: 2 additions & 1 deletion config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

/*
* Services are defined explicitly, without autowiring nor autoconfiguration, as Symfony recommends
* for reusable bundles: ids are prefixed with the bundle alias and private unless stated otherwise.
* for reusable bundles: ids are prefixed with the bundle alias and private unless stated otherwise (the controller keeps its
* class as id, its route names it).
*/
return static function (ContainerConfigurator $container): void {
$container->import('services/*.php');
Expand Down
5 changes: 3 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ list: [Features](features.md). Installation in short and production notes: the r
|---------|-------------------|
| Merchant (back office) | [Back-office guide](admin/user-guide.md), [Storefront](shop/storefront.md), [Conditional taxons](domain/conditional-taxons.md) |
| Sylius integrator | [Installation](integration/installation.md), [Configuration](integration/configuration.md), [Theming and Twig hooks](integration/theming.md), [Assets](integration/assets.md), [Twig functions](shop/twig-functions.md), [Public contract](architecture/public-contract.md), [Troubleshooting](integration/troubleshooting.md) |
| Ops (deployment) | [Migrations](persistence/migrations.md), [Database schema](persistence/schema.md), [Configuration: Messenger](integration/configuration.md), [Uninstall](integration/uninstall.md), [UPGRADE](../UPGRADE.md) |
| Plugin contributor | [Architecture overview](architecture/overview.md), [Code map](development/code-map.md), [Tests](testing/tests.md), [CONTRIBUTING](../CONTRIBUTING.md) |
| Ops (deployment) | [Migrations](persistence/migrations.md), [Database schema](persistence/schema.md), [Configuration: Messenger](integration/configuration.md#conditional-taxon-synchronization-messenger), [Uninstall](integration/uninstall.md), [UPGRADE](../UPGRADE.md) |
| Plugin contributor | [Architecture overview](architecture/overview.md), [Decision records](adr/README.md), [Code map](development/code-map.md), [Tests](testing/tests.md), [CONTRIBUTING](../CONTRIBUTING.md) |

## Table of contents

Expand Down Expand Up @@ -45,6 +45,7 @@ list: [Features](features.md). Installation in short and production notes: the r

- [Architecture overview](architecture/overview.md): layers, main flows, principles
- [Public contract](architecture/public-contract.md): what is stable (semver major), support policy
- [Decision records](adr/README.md): one file per architecture decision - its context, the decision, what follows from it
- [Database schema](persistence/schema.md): added columns and tables
- [Migrations](persistence/migrations.md): the single migration, portability, drift check

Expand Down
31 changes: 31 additions & 0 deletions docs/adr/0001-never-replace-a-sylius-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 0001. The plugin never replaces a Sylius model

- Status: accepted
- Date: 2026-10-14

## Context

A plugin that adds fields to taxons, channels and taxon images could ship its own entities and ask
the application to use them. The application then loses its own models, and two plugins doing the
same cannot be installed together. A relation that names a concrete class also breaks as soon as the
application overrides that class.

## Decision

The application keeps its own taxon, channel and taxon image entities. The plugin ships a trait and
an interface for each, with the mapping as attributes in the trait, the usual Sylius 2 pattern: an
XML mapping to copy into the application would be heavier to install. Relations target the Sylius
interfaces, resolved by Doctrine to the application's classes. The code tests interfaces, never a
Sylius or application model class. What the plugin owns alone lives in tables of its own.

## Consequences

- Installing the plugin means editing three application entities, which the installation guide and
the Flex recipe describe.
- A taxon that does not implement the plugin interface is ignored by the shop helpers instead of
failing.
- The traits and interfaces are public contract: changing them is a major version.

## Related

- [0031. A schema change is a new migration, written against the DBAL `Schema` API](0031-schema-change-is-a-new-migration.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 0002. A universe is kept out of main taxons by a validation constraint, not a form override

- Status: accepted
- Date: 2026-10-14

## Context

A universe page lists child taxons, not products. A product whose main taxon is a universe would get
a breadcrumb and a canonical URL pointing to a page where it does not appear. Filtering the choices
of the main taxon field of the product form would only protect the back office, and replacing a core
field leaves the loading order of the form extensions decide which definition wins, losing the
native options of the field.

## Decision

A validation constraint on the product, in the `sylius` group, rejects a universe as main taxon. The
product form is not touched.

## Consequences

- The rule holds wherever the product is validated in the `sylius` group: the back office and the
API.
- A universe still appears in the choices of the field; picking it shows a validation error.
- Code that saves a product without validating it, such as the Sylius fixtures, is not protected.
30 changes: 30 additions & 0 deletions docs/adr/0003-media-zones-replace-the-native-images-section.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# 0003. Plugin media zones replace the native taxon Images section

- Status: accepted
- Date: 2026-10-14

## Context

The plugin places taxon images in zones (main image, top, bottom, left, inside the product list,
featured, universe slider), most with their own display settings, stored as Sylius taxon images with
a type. Keeping the native Images collection next to the zones would have two forms managing the
same images, and the native collection would delete the zone media when the taxon is saved. Showing
the native collection only for the types the plugin does not use would still give two image
interfaces in one form.

## Decision

The native Images section is removed from the taxon form and replaced by the zones. A main image
zone takes over the images Sylius stores without type; they get the type of that zone when the taxon
is saved, and the shop shows the first one, by position, as the taxon image.

## Consequences

- Images of other types, set by a theme or another plugin, are kept in the database but can no
longer be edited from the taxon form.
- One image interface in the taxon form, with one uploader and one storage: the Sylius ones.

## Related

- [0019. Pictograms are Sylius images](0019-pictograms-are-sylius-images.md)
- [0023. The random media display modes are kept](0023-random-media-display-modes-kept.md)
25 changes: 25 additions & 0 deletions docs/adr/0004-twig-hooks-imported-not-prepended.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 0004. Twig hooks are imported, not prepended

- Status: accepted
- Date: 2026-10-14

## Context

Several plugin hooks override hookables of the Sylius core. A configuration prepended by the bundle
extension is loaded before the application's and the Sylius one, so the Sylius core hooks would win
over the plugin hooks that replace them.

## Decision

The hooks live in YAML files that the application imports through the plugin configuration file,
after the Sylius configuration.

## Consequences

- Installing the plugin means importing that file, which the Flex recipe does.
- An application can still override any plugin hook after the import.
- The hooks and their hookables are public contract.

## Related

- [0005. The plugin takes over the Sylius search form so it keeps the advanced filters](0005-search-form-taken-over-to-keep-the-advanced-filters.md)
Loading
Loading