Skip to content

Add a data grid bundle wrapping pentiminax/ux-datatables - #202

Open
pierredup wants to merge 28 commits into
mainfrom
datagrid
Open

Add a data grid bundle wrapping pentiminax/ux-datatables#202
pierredup wants to merge 28 commits into
mainfrom
datagrid

Conversation

@pierredup

Copy link
Copy Markdown
Member

Adds a DataGridBundle that wraps pentiminax/ux-datatables, so an application gets a Doctrine-backed, Tabler-styled data grid from one class declaration and one Twig tag:

#[AsDataTable(Client::class)]
final class ClientDataGrid extends AbstractDataGrid
{
}
<twig:Platform:DataGrid name="client" />

Columns come from the entity's Doctrine mapping, the table is server-side, and the Ajax endpoint, table token, CSRF protection and Bootstrap 5 theme are wired for you. Opt out entirely with datagrid.enabled: false — neither this bundle nor its dependency is registered.

What's here

  • AbstractDataGrid — platform defaults (server-side, Bootstrap 5, page length, responsive, column control, server-side CSV/XLSX export). Every default is overridable through the matching configure*() hook.
  • Doctrine column auto-detection — maps field types to column types and humanises titles. Skips fields with no public reader, collection associations, and to-one associations whose target does not stringify.
  • <twig:Platform:DataGrid name="…" /> — grids are addressed by a name derived from the class (ClientDataGridclient), overridable with a NAME constant. Rendering the same grid twice on a page gives the second table a -2 id suffix.
  • IconColumn / ActionsColumn — rendered server-side through ux_icon(), so grid icons use the configured Tabler pack rather than upstream's Lucide set. The action buttons emit the attributes upstream's Stimulus controller delegates on, so edit, delete and detail work unchanged.
  • DataGridAccessSubscriber — see below.
  • Frontend — the upstream controller is registered from core.ts, the bs5 chain only, with an IgnorePlugin suppressing the six unused style frameworks. Tabler overrides live in assets/scss/_datagrid.scss.
  • Docsdocs/datagrid/, linked from the docs index.

Security

Upstream's README is explicit that the table token identifies which grid is requested, not who is asking. Without a guard, any authenticated user holding another grid's token can read it — and that token travels in a query string, which upstream notes leaks through logs and the Referer header.

DataGridAccessSubscriber adds two layers over all eight Ajax routes:

  1. A coarse gate — datagrid.security.ajax_access, default IS_AUTHENTICATED_FULLY.
  2. The resolved grid's own getSecurityAttribute(), checked on every route from which a grid is resolvable, taking the read token from the query string (data, export) or the body (templates), and the action token from the body (the rest).

It is deliberately not a prepended security.access_control rule: that list is first-match-wins, so injecting an entry would silently change the meaning of an application's existing rules.

Worth knowing: only three of upstream's eight controllers validate CSRF at all, and AjaxTemplateRenderController — which re-hydrates entities from Doctrine by id — validates none. That is why the per-grid check covers everything rather than a subset.

Notable behaviours

  • ActionsColumn renders its own markup, so it does not reproduce upstream's mutationsEnabled disabling of the delete button. The server still validates CSRF, so a delete attempted without a session fails server-side rather than being disabled client-side.
  • Per-row Ajax actions are unsupported by ActionsColumn: their CSRF token is added to the row only after template columns render. Use upstream's ActionColumn for those.
  • The styling deliberately contains no rules for the length menu or search box — the bs5 integration already applies form-select/form-control to them, so adding rules there would duplicate Bootstrap and break dark mode.
  • Mercure is out of scope for this integration.

Testing

87 tests / 142 assertions covering the config tree and its builder, the DI extension and autoconfiguration, Doctrine column mapping, name resolution and the registry compiler pass, the renderer, both column types, the access subscriber, and the Kernel's opt-out and route registration.

pentiminax/ux-datatables is pinned ~0.87.0 — it is pre-1.0 with an active upgrade guide.

Pre-existing issues this branch does not fix

These predate the branch and are deliberately out of scope, but they are worth their own work:

  • The frontend cannot be built. assets/package.json has no scripts key, assets/webpack.config.js ends export default Encore rather than Encore.getWebpackConfig(), @symfony/stimulus-bridge wants a controllers.json alias that does not exist, and ESLintPlugin has no config. With those present webpack emits zero output files.
  • vendor/bin/phpstan reports 9 errors and vendor/bin/phpunit reports 13 errors + 2 deprecations, all in src/Bundle/Saas, tests/Bundle/Saas and src/Bundle/Platform/DependencyInjection. This branch touches none of those files and adds no new failures — the DataGrid suite is green and PHPStan is clean at level max over src/Bundle/DataGrid and tests/Bundle/DataGrid.
  • rector conflicts with PHPStan on two DataGrid files: it wants to strip @var annotations that PHPStan needs for narrowing before a bare return. Applying it introduces 3 new PHPStan errors, so both files are left as they are.
  • Regenerating the lockfile bumped @tiptap/core 2.27.2 → 3.31.3. assets/package.json already declared ^3.31.3; the stale lockfile was violating its own range. node(deps): bump @tiptap/starter-kit from 2.27.3 to 3.31.3 #200 is bumping @tiptap/starter-kit to match.

https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY

Aliases the bare @pentiminax/ux-datatables specifier at the compiled
controller inside the Composer package, since the package is not published
to npm, and ignores the six unused style frameworks so webpack does not try
to resolve their static import() calls at build time.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
assets/package.json added five datatables.net packages but bun.lock was
never regenerated, so it had zero references to them. bun install resolves
to the workspace root, not assets/bun.lock, which was a stale duplicate bun
no longer maintains and is left untouched here.

Also drops the "test-app" workspace entry from the root package.json, which
pointed at a directory that has never existed and blocked bun install from
resolving workspaces at all.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
Defines the datagrid: block for platform.yaml along with its fluent PHP
builder, covering page size, responsive and column control toggles, the
table CSS class, the Ajax route access attribute and export formats.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
The bundle is yielded from the Kernel only when the config section does not
disable it, so an application that does not want grids loads neither this
bundle nor pentiminax/ux-datatables. The extension prepends the platform's
page size, table class and Bootstrap 5 edit modal templates onto the
datatables extension.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
`Pentiminax\UX\DataTables\DataTablesBundle` derives its container extension
alias as `data_tables` (AbstractBundle's default underscore of the class
name), not `datatables`, so the prepend() block that forwards page size,
table class and the Bootstrap 5 edit modal templates was silently dead in
every real application; only the test's own stub extension, which hardcoded
the wrong alias, made it look wired up. A new test now pins the stub's alias
to the real bundle's so this cannot rot unnoticed again.

Also creates `src/Bundle/DataGrid/templates/` (populated in Task 7) since
Twig's FilesystemLoader throws immediately if a registered namespace path
does not exist, and the grid bundle defaults to enabled.

Kernel::isDataGridEnabled() now coerces the `enabled` flag with filter_var()
instead of a strict `=== true` comparison: unlike two-factor, which defaults
off and safely degrades on a type mismatch, the grid defaults on, so a
strict comparison silently disabled it for "true", 1, or an unresolved
%env(...)% placeholder. Added Kernel tests covering the section being
absent, non-array, and enabled/disabled via bool, string and int scalars.

Also fills in the previously-unasserted setParameter() calls in the
extension test (responsive, column_control, table_class, export.formats,
edit_modal.enabled).

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
phpstan.dist.neon scans src/ and tests/, but both prior verification passes
scoped phpstan to src/Bundle/DataGrid only, so nine offset-on-mixed and
return-type errors in the DataGrid tests went unnoticed. Processor::process()
returns array<string, mixed>, so accessing typed keys on the result degrades
to mixed; DataGridConfigurationTest now imports the DataGridConfig phpstan
type from DataGridConfiguration and annotates its process() helper with it,
the same pattern UiConfigurationTest already uses for UiConfig. The extension
test's two remaining errors come from ContainerBuilder::getExtensionConfig()
returning array<array<string, mixed>>; each nested value is now asserted
with assertIsArray() and bound to a locally-typed variable before its keys
are read, rather than reaching through three offsets in one expression.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
Maps each Doctrine field type to its column type and humanises the title, so
a grid renders without declaring columns. Skips fields with no public
reader, collection associations, and to-one associations whose target does
not stringify, since none of them can be read or ordered meaningfully.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
Client's only to-many fixture, tags, targets Tag, which is not Stringable,
so testToManyAssociationIsSkipped passed even with the to-many guard
removed entirely -- it was really exercising the Stringable check, not the
to-many rule. Adds a countries ManyToMany targeting Country, which IS
Stringable, so the assertion can only pass because of the to-many guard.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
Grids are server-side and Bootstrap 5 styled, carry the Tabler table
classes, the configured page size and length menu, and the responsive,
column control and server-side export extensions. Columns fall back to
Doctrine metadata when the grid declares none.

Also registers AbstractDataGrid for autoconfiguration so the container
calls setDataGridDefaults() via setter injection on every grid service,
mirroring how upstream injects its own DataTableInfrastructure. This
closes the gap Task 3 deliberately left open, since it depended on
classes this task introduces.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
Derives a grid's name from its class, overridable with a NAME constant, and
builds a service locator keyed by that name at compile time so a template can
render a grid without knowing its class. Duplicate names fail the build
rather than silently shadowing one another.

Registers DataGridRegistry with autowiring off, since its only argument (the
locator) is supplied by DataGridRegistryPass at compile time and there is
nothing for the autowirer to resolve.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
testDuplicateNamesAreRejected registered the same class under two service
ids, so the exception's two class-name placeholders were always identical.
The assertion passed whether DataGridRegistryPass correctly named both
colliding classes or wrongly printed one of them twice. Registers two
distinct grids that collide on name instead, and asserts both fully-
qualified class names appear in the message.

No production change: DataGridRegistryPass was already correct.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
<twig:Platform:DataGrid name="client" /> resolves the grid from the registry
and renders it, so a page needs no controller wiring. Repeat renders of one
grid get a unique element id: the id is read back out of the markup upstream
already produced, rather than off the grid itself, because reading it via
AbstractDataGrid::getDataTable() would force upstream's initialize() to run
and throw for a grid with no DataGridDefaults injected.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
Upstream derives the DOM id from a grid's short class name, not its FQCN, so
two grids sharing a short name in different namespaces (a case this plan's
own NAME override exists to support) would collide on id="ClassName" while
landing under two different counter keys, and neither render got rewritten.
Key the counter the same way upstream derives the id, and reshape the
first-occurrence test so its decoy is actually reachable by the regex.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
Add spaces around union (|) and intersection (&) operators in type
declarations, and format setAttributes array across multiple lines per
TypesSpacesFixer configuration.
Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
Both are template columns rendered server-side through ux_icon(), so they use
the configured icon pack rather than the Lucide set upstream resolves in the
browser. The action buttons emit the same data-action-type and data-id
attributes the upstream controller delegates on, so edit, delete and detail
keep working unchanged.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
… the delete() docblock

actions.html.twig called path() with a null id whenever a row lacked the
idField key, throwing InvalidParameterException for the whole row. Upstream's
ActionRowDataResolver treats a failed URL generation as "omit the action";
this now does the same, skipping the CUSTOM link while leaving the row's
other actions intact.

The delete() docblock claimed the button is disabled without a session, but
that behaviour lives in upstream's Stimulus-rendered actionColumnRenderer.js
and this column renders its own markup, bypassing it entirely - delete always
renders enabled and relies on server-side CSRF validation instead.

Also closes a gap where a mutator's own re-send of the template parameters
went unguarded whenever a later mutator in the same chain happened to
re-send the same state.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
… guard their own re-send

The previous fix round's chained test (an action mutator then identifiedBy())
only proved the interaction between the two; identifiedBy() runs last and its
own applyTemplate() call re-sends whatever addAction() already mutated, so a
broken edit(), delete() or link() that dropped its own applyTemplate() call
stayed invisible. Each of the three now has its own isolated test that calls
exactly one mutator and asserts immediately, with nothing after it to mask a
skipped re-send - the same shape IconColumnTest already used for icons() and
fallbackIcon(). No production code changed.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
Requires the configured attribute on all eight routes the DataTables bundle
registers, and additionally checks a grid's own attribute on the two read
routes. The upstream table token identifies which grid is requested, not who
is asking, so without the second check any authenticated user holding a token
could read that grid.

Implemented as a subscriber rather than a prepended access_control rule,
which is first-match-wins and would change the meaning of an application's
existing rules.

AjaxDataTableRegistry and AjaxDataTableTokenManager are both final upstream,
so the test builds a real registry over a real token manager and a real
ServiceLocator instead of stubbing them, deriving tokens through the
registry's own getToken().

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
… is resolvable, including ux_datatables_ajax_templates, which carries the same unprivileged read token in the request body and validates no CSRF.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
Imports the DataTables Bootstrap 5 theme and the enabled extensions, then
removes the duplicated table chrome, gives the length menu and search box
Tabler's control styling, and aligns the info and paging rows with the card
spacing.

Also ignores assets/node_modules/ and assets/public/, the build output
directories this task's frontend build creates; only /node_modules/ was
previously ignored at the repo root.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
Round-1 review found the partial was written against DataTables 1.x/early-2.x
class naming rather than the installed 3.0.3:

- .dataTables_wrapper never existed in the installed packages; the bs5
  integration's own wrapper class is dt-container. The table/row chrome
  fix this rule was meant to apply has been dead since round 1.
- span.dt-column-order can never match: DataTables creates this element as
  a bare <div>, and the vendor stylesheet never tag-qualifies the class
  either. Dropped the tag qualifier to match the vendor's own convention.

Also replaced @extend .form-select / @extend .form-control on the length
menu and search box with copied declarations (same Bootstrap/Tabler
variables and mixins, just not @extend), since extending a vendor class
appends our selector as an extra arm to every rule that mentions it -
validation states, input-group compounds, floating labels, tom-select
overrides included.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
The bs5 integration already applies the real Bootstrap classes to both
elements (node_modules/datatables.net-bs5/js/dataTables.bootstrap5.js:
search.input is "form-control form-control-sm", length.select is
"form-select form-select-sm"), and the vendor CSS already sizes them
inline. The round-1 hand-copy was redundant from the start and, on top of
that, regressed the dark-mode select chevron and Tabler's layered focus
box-shadow, both of which the real .form-select/.form-control rules
already handle for free. Deleting the block fixes both regressions and
removes the ongoing burden of keeping a hand-copy in sync with Bootstrap.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
Adds the user guide covering grid declaration, column auto-detection, the
icon and action columns, the security layers and the configuration
reference, and regenerates the platform.yaml JSON Schema so the datagrid
section autocompletes.

Corrects several claims in the original plan against the shipped code: the
per-grid security check covers all eight Ajax routes (not two), only three
of those eight validate CSRF, ActionsColumn::delete() does not reproduce
upstream's client-side disabling, and per-row Ajax actions are unsupported
by ActionsColumn.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
Nothing ever read it. The key was wired through to a container parameter
under the mistaken assumption that AbstractDataGrid adds a default EDIT
action; actions are entirely user-declared on ActionsColumn, so the flag
could not gate anything as designed. A schema-visible knob that silently
ignores the user is worse than no knob, so it is removed rather than kept
around for a follow-up task that has no home in this plan.

Also fixes docs/datagrid/customization.md, where the PHP fluent builder
section named a nonexistent enabled() method — the builder only has
disabled() — which would have sent a reader straight into an undefined
method call.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
…id Ajax routes, and drop a stale config key

The actions.html.twig template read row[idField], but TemplateColumnRenderer
binds row to the source entity and payload to the mapped array, so the id
always resolved to null: EDIT/DELETE buttons rendered with an empty data-id
and CUSTOM links were silently swallowed by the not-null guard. Read
payload[idField] instead, with an attribute(row, idField) fallback for grids
that drop the id column in configureColumns(). ActionsColumnTest bound an
array as row, matching the same misreading, so it passed either way; it now
binds an entity as row and the mapped array as payload, and gained tests for
the payload path, the entity fallback, and the missing-identifier case.

Upstream's datatables.route_loader is tagged routing.route_loader, which
only registers it for lookup — nothing imports it unless a routing config
names it with type: service. Without the Flex recipe, a consuming app's
first grid render throws RouteNotFoundException. Kernel::configureRoutes()
now imports it, guarded by the same isDataGridEnabled() check used for
bundle registration.

Also removed the commented edit_modal key from platform.yaml's datagrid
reference block; DataGridConfiguration has no such option, so uncommenting
the block as documented would fail to boot.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
Copilot AI lite review requested due to automatic review settings September 10, 2026 13:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

src/Bundle/DataGrid/Resources/config/services.php excludes Config/, preventing DataGridConfiguration (a PlatformConfigurationInterface) from being registered/tagged, which breaks schema generation for the datagrid: section.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR introduces a new DataGridBundle under src/Bundle/DataGrid/ to provide a Doctrine-backed, Tabler-styled, server-side DataTables grid integration (wrapping pentiminax/ux-datatables), including security hardening for all upstream Ajax routes and extensive test + documentation coverage.

Changes:

  • Add SolidWorxPlatformDataGridBundle with autoconfiguration, defaults (AbstractDataGrid), grid registry + renderer, Doctrine column auto-detection, and Tabler-compatible columns.
  • Add DataGridAccessSubscriber to enforce a bundle-wide access gate plus an optional per-grid authorization attribute across all upstream Ajax routes.
  • Add docs + upgrade notes + schema/config examples; wire frontend assets (Encore aliasing, SCSS imports, DataTables packages).
File summaries
File Description
UPGRADE.md Upgrade notes for datagrid feature
tests/Bundle/PlatformBundle/KernelTest.php Kernel opt-in/out + routes tests
tests/Bundle/DataGrid/Grid/GridNameResolverTest.php Grid name derivation tests
tests/Bundle/DataGrid/Grid/DataGridRendererTest.php Renderer id-rewrite/counter tests
tests/Bundle/DataGrid/Grid/DataGridRegistryTest.php Registry lookup + error tests
tests/Bundle/DataGrid/Grid/AbstractDataGridTest.php Defaults + Doctrine fallback tests
tests/Bundle/DataGrid/Fixtures/Grid/SecuredDataGrid.php Fixture: role-secured grid
tests/Bundle/DataGrid/Fixtures/Grid/NamedDataGrid.php Fixture: NAME override grid
tests/Bundle/DataGrid/Fixtures/Grid/Legacy/ClientDataGrid.php Fixture: short-name collision
tests/Bundle/DataGrid/Fixtures/Grid/ExpressionSecuredDataGrid.php Fixture: Expression-secured grid
tests/Bundle/DataGrid/Fixtures/Grid/DuplicateNamedDataGrid.php Fixture: duplicate NAME collision
tests/Bundle/DataGrid/Fixtures/Grid/ClientDataGrid.php Fixture: basic grid
tests/Bundle/DataGrid/Fixtures/Entity/Tag.php Fixture entity for mapping rules
tests/Bundle/DataGrid/Fixtures/Entity/Country.php Fixture entity with __toString
tests/Bundle/DataGrid/Fixtures/Entity/Client.php Fixture entity covering edge cases
tests/Bundle/DataGrid/EventSubscriber/DataGridAccessSubscriberTest.php Subscriber security behavior tests
tests/Bundle/DataGrid/DependencyInjection/SolidWorxPlatformDataGridExtensionTest.php DI extension tests
tests/Bundle/DataGrid/DependencyInjection/CompilerPass/DataGridRegistryPassTest.php Compiler pass mapping tests
tests/Bundle/DataGrid/Config/DataGridConfigurationTest.php Config tree validation tests
tests/Bundle/DataGrid/Config/Builder/DataGridConfigBuilderTest.php Fluent builder tests
tests/Bundle/DataGrid/Column/IconColumnTest.php IconColumn template/params tests
tests/Bundle/DataGrid/Column/DoctrineColumnFactoryTest.php Doctrine type/visibility mapping tests
tests/Bundle/DataGrid/Column/ActionsColumnTest.php ActionsColumn rendering/behavior tests
src/Bundle/Platform/Kernel.php Conditional bundle + route import
src/Bundle/DataGrid/Twig/Components/DataGrid.php Twig component for rendering grids
src/Bundle/DataGrid/templates/components/data_grid.html.twig Twig component template
src/Bundle/DataGrid/templates/columns/icon.html.twig IconColumn cell template
src/Bundle/DataGrid/templates/columns/actions.html.twig ActionsColumn cell template
src/Bundle/DataGrid/templates/.gitkeep Keep templates directory in VCS
src/Bundle/DataGrid/SolidWorxPlatformDataGridBundle.php Bundle class + compiler pass registration
src/Bundle/DataGrid/Resources/config/services.php Service auto-registration config
src/Bundle/DataGrid/Grid/GridNameResolver.php Name resolution implementation
src/Bundle/DataGrid/Grid/DataGridRenderer.php Renderer with unique id suffixing
src/Bundle/DataGrid/Grid/DataGridRegistry.php Name → grid service resolution
src/Bundle/DataGrid/Grid/DataGridDefaults.php Defaults carrier + parameter wiring
src/Bundle/DataGrid/Grid/AbstractDataGrid.php Platform defaults + Doctrine column fallback
src/Bundle/DataGrid/Exception/UnknownDataGridException.php Missing-grid exception helper
src/Bundle/DataGrid/EventSubscriber/DataGridAccessSubscriber.php Ajax route authorization guard
src/Bundle/DataGrid/DependencyInjection/SolidWorxPlatformDataGridExtension.php Parameters, Twig paths, upstream config prepends
src/Bundle/DataGrid/DependencyInjection/CompilerPass/DataGridRegistryPass.php Builds ServiceLocator map for registry
src/Bundle/DataGrid/Config/DataGridConfiguration.php datagrid: config tree
src/Bundle/DataGrid/Config/Builder/DataGridConfigBuilder.php PHP fluent config builder
src/Bundle/DataGrid/Column/IconColumn.php Tabler icon template column
src/Bundle/DataGrid/Column/DoctrineColumnFactory.php Doctrine mapping → columns factory
src/Bundle/DataGrid/Column/ActionsColumn.php Server-rendered row actions column
platform.yaml Example datagrid config block
platform-schema.json JSON schema updates for datagrid
package.json Workspace adjustment
docs/index.md Add Data Grids docs link
docs/datagrid/security.md Security model documentation
docs/datagrid/index.md Data grids entrypoint docs
docs/datagrid/grids.md Grid authoring + defaults docs
docs/datagrid/customization.md Config + styling docs
docs/datagrid/columns.md Column mapping + custom columns docs
composer.json Add datagrid-related PHP deps + autoload
assets/webpack.config.js Alias upstream controller + IgnorePlugin
assets/scss/platform.scss Import datagrid stylesheet
assets/scss/_datagrid.scss DataTables bs5 + Tabler overrides
assets/package.json Add DataTables bs5 packages
assets/datatables-missing.js Stub controller when vendor missing
assets/core.ts Register upstream Stimulus controller
.gitignore Ignore assets build artifacts
Review details
  • Files reviewed: 60/63 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +28 to +30
$services
->load(SolidWorxPlatformDataGridBundle::NAMESPACE . '\\', dirname(__DIR__, 2))
->exclude(dirname(__DIR__, 2) . '/{Config,DependencyInjection,Resources,templates}');
…d rename the datatable Stimulus controller

Upstream registers DataTablesExtension only under the service id
datatables.twig_extension, with no class alias, so autowiring it by type (as
DataGridRenderer's constructor did) could never resolve and a real
application's container would fail to compile -- every existing test stubbed
the extension directly, so nothing caught it. Fixed by decorating
datatables.twig_extension with a new DataGridTwigExtension, wiring both
services explicitly in services.php, and having DataGridRenderer take the
decorated inner extension by id.

The decorator also replaces render_datatable() with one that delegates to
DataGridRenderer, which now also rewrites upstream's hardcoded, unoverridable
Stimulus controller identifier (pentiminax--ux-datatables--datatable) down to
the short "datatable" name assets/core.ts registers the controller under, in
both attribute shapes it appears in, without ever touching the view-value
JSON payload's contents.

Added a container test that compiles upstream's and our own services.php
together for real, catching the exact autowiring failure the rest of the
suite's stubbing hides.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
The decorator now owns the only render_datatable function in the
container, so narrowing its signature removed upstream's documented
second parameter from every consumer. PHP does not error when a userland
method is called with more arguments than it declares, so an attributes
array passed from a template was silently discarded rather than failing
loudly.

Restores the parameter on the Twig extension and threads it through
DataGridRenderer to the inner extension.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
…tribute-name position instead of a blanket string replacement, so it can no longer corrupt identifier-shaped text inside the escaped JSON view-value payload. Widened the character class to cover addController()'s -class and -outlet suffixes, whose outlet names are not lowercased, alongside -value. Replaced the test that gave false confidence -- it embedded the bare identifier with neither the data- prefix nor a trailing hyphen, so it never exercised the at-risk shape -- with one using the actual corrupting shape, and added coverage for the -class/-outlet suffixes.

Claude-Session: https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY
@pierredup

Copy link
Copy Markdown
Member Author

Three follow-up commits since the description above, prompted by a request to shorten the Stimulus controller identifier. The rename turned out to be the smaller half.

7067f0e — fixes a Critical latent bug that would have stopped this working at all.

DataGridRenderer::__construct() type-hinted Pentiminax\UX\DataTables\Twig\DataTablesExtension and was autowired, but that type is not resolvable: upstream registers the extension under the id datatables.twig_extension with no class alias. Verified against a real ContainerBuilderhasDefinition(), hasAlias() and has() all return false for the FQCN. A consuming application's container could not compile, so the grid could never have rendered.

Every test stubbed the extension, which is why this survived eleven per-task reviews and a whole-branch review. Both services are now wired explicitly, and tests/Bundle/DataGrid/Resources/config/ServicesTest.php compiles a real container from upstream's services.php plus this bundle's and asserts both services are constructible — it fails if the wiring is reverted to autowiring.

The rename itself. data-controller is now datatable rather than pentiminax--ux-datatables--datatable. Upstream hardcodes the long name and explicitly skips data-controller when applying table attributes, so it cannot be overridden through the public API. Instead, a DataGridTwigExtension decorates datatables.twig_extension and replaces render_datatable with one delegating to DataGridRenderer, which rewrites the identifier in the returned markup. Symfony's DecoratorServicePass moves the twig.extension tag to the decorator, so there is no duplicate function. Delegation is direct, so upstream's profiler collection, permission filtering, CSRF token, locale and extra controller all still run.

9cb3e3b — restores a parameter the decorator had dropped. The override initially omitted upstream's array $attributes = []. Since the decorator now owns the only render_datatable in the container, and PHP silently discards extra arguments to a non-variadic userland method, {{ render_datatable(table, {class: 'x'}) }} was quietly ignoring the attributes with no error.

05cbd36 — fixes silent payload corruption in the rewrite. The identifier rewrite's second replacement was an unscoped str_replace over the whole markup, contradicting its own docblock. Column className, data, title and name are free-form strings serialised into the escaped JSON data-…-view-value payload, so a column with className: 'data-pentiminax--ux-datatables--datatable-legacy' was silently rewritten to data-datatable-legacy inside that payload. Now bounded to the attribute-name position. The character class is [a-zA-Z0-9-]+ rather than lowercase-only, because StimulusAttributes::normalizeControllerName() does not lowercase and outlet names can carry uppercase.

Gates unchanged: 99 tests / 167 assertions in the DataGrid suite, PHPStan clean at level max, ECS at the single pre-existing error, and the full suite still at its pre-existing baseline of 13 errors + 2 deprecations in LemonSqueezyWebhookConsumerTest / PlanFeatureGateTest — untouched by this branch.

https://claude.ai/code/session_013neWyR1xfhg449TrVb4BMY

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.

2 participants