Skip to content

Conversation

@tijsverkoyen
Copy link
Member

@tijsverkoyen tijsverkoyen commented Jan 27, 2026

Summary by Sourcery

Replace deprecated SpoonFilter usage across backend and frontend with Symfony String utilities, native PHP functions, and Symfony Validator; clean up related URL, HTML, and regex handling; and remove obsolete site domain configuration handling.

Bug Fixes:

  • Fix validation logic for template syntax and time fields by using FILTER_VALIDATE_REGEXP and Symfony\Component\Validator\Constraints\Regex.
  • Correct redirect helper name from redirectToFistAvailableLink to redirectToFirstAvailableLink and ensure recursive calls use the corrected name.
  • Align URL parameter type casting in Frontend URL handling with native PHP casts instead of SpoonFilter::getValue to avoid charset-dependent behaviour.
  • Ensure canonical URL meta tags and various labels/titles are consistently HTML-escaped with native functions to prevent XSS or malformed output.
  • Remove obsolete site_domains setting and validation from core settings to avoid storing and validating unused configuration.

Enhancements:

  • Standardize string casing, label generation, and identifier formatting using Symfony\Component\String\s() helper instead of SpoonFilter utilities.
  • Replace SpoonFilter-based validation (integer checks, regex, URL handling, value casting) with native PHP functions, Symfony Validator constraints, and explicit in_array checks.
  • Refine HTML encoding/decoding and link autoconversion in text helpers, RSS, and search AJAX endpoints to rely on native htmlspecialchars/html_entity_decode and consistent rel attributes.
  • Simplify theme/template and navigation handling by removing SpoonFilter::getValue usage and making defaults explicit, and fix a typo in the redirectToFirstAvailableLink helper name.
  • Adjust form and template helpers to generate field, widget, and position keys/IDs via Symfony String, keeping template and permission naming consistent across back‑ and frontend.

CI:

  • Limit CI workflow push trigger to the master branch to avoid unnecessary builds from other branches.

Tests:

  • Update test database fixture to drop the deprecated Core.site_domains setting to match the new configuration behaviour.

@sourcery-ai
Copy link

sourcery-ai bot commented Jan 27, 2026

Reviewer's Guide

Refactors a large portion of backend and frontend string handling, validation, and URL/HTML utilities away from deprecated SpoonFilter helpers toward Symfony String, native PHP, and Symfony Validator; normalizes template/field naming conventions; tightens XSS/encoding behaviour; fixes a redirect helper typo; removes obsolete site_domains configuration; and narrows the CI workflow push trigger to master only.

Sequence diagram for backend redirectToFirstAvailableLink flow

sequenceDiagram
    actor User
    participant Browser
    participant BackendKernel
    participant Url as Backend_Core_Engine_Url
    participant Auth as Backend_Core_Engine_Authentication
    participant NavCache as cache_backend_navigation

    User->>Browser: Request backend URL
    Browser->>BackendKernel: HTTP request
    BackendKernel->>Url: Route request
    Url->>Url: getModuleFromRequest()
    Url->>Url: getActionFromRequest(module, language)

    Url->>Auth: isAllowedModule(module)
    alt module is Dashboard and not allowed
        Url->>NavCache: get()
        NavCache-->>Url: navigation tree
        Url->>Url: redirectToFirstAvailableLink(language, navigation)
        loop navigation items
            Url->>Auth: isAllowedModule(candidateModule)
            Url->>Auth: isAllowedAction(candidateAction, candidateModule)
            alt first allowed combination found
                Url-->>BackendKernel: RedirectResponse to candidateModule/candidateAction
                BackendKernel-->>Browser: 302 Redirect
                Browser-->>User: Navigate to first available link
            else child navigation exists
                Url->>Url: redirectToFirstAvailableLink(language, children)
            end
        end
    else module is allowed
        Url-->>BackendKernel: Continue normal controller dispatch
        BackendKernel-->>Browser: HTTP response
        Browser-->>User: Render backend page
    end
Loading

Updated class diagram for backend and frontend URL handling

classDiagram
    class Backend_Core_Engine_Url {
        -string module
        -string action
        +__construct(KernelInterface kernel)
        +getModuleFromRequest() string
        +getActionFromRequest(string module, string language) string
        +processRegularRequest(string module, string action, string language) void
        -redirectToFirstAvailableLink(string language, array navigation) void
        +setAction(string action, string module) void
        +setModule(string module) void
    }

    class Backend_Core_Engine_Authentication {
        +isAllowedModule(string module) bool
        +isAllowedAction(string action, string module) bool
    }

    class Backend_Core_Engine_Navigation {
        +getNavigationItemForCurrentlyAuthenticatedUser(array navigation) array
    }

    class Frontend_Core_Engine_Url {
        +array parameters
        +getParameter(int index, string type, mixed defaultValue) mixed
        +hasParameter(int index) bool
    }

    Backend_Core_Engine_Url --> Backend_Core_Engine_Authentication : uses
    Backend_Core_Engine_Url --> Backend_Core_Engine_Navigation : uses
    Backend_Core_Engine_Url ..> Frontend_Core_Engine_Url : similar_casting_conventions
Loading

Updated class diagram for FormBuilder validation and field handling

classDiagram
    class Frontend_Modules_FormBuilder_Widgets_Form {
        -Form form
        -array fields
        +createAction() string
        +validateForm() void
    }

    class Backend_Modules_FormBuilder_Actions_Add {
        -BackendForm form
        +execute() void
        +loadForm() void
        +validateForm() void
    }

    class Backend_Modules_FormBuilder_Actions_Edit {
        -BackendForm form
        +execute() void
        +loadForm() void
        +parseFields() void
        +validateForm() void
    }

    class Symfony_Component_Validator_ValidatorInterface {
        +validate(mixed value, Constraint constraint, array groups) ConstraintViolationListInterface
    }

    class Symfony_Component_Validator_Constraints_Regex {
        +string pattern
        +string message
        +__construct(array options)
    }

    Frontend_Modules_FormBuilder_Widgets_Form --> Symfony_Component_Validator_ValidatorInterface : uses_for_time_validation
    Frontend_Modules_FormBuilder_Widgets_Form --> Symfony_Component_Validator_Constraints_Regex : time_rule

    Backend_Modules_FormBuilder_Actions_Add --> Symfony_Component_Validator_ValidatorInterface : uses_for_identifier
    Backend_Modules_FormBuilder_Actions_Add --> Symfony_Component_Validator_Constraints_Regex : identifier_pattern

    Backend_Modules_FormBuilder_Actions_Edit --> Symfony_Component_Validator_ValidatorInterface : uses_for_identifier
    Backend_Modules_FormBuilder_Actions_Edit --> Symfony_Component_Validator_Constraints_Regex : identifier_pattern
Loading

File-Level Changes

Change Details Files
Replace SpoonFilter-based string casing and identifier generation with Symfony\Component\String\s() helpers across forms, grids, navigation, and language utilities.
  • Form field template placeholders, widget IDs, permission keys, and Twig-assigned variable names now use s()->replace()->camel()->title() instead of SpoonFilter::toCamelCase/ucfirst
  • Backend and frontend Language classes (getLabel/getError/getMessage/getAction) now normalize keys via Symfony String instead of SpoonFilter
  • DataGrid headers, button labels, module/action labels, and various UI texts now use s(...)->title() for human-readable labels
  • Widget/action/module identifiers in Groups, Dashboard, Navigation, and authentication checks are derived via Symfony String casing instead of SpoonFilter
src/Frontend/Core/Engine/Form.php
src/Backend/Core/Engine/DataGrid.php
src/Backend/Core/Engine/Url.php
src/Backend/Core/Engine/Base/Action.php
src/Backend/Core/Engine/TwigTemplate.php
src/Backend/Core/Language/Language.php
src/Frontend/Core/Language/Language.php
src/Backend/Core/Engine/TemplateModifiers.php
src/Frontend/Core/Engine/TemplateModifiers.php
src/Backend/Modules/Groups/Actions/Add.php
src/Backend/Modules/Groups/Actions/Edit.php
src/Backend/Modules/Dashboard/Actions/Index.php
src/Backend/Modules/Extensions/Engine/Model.php
src/Backend/Modules/Extensions/Actions/Modules.php
src/Backend/Modules/Pages/Actions/Add.php
src/Backend/Modules/Pages/Actions/Edit.php
src/Backend/Modules/Pages/Actions/Index.php
src/Backend/Modules/Pages/Installer/Installer.php
src/Backend/Modules/Profiles/Installer/Installer.php
src/Backend/Modules/Profiles/Actions/Add.php
src/Backend/Modules/Profiles/Actions/Edit.php
src/Backend/Modules/Profiles/Engine/Model.php
src/Backend/Modules/Blog/Actions/Add.php
src/Backend/Modules/Blog/Actions/Edit.php
src/Backend/Modules/Blog/Actions/Categories.php
src/Backend/Modules/Blog/Actions/Comments.php
src/Backend/Modules/Blog/Actions/Index.php
src/Backend/Modules/Blog/Actions/EditComment.php
src/Backend/Modules/Tags/Actions/Edit.php
src/Backend/Modules/Tags/Actions/Index.php
src/Backend/Modules/Locale/Engine/Model.php
src/Backend/Modules/Locale/Engine/CacheBuilder.php
src/Backend/Modules/Locale/Actions/Index.php
src/Backend/Modules/Locale/Actions/Add.php
src/Backend/Modules/Locale/Actions/Edit.php
src/Backend/Modules/ContentBlocks/Domain/ContentBlock/ContentBlockDataGrid.php
src/Backend/Modules/ContentBlocks/Domain/ContentBlock/ContentBlockRevisionDataGrid.php
src/Backend/Modules/FormBuilder/Engine/Model.php
src/Backend/Modules/FormBuilder/Engine/Helper.php
src/Backend/Modules/FormBuilder/Installer/Installer.php
src/Backend/Modules/FormBuilder/Actions/Add.php
src/Backend/Modules/FormBuilder/Actions/Edit.php
src/Backend/Modules/FormBuilder/Actions/Index.php
src/Backend/Modules/Location/Engine/Model.php
src/Backend/Modules/MediaLibrary/Domain/MediaItem/MediaItemSelectionDataGrid.php
src/Backend/Modules/MediaLibrary/Domain/MediaItem/MediaItemDataGrid.php
src/Backend/Modules/MediaGalleries/Domain/MediaGallery/MediaGalleryDataGrid.php
src/Backend/Modules/MediaLibrary/Domain/MediaGroup/TypeType.php
src/Backend/Modules/Profiles/Installer/Installer.php
src/Backend/Modules/Search/Installer/Installer.php
src/Backend/Modules/Search/Actions/Statistics.php
src/Backend/Modules/Users/Actions/Index.php
src/Backend/Core/Engine/Authentication.php
src/Backend/Core/Engine/Navigation.php
src/Frontend/Core/Engine/Block/ExtraInterface.php
src/Frontend/Core/Engine/Page.php
src/Frontend/Modules/Blog/Actions/Archive.php
src/Frontend/Modules/Blog/Actions/Category.php
src/Frontend/Modules/Blog/Actions/Rss.php
src/Frontend/Modules/Blog/Actions/CommentsRss.php
src/Frontend/Modules/Mailmotor/Domain/Subscription/SubscribeType.php
src/Frontend/Modules/Mailmotor/Domain/Subscription/UnsubscribeType.php
src/Frontend/Modules/Profiles/Actions/Settings.php
src/Frontend/Modules/Tags/Engine/Model.php
src/Frontend/Core/Engine/SeoFormNode.php
Replace SpoonFilter-based validation (regex, integer/numeric checks, URL handling, value typing) with native PHP and Symfony Validator; tighten template syntax and time validations.
  • Backend Extensions template syntax validation now uses filter_var(FILTER_VALIDATE_REGEXP) and compares the filtered value to the original string
  • FormBuilder form identifier and template position-name validations use Symfony\Component\Validator\Constraints\Regex instead of SpoonFilter::isValidAgainstRegexp
  • Frontend FormBuilder widget and frontend Blog comment forms switch to filter_var(FILTER_VALIDATE_INT) and Symfony Assert\Url/Regex for parameter/time and URL validation
  • Various integer/numeric checks (extras default_extras, FormBuilder URL segment parameters, MetaType URL-suffix detection, Common/Core Model::addNumber) now use filter_var or ctype_digit instead of SpoonFilter::isInteger/isNumeric
  • Backend/Frontend Navigation::getUrlForBlock and Backend Url/module/action resolution keep behaviour but rely on Symfony String casing and native casting/validation
src/Backend/Modules/Extensions/Engine/Model.php
src/Backend/Modules/Extensions/Actions/AddThemeTemplate.php
src/Backend/Modules/Extensions/Actions/EditThemeTemplate.php
src/Backend/Modules/FormBuilder/Actions/Add.php
src/Backend/Modules/FormBuilder/Actions/Edit.php
src/Frontend/Modules/FormBuilder/Widgets/Form.php
src/Backend/Form/Type/MetaType.php
src/Common/Core/Model.php
src/Frontend/Modules/Blog/Actions/Detail.php
src/Backend/Modules/FormBuilder/Engine/Model.php
src/Backend/Modules/Tags/Engine/Model.php
src/Common/Doctrine/Repository/MetaRepository.php
src/Frontend/Core/Engine/Navigation.php
src/Frontend/Core/Engine/Url.php
src/Backend/Modules/FormBuilder/Actions/ExportData.php
Standardize HTML encoding/decoding and link auto-conversion using native htmlspecialchars/html_entity_decode and consistent rel attributes; improve XSS resilience.
  • Replace SpoonFilter::htmlspecialchars/htmlentities/htmlspecialcharsDecode/htmlentitiesDecode with native htmlspecialchars/html_entity_decode throughout FormBuilder, RSS, Meta, Tags, Profiles, Locale filters, and search Ajax handlers
  • Adjust template/meta URL handling (Backend Meta, MetaType, Pages CacheBuilder) to decode via htmlspecialchars_decode before slug generation and to re-encode with htmlspecialchars when storing/displaying
  • Update text cleanup helpers (Backend DataGridFunctions::cleanupPlainText and Frontend TemplateModifiers::cleanupPlainText) to convert URLs to tags via Symfony String regex replacement and set rel="nofollow noopener" or configurable rel values
  • Search autocomplete/autosuggest/livesuggest Ajax endpoints now always escape terms with htmlspecialchars, independent of charset; canonical search URLs now escape query parameters via htmlspecialchars
src/Backend/Modules/FormBuilder/Ajax/SaveField.php
src/Backend/Modules/FormBuilder/Actions/ExportData.php
src/Backend/Modules/Tags/Actions/Edit.php
src/Backend/Modules/Tags/Ajax/Edit.php
src/Backend/Modules/Tags/Engine/Model.php
src/Backend/Modules/Profiles/Engine/Model.php
src/Backend/Core/Engine/Meta.php
src/Backend/Form/Type/MetaType.php
src/Backend/Modules/Locale/Actions/Add.php
src/Backend/Modules/Locale/Actions/Edit.php
src/Backend/Core/Engine/DataGridFunctions.php
src/Common/Core/Twig/Extensions/BaseTwigModifiers.php
src/Frontend/Core/Engine/TemplateModifiers.php
src/Frontend/Core/Engine/Rss.php
src/Frontend/Core/Engine/RssItem.php
src/Common/Doctrine/Repository/MetaRepository.php
src/Backend/Modules/Pages/Engine/CacheBuilder.php
src/Frontend/Modules/Search/Ajax/Autocomplete.php
src/Frontend/Modules/Search/Ajax/Autosuggest.php
src/Frontend/Modules/Search/Ajax/Livesuggest.php
src/Frontend/Modules/Search/Ajax/Save.php
src/Frontend/Modules/Search/Actions/Index.php
src/Backend/Core/Header/Header.php
Normalize form, widget, and position naming/IDs in frontend and backend templates, and clean up form-related labels and placeholders.
  • Frontend and backend form helpers now generate field names, widget checkbox names, position identifiers, and authenticatedUser* keys consistently using Symfony String instead of ad-hoc SpoonFilter casing
  • Frontend FormBuilder widget and Backend FormBuilder Helper re-map checkbox/radiobutton field names (chk*/rbt*) via Symfony String so that field HTML keys and validation align
  • Backend Form/File form elements now expose file field variables and error keys with names built via Symfony String, matching other template conventions
  • Mailmotor subscription/unsubscription forms and Authentication login forms update placeholders and button labels via Symfony String title casing
src/Frontend/Core/Engine/Form.php
src/Backend/Core/Engine/Form.php
src/Backend/Core/Engine/FormFile.php
src/Backend/Core/Engine/FormImage.php
src/Backend/Core/Engine/TwigTemplate.php
src/Backend/Modules/FormBuilder/Engine/Helper.php
src/Frontend/Modules/FormBuilder/Widgets/Form.php
src/Frontend/Modules/Mailmotor/Domain/Subscription/SubscribeType.php
src/Frontend/Modules/Mailmotor/Domain/Subscription/UnsubscribeType.php
src/Backend/Modules/Authentication/Actions/Index.php
Remove obsolete Core.site_domains configuration and its UI/validation; adjust tests and installer accordingly.
  • Settings index form no longer displays or validates the site_domains textarea; saving no longer normalizes or stores Core.site_domains
  • Core installer no longer seeds Core.site_domains in default settings
  • Test database fixture drops the Core.site_domains row to reflect new configuration
  • Any logic relying on site_domains must now be considered dead/removed; only language lists remain for multi-domain setups
src/Backend/Modules/Settings/Actions/Index.php
src/Backend/Core/Installer/CoreInstaller.php
tests/data/test_db.sql
Fix backend redirect helper typo and ensure navigation-driven redirects use new naming and module/action casing.
  • Backend Core Url::redirectToFistAvailableLink is renamed to redirectToFirstAvailableLink, and all recursive calls updated
  • Module/action casing when redirecting based on cached backend navigation now uses Symfony String camel()->title() before permission checks and URL creation
  • Authentication::isAllowedModule and Navigation use the same Symfony String-based module names, improving consistency across redirects and permission checking
src/Backend/Core/Engine/Url.php
src/Backend/Core/Engine/Authentication.php
src/Backend/Core/Engine/Navigation.php
Tighten miscellaneous behaviour in pages, forms, content blocks, and media/navigation helpers while aligning to new utilities.
  • Pages dropdown/tree builders now default typeOfDrop/tree via in_array instead of SpoonFilter::getValue, and escape navigation titles using htmlentities
  • Pages tree HTML section headings (MainNavigation/Meta/Footer/Root) now use Symfony String title labels
  • Frontend Page assigns empty positions as position{PositionName} using Symfony String title casing, avoiding undefined-template-variable issues
  • MediaLibrary folder navigation tree header and MediaGallery/DataGrid column headings now use Symfony String-based labels
  • Blog, FAQ, FormBuilder, Location, Search, Users, Tags, ContentBlocks, and MediaLibrary data grids adjust header labels and visibility columns to use the new label casing helpers
src/Backend/Modules/Pages/Engine/Model.php
src/Backend/Modules/Pages/Engine/CacheBuilder.php
src/Frontend/Core/Engine/Page.php
src/Backend/Modules/MediaLibrary/Manager/TreeManager.php
src/Backend/Modules/MediaGalleries/Domain/MediaGallery/MediaGalleryDataGrid.php
src/Backend/Modules/ContentBlocks/Domain/ContentBlock/ContentBlockDataGrid.php
src/Backend/Modules/ContentBlocks/Domain/ContentBlock/ContentBlockRevisionDataGrid.php
src/Backend/Modules/Faq/Actions/Categories.php
src/Backend/Modules/Faq/Engine/Model.php
src/Backend/Modules/Search/Actions/Statistics.php
src/Backend/Modules/Users/Actions/Index.php
src/Backend/Modules/Blog/Actions/Comments.php
src/Backend/Modules/Blog/Actions/Index.php
src/Backend/Modules/Blog/Actions/Edit.php
src/Backend/Modules/Blog/Actions/Categories.php
src/Backend/Modules/Tags/Actions/Edit.php
src/Backend/Modules/Tags/Actions/Index.php
src/Backend/Modules/FormBuilder/Actions/Index.php
src/Backend/Modules/Location/Engine/Model.php
src/Backend/Modules/MediaLibrary/Domain/MediaItem/MediaItemSelectionDataGrid.php
src/Backend/Modules/MediaLibrary/Domain/MediaItem/MediaItemDataGrid.php
Adjust CI workflow to limit push-triggered builds to master branch only.
  • GitHub Actions CI workflow now triggers on push events only for the master branch, plus pull_request and manual workflow_dispatch remain unchanged
.github/workflows/ci.yml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In Backend\Modules\FormBuilder\Engine\Model::getLocale the switch from SpoonFilter::toCamelCase($name) to s($name)->title()->toString() changes the lookup key semantics (title-casing vs camel-casing, different handling of underscores), which is likely to break existing locale key resolution; consider keeping camelCase behavior here or adding an explicit helper that mimics the original transformation.
  • In Backend\Modules\Blog\Actions\Categories::loadDataGrid the header label construction s(BL::lbl('Amount')->title()->toString()) appears to call title() on the string returned by BL::lbl, which will fail; this should likely be s(BL::lbl('Amount'))->title()->toString() as done elsewhere.
  • Across the changes, the use of s(...)->title() sometimes omits ->toString() and relies on implicit casting during concatenation while other places call ->toString() explicitly; it would be good to standardize on one approach (preferably always casting to string) to keep behavior and readability consistent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `Backend\Modules\FormBuilder\Engine\Model::getLocale` the switch from `SpoonFilter::toCamelCase($name)` to `s($name)->title()->toString()` changes the lookup key semantics (title-casing vs camel-casing, different handling of underscores), which is likely to break existing locale key resolution; consider keeping camelCase behavior here or adding an explicit helper that mimics the original transformation.
- In `Backend\Modules\Blog\Actions\Categories::loadDataGrid` the header label construction `s(BL::lbl('Amount')->title()->toString())` appears to call `title()` on the string returned by `BL::lbl`, which will fail; this should likely be `s(BL::lbl('Amount'))->title()->toString()` as done elsewhere.
- Across the changes, the use of `s(...)->title()` sometimes omits `->toString()` and relies on implicit casting during concatenation while other places call `->toString()` explicitly; it would be good to standardize on one approach (preferably always casting to string) to keep behavior and readability consistent.

## Individual Comments

### Comment 1
<location> `src/Backend/Modules/Blog/Actions/Categories.php:38` </location>
<code_context>
         // set headers
         $this->dataGrid->setHeaderLabels([
-            'num_items' => \SpoonFilter::ucfirst(BL::lbl('Amount')),
+            'num_items' => s(BL::lbl('Amount')->title()->toString()),
         ]);

</code_context>

<issue_to_address>
**issue (bug_risk):** The chained `->title()->toString()` is applied on the result of `BL::lbl()` instead of on the Symfony string, which will cause a runtime error.

`BL::lbl('Amount')` returns a string, but `->title()->toString()` is called on that string before it is passed to `s()`, which will cause `Call to a member function title() on string` at runtime.

It should be:
```php
'num_items' => s(BL::lbl('Amount'))->title()->toString(),
```
so `title()` is applied to the `UnicodeString` returned by `s()`, not to the raw string.
</issue_to_address>

### Comment 2
<location> `src/Backend/Modules/FormBuilder/Engine/Model.php:478-480` </location>
<code_context>
-        $name = \SpoonFilter::toCamelCase($name);
-        $class = \SpoonFilter::ucfirst($application) . '\Core\Language\Language';
-        $function = 'get' . \SpoonFilter::ucfirst($type);
+        $name = s($name)->title()->toString();
+        $class = s($application)->title()->toString() . '\Core\Language\Language';
+        $function = 'get' . s($type)->title()->toString();

         // execute and return value
</code_context>

<issue_to_address>
**issue (bug_risk):** Changing from `toCamelCase`/`ucfirst` to `title()` in `getLocale` alters lookup keys and may break existing translations.

Using `s(...)->title()` for `$name` changes its format compared to `�SpoonFilter::toCamelCase($name)`, especially for multi-word or underscore-separated keys, so existing camelCase locale keys may no longer match and translations could stop resolving.

To keep behavior while replacing `ucfirst`, you can leave `$name` as camelCase and only change the parts that originally used `ucfirst`:
```php
$name = \SpoonFilter::toCamelCase($name);
$class = s($application)->title()->toString() . '\\Core\\Language\\Language';
$function = 'get' . s($type)->title()->toString();
```
This keeps `getLocale()` backward compatible.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@tijsverkoyen tijsverkoyen changed the base branch from 534-github-actions-in-line-with-gitlab to master January 28, 2026 07:17
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 5 issues, and left some high level feedback:

  • There are a few obvious syntax issues introduced when replacing SpoonFilter helpers, e.g. s($url->getModule() . ' ' . $url->getAction())->camel->toString() in Backend/Core/Engine/Form.php (missing () on camel()) and s(BL::lbl('Amount')->title()->toString()) in Blog/Actions/Categories.php (calling ->title() on the result of BL::lbl()), which need to be fixed before this can run.
  • The new manual link detection in Frontend/Core/Engine/TemplateModifiers::cleanupPlainText() and Backend/Core/Engine/DataGridFunctions::cleanupPlainText() (simple ~(https?://[^\s]+)~ regex) is much less feature-complete than SpoonFilter::replaceURLsWithAnchors and may mishandle edge cases (trailing punctuation, existing <a> tags, non-HTTP links, HTML contexts); consider either reusing a more robust utility or tightening the regex/logic to avoid regressions.
  • The repeated construction of label keys with s(...)->replace('_', ' ')->camel()->title() is duplicated in many places (forms, widgets, datagrids, URL/module/action handling); pulling this into a small shared helper (e.g. toTemplateKey() / toLabelKey()) would reduce the chance of subtle inconsistencies and make future changes to the naming convention much easier.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- There are a few obvious syntax issues introduced when replacing SpoonFilter helpers, e.g. `s($url->getModule() . ' ' . $url->getAction())->camel->toString()` in `Backend/Core/Engine/Form.php` (missing `()` on `camel()`) and `s(BL::lbl('Amount')->title()->toString())` in `Blog/Actions/Categories.php` (calling `->title()` on the result of `BL::lbl()`), which need to be fixed before this can run.
- The new manual link detection in `Frontend/Core/Engine/TemplateModifiers::cleanupPlainText()` and `Backend/Core/Engine/DataGridFunctions::cleanupPlainText()` (simple `~(https?://[^\s]+)~` regex) is much less feature-complete than `SpoonFilter::replaceURLsWithAnchors` and may mishandle edge cases (trailing punctuation, existing `<a>` tags, non-HTTP links, HTML contexts); consider either reusing a more robust utility or tightening the regex/logic to avoid regressions.
- The repeated construction of label keys with `s(...)->replace('_', ' ')->camel()->title()` is duplicated in many places (forms, widgets, datagrids, URL/module/action handling); pulling this into a small shared helper (e.g. `toTemplateKey()` / `toLabelKey()`) would reduce the chance of subtle inconsistencies and make future changes to the naming convention much easier.

## Individual Comments

### Comment 1
<location> `src/Backend/Core/Engine/Form.php:35` </location>
<code_context>

         parent::__construct(
-            $name ?? \SpoonFilter::toCamelCase($url->getModule() . '_' . $url->getAction(), '_', true),
+            $name ?? s($url->getModule() . ' ' . $url->getAction())->camel->toString(),
             $action,
             $method ?? 'post',
</code_context>

<issue_to_address>
**issue (bug_risk):** String helper call is missing parentheses, causing a runtime error.

`s($url->getModule() . ' ' . $url->getAction())->camel->toString()` accesses `camel` as a property, which will throw at runtime. Call it as a method instead: `s(...)->camel()->toString()`.
</issue_to_address>

### Comment 2
<location> `src/Backend/Modules/Blog/Actions/Categories.php:37-38` </location>
<code_context>

         // set headers
         $this->dataGrid->setHeaderLabels([
-            'num_items' => \SpoonFilter::ucfirst(BL::lbl('Amount')),
+            'num_items' => s(BL::lbl('Amount')->title()->toString()),
         ]);

</code_context>

<issue_to_address>
**issue (bug_risk):** Method chaining on `BL::lbl()` is mis-parenthesized and will not compile.

`BL::lbl('Amount')` returns a string, so calling `->title()` on it is invalid. You probably meant to chain on the `Stringy` wrapper instead:

```php
'num_items' => s(BL::lbl('Amount'))->title()->toString(),
```
</issue_to_address>

### Comment 3
<location> `src/Frontend/Core/Engine/TemplateModifiers.php:475-476` </location>
<code_context>
     {
-        // detect links
-        $var = SpoonFilter::replaceURLsWithAnchors($var);
+        // replace links with a tags
+        $var = s($var)->replaceMatches(
+            '~(https?://[^\s]+)~',
</code_context>

<issue_to_address>
**issue (bug_risk):** The new URL-to-anchor replacement is much more naive than `replaceURLsWithAnchors` and may mis-handle HTML and punctuation.

Previously `SpoonFilter::replaceURLsWithAnchors()` encapsulated safer link detection and anchor generation; the new `~(https?://[^\s]+)~` on the raw string can:
- Match inside existing HTML (attributes, already-linked URLs), breaking markup.
- Swallow trailing punctuation into the URL.
- Create nested/malformed anchors in user-supplied HTML.

Consider reusing the existing helper or, if you must inline this, tightening the regex and/or applying it only to plain text (e.g. after stripping tags) to avoid HTML corruption and XSS risk.
</issue_to_address>

### Comment 4
<location> `src/Backend/Core/Engine/DataGridFunctions.php:26-27` </location>
<code_context>
     {
-        // detect links
-        $var = SpoonFilter::replaceURLsWithAnchors($var);
+        // replace links with a tags
+        $var = s($var)->replaceMatches(
+            '~(https?://[^\s]+)~',
+            '<a href="$1" rel="nofollow noopener">$1</a>'
</code_context>

<issue_to_address>
**issue (bug_risk):** Same naive link replacement pattern here, with an explicit `rel` that may not be desired in all contexts.

`cleanupPlainText()` now uses a simple URL regex and always wraps matches as `<a href="$1" rel="nofollow noopener">`. This risks:
- Breaking existing markup.
- Including trailing punctuation/entities in the URL.
- Forcing a specific `rel` that may not suit all callers.
Consider reusing the dedicated linkification helper, or making the regex and `rel` handling more robust/configurable.
</issue_to_address>

### Comment 5
<location> `src/Backend/Modules/FormBuilder/Ajax/SaveField.php:231-239` </location>
<code_context>
         if ($type !== 'paragraph') {
             if ($values !== '') {
-                $values = \SpoonFilter::htmlspecialchars($values);
+                $values = htmlspecialchars($values);
             }
             if ($defaultValues !== '') {
-                $defaultValues = \SpoonFilter::htmlspecialchars($defaultValues);
+                $defaultValues = htmlspecialchars($defaultValues);
             }
         }
</code_context>

<issue_to_address>
**🚨 suggestion (security):** Switching from SpoonFilter HTML escaping to bare `htmlspecialchars()` changes encoding flags and charset handling.

`\SpoonFilter::htmlspecialchars()` previously encoded both single and double quotes and respected the app charset. The raw `htmlspecialchars()` calls now rely on defaults (`ENT_COMPAT`, default encoding), so single quotes are no longer escaped and behavior may change if the default encoding isn’t UTF‑8—especially for HTML attributes. To retain prior behavior, explicitly pass flags and encoding, e.g. `htmlspecialchars($values, ENT_QUOTES, 'UTF-8')`.

```suggestion
        // htmlspecialchars except for paragraphs
        if ($type !== 'paragraph') {
            if ($values !== '') {
                $values = htmlspecialchars($values, ENT_QUOTES, 'UTF-8');
            }
            if ($defaultValues !== '') {
                $defaultValues = htmlspecialchars($defaultValues, ENT_QUOTES, 'UTF-8');
            }
        }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request refactors the codebase to replace legacy SpoonFilter utility methods with modern alternatives: Symfony String component for string manipulation, native PHP functions for HTML encoding/decoding and filtering, and Symfony Validator for validation. Additionally, it removes the deprecated site_domains setting from Core and fixes a typo in the backend navigation helper.

Changes:

  • Replace SpoonFilter string methods (ucfirst, toCamelCase) with Symfony String s()->title()/camel() across 100+ files
  • Replace SpoonFilter HTML encoding/decoding methods with native PHP htmlspecialchars, htmlentities, and their decode equivalents
  • Replace SpoonFilter validation methods (isURL, isValidAgainstRegexp, isNumeric, isInteger) with Symfony Validator constraints and native PHP filter functions
  • Remove deprecated Core site_domains setting from installer, settings form, test data, and validation
  • Fix typo in Backend URL helper: redirectToFistAvailableLinkredirectToFirstAvailableLink

Reviewed changes

Copilot reviewed 112 out of 112 changed files in this pull request and generated 38 comments.

Show a summary per file
File Description
tests/data/test_db.sql Remove site_domains test fixture
src/Frontend/** Replace SpoonFilter with Symfony String/native PHP in Search, Tags, Profiles, Mailmotor, FormBuilder, Blog modules and core classes
src/Common/** Replace SpoonFilter in Twig modifiers, core model/form, and meta repository
src/Backend/Modules/Settings/** Remove site_domains setting form fields and validation
src/Backend/Modules/** Replace SpoonFilter across all backend modules (Users, Tags, Pages, MediaLibrary, Locale, Groups, FormBuilder, Extensions, Blog, etc.)
src/Backend/Core/** Replace SpoonFilter in core engine classes, language, authentication, and installers
.github/workflows/ci.yml Restrict CI push trigger to master branch only

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@tijsverkoyen
Copy link
Member Author

  • There are a few obvious syntax issues introduced when replacing SpoonFilter helpers, e.g. s($url->getModule() . ' ' . $url->getAction())->camel->toString() in Backend/Core/Engine/Form.php (missing () on camel()) and s(BL::lbl('Amount')->title()->toString()) in Blog/Actions/Categories.php (calling ->title() on the result of BL::lbl()), which need to be fixed before this can run.

Fixed.

  • The new manual link detection in Frontend/Core/Engine/TemplateModifiers::cleanupPlainText() and Backend/Core/Engine/DataGridFunctions::cleanupPlainText() (simple ~(https?://[^\s]+)~ regex) is much less feature-complete than SpoonFilter::replaceURLsWithAnchors and may mishandle edge cases (trailing punctuation, existing <a> tags, non-HTTP links, HTML contexts); consider either reusing a more robust utility or tightening the regex/logic to avoid regressions.

Correct, won't bother.

  • The repeated construction of label keys with s(...)->replace('_', ' ')->camel()->title() is duplicated in many places (forms, widgets, datagrids, URL/module/action handling); pulling this into a small shared helper (e.g. toTemplateKey() / toLabelKey()) would reduce the chance of subtle inconsistencies and make future changes to the naming convention much easier.

Myeah. True. Will ask the opinion of the other devs.

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.

4 participants