Skip to content

Extract LinkGenerator interface for replaceable/decoratable link generation#370

Open
jelen07 wants to merge 6 commits intonette:masterfrom
jelen07:feature/link-generator-interface
Open

Extract LinkGenerator interface for replaceable/decoratable link generation#370
jelen07 wants to merge 6 commits intonette:masterfrom
jelen07:feature/link-generator-interface

Conversation

@jelen07
Copy link
Copy Markdown

@jelen07 jelen07 commented Apr 7, 2026

Problem

Since v3.2.3, the link generation logic was moved from Presenter to the LinkGenerator class (which is a great improvement). However, the class is final and Presenter::getLinkGenerator() is also final, which means there is no way to customize link generation behavior.

Real-world use case

In our application, we use referenceable modals that can overlap each other. When a user arrives at a page with open modals (via direct link or after a page refresh), links generated in components below the modal incorrectly include persistent parameters of all opened modals.

To solve this, we needed to intercept link generation and nullify persistent parameters based on context. Since LinkGenerator is final and getLinkGenerator() is also final, we were forced to create a workaround that overrides four methods (link(), isLinkCurrent(), redirect(), redirectPermanent()) on every component via a trait, all full of @phpstan-ignore-line comments because we access @internal methods. This works but is fragile and not a clean solution.

We believe this is a valid use case that the framework should support natively.

Solution

This PR extracts a LinkGenerator interface from the existing class (renamed to DefaultLinkGenerator), following Nette's established naming convention (plain noun for interfaces, e.g. TemplateFactory, Response, Renderable).

Changes

New files:

  • src/Application/LinkGenerator.php — interface with link(), createRequest(), requestToUrl(), withReferenceUrl(), getLastRequest()
  • src/Application/DefaultLinkGenerator.php — the original class, renamed, implements the interface

Modified files:

  • Presenter — property typed as LinkGenerator (interface), getLinkGenerator() is no longer final and returns the interface, injectPrimary() accepts optional ?LinkGenerator for custom injection
  • ApplicationExtension — registers the service with type LinkGenerator (interface) and factory DefaultLinkGenerator
  • LinkBaseNode — static calls updated to DefaultLinkGenerator::applyBase()

How it enables decoration

Users can now create a custom LinkGenerator implementation (decorator) and inject it via DI:

services:
    application.linkGenerator:
        type: Nette\Application\LinkGenerator
        factory: App\MyCustomLinkGenerator(
            inner: Nette\Application\DefaultLinkGenerator(...)
        )

Or override getLinkGenerator() in a Presenter subclass for more specific control.

BC impact

Usage Impact
LinkGenerator $x type hints No break — interface satisfies existing hints
DI autowiring No break — extension registers the interface type
new LinkGenerator(...) Breaks — use new DefaultLinkGenerator(...) (rare in userland, DI handles this)
LinkGenerator::parseDestination() Breaks — use DefaultLinkGenerator::parseDestination() (@internal)
LinkGenerator::applyBase() Breaks — use DefaultLinkGenerator::applyBase() (@internal)

The breaking changes only affect direct instantiation and @internal static method calls, which are not part of the public API and are extremely rare in userland code.

Tests

  • All existing tests updated and passing (9 pre-existing failures unrelated to this PR)
  • New test: LinkGenerator.interface.phpt — interface contract verification
  • New test: LinkGenerator.decorator.phpt — decorator injection and URL modification patterns

Resolves #333

Thank you for your time and for the framework!

jelen07 added 6 commits April 7, 2026 16:42
- Extract LinkGeneratorInterface with link(), createRequest(), requestToUrl(), withReferenceUrl(), getLastRequest() methods
- LinkGenerator class implements LinkGeneratorInterface, remove final keyword
- Presenter::getLinkGenerator() is no longer final, returns LinkGeneratorInterface
- Presenter::injectPrimary() accepts optional ?LinkGeneratorInterface for custom injection
- ApplicationExtension sets interface type for DI autowiring
- Add getLastRequest() accessor (replaces direct $lastRequest property access in Presenter)
- Change new self() to new static() in withReferenceUrl() for proper inheritance
- Add tests for interface contract and decorator injection pattern

Resolves nette#333
- Change $linkGenerator property from readonly to nullable with default null, preventing fatal error when neither custom generator nor router/factory pair is provided
- Add missing use import for LinkGeneratorInterface in ApplicationExtension
- Remove @internal from interface methods, add descriptive docs instead
- Change requestToUrl() parameter from ?bool to bool (null had no distinct meaning)
- Add PrefixingLinkGenerator test proving decorators can modify URLs
- Add DI container autowiring test for LinkGeneratorInterface
- Improve interface PHPDoc with explicit null-return documentation on link()
- Clean up interface test: remove concrete class assertions, add createRequest reset test
- Remove fragile hardcoded URL assertion from decorator test
- Replace Reflection-based injection test with behavioral assertion via Presenter::link()
- Add PrefixingLinkGenerator test proving decorators can transform URLs
- Add null-return preservation test for decorators (test mode)
- Add edge case tests: getLastRequest after failed link, withReferenceUrl immutability, createRequest and requestToUrl direct usage
- Add LinkGeneratorInterface assertion to ApplicationExtension.basic.phpt
- Add DI container autowiring test for LinkGeneratorInterface
- Remove unnecessary PresenterFactory from test constructors
- Fix empty catch blocks with proper Assert::exception usage
- Rename interface from LinkGeneratorInterface to LinkGenerator (Nette uses plain nouns for interfaces)
- Rename class from LinkGenerator to DefaultLinkGenerator
- Update all references in Presenter, ApplicationExtension, LinkBaseNode
- Update all existing tests to use DefaultLinkGenerator for instantiation
- Update static method calls: DefaultLinkGenerator::parseDestination(), DefaultLinkGenerator::applyBase()
- Update Latte bridge compiled template output assertions
- DefaultLinkGenerator docblock: "Default link generator." to distinguish from interface
- ApplicationExtension: add use import for DefaultLinkGenerator, use short name in setFactory
- Remove public visibility from interface methods (Nette interfaces use bare function)
- Fix use import ordering in Presenter: DefaultLinkGenerator before Helpers (alphabetical)
- Add missing str_starts_with to use function imports in DefaultLinkGenerator
- Restore @internal on createRequest() and getLastRequest() in interface
- Improve interface docblock to "Generates links to presenter actions."
- Remove redundant DI test from decorator test file (already covered in ApplicationExtension.basic)
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.

Ability to replace/decorate LinkGenerator.

1 participant