Skip to content

Make the price extension a faithful decorator - #18

Open
loevgaard wants to merge 1 commit into
masterfrom
fix/price-extension-decoration
Open

loevgaard wants to merge 1 commit into
masterfrom
fix/price-extension-decoration

Conversation

@loevgaard

Copy link
Copy Markdown
Member

Three ways the decoration could silently drop or corrupt behaviour. None of them misbehave against Sylius 1.12 today — they are all "the moment X changes, this breaks quietly", which is the worst way for a price calculation to break.

1. Rebuilt filters lost their options. new TwigFilter($name, $closure) discarded whatever options the original carried. The three Sylius filters declare none, so nothing is lost right now, but escaping (is_safe, pre_escape, preserves_safety) and argument passing (needs_environment, needs_context, is_variadic) would have silently changed the moment one of them did. copyOptions() now carries them across.

is_safe is the interesting one: it cannot be read back directly, and getSafe() needs the argument Node, which only exists at compile time. Forwarding an is_safe_callback that defers to the original getSafe() preserves both is_safe and is_safe_callback correctly and at the right moment.

Two options are deliberately not copied, both commented in the code: node_class (a filter compiled through a custom node would not route through our callable at all, so copying it would produce a filter that silently ignores the VAT context) and the deprecation version/alternative detail (only readable via methods Twig has itself deprecated — the notice survives, the detail does not).

2. __call was decorative. It looked like the remaining extension methods were forwarded to the decorated extension. It never fires for any of them: AbstractExtension declares getFunctions(), getTests(), getTokenParsers() and getNodeVisitors(), so the parent's empty values won and anything the base extension grew would have vanished. They are delegated explicitly now and __call is gone.

getOperators() is the one exception and it has a comment saying why — Twig 3.21 annotates it with classes it has since removed, which any override inherits, and psalm rejects the result. The decorated extension registers no operators, so inheriting AbstractExtension's [[], []] is identical in behaviour.

3. The price context was assumed to be argument 2. That stops being true the moment a filter asks for the environment or the template context, since Twig prepends those — the wrapper would then have written vat_context_aware into the variant argument. The index is now derived from the filter itself.

Also adds PriceExtensionTest, which the decorator previously had none of. It drives the filters through a real BasePriceExtension/PriceHelper and asserts the calculator actually receives vat_context_aware, that sylius_has_discount is passed through un-rebuilt, and that the remaining methods delegate.

Addresses the third architecture item in #5.

https://claude.ai/code/session_01P9NzuVPQGvaVR97HZFq98a

Two ways the decoration could silently drop behaviour:

Rebuilding the two VAT aware filters discarded whatever options the originals
carried. The Sylius filters currently declare none, so nothing was lost yet,
but escaping and argument passing would have been silently altered the moment
they did. copyOptions() now carries them over. is_safe is forwarded through an
is_safe_callback that defers to the original getSafe(), which is the only way
to preserve both is_safe and is_safe_callback, since neither can be read back.

__call() suggested the remaining extension methods were forwarded to the
decorated extension. It never fires for them: AbstractExtension declares
getFunctions(), getTests(), getTokenParsers() and getNodeVisitors(), so they
returned the empty parent values and any function the base extension grew would
have vanished. They are now delegated explicitly and __call() is gone.

Wrapping the filter also assumed the price context is the second argument,
which stops being true if a filter asks for the environment or the template
context. The index is now derived from the filter.

Adds PriceExtensionTest, which drives the filters through a real base extension
and asserts the calculator sees vat_context_aware.

Claude-Session: https://claude.ai/code/session_01P9NzuVPQGvaVR97HZFq98a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant