feat: add semver filter to admin version columns#75
Merged
Conversation
Add a reusable EasyAdmin filter for dotted-version columns that compares using semver order (=, !=, >, >=, <, <=) instead of the default lexicographic string comparison, so e.g. "10.5.9" correctly sorts above "9.5.1" and "v5.5.40" sorts identically to "5.5.40". Comparison runs in SQL via a new Doctrine DQL function SEMVER_NUMERIC(version) that strips an optional leading v/V, then packs major/minor/patch/extra into a sortable BIGINT using SUBSTRING_INDEX + CAST AS UNSIGNED. A REGEXP guard on the column excludes non-semver rows (?, unknown, "main", ...) from results when the filter is active. Wired into the version columns on Installation (frameworkVersion, composerVersion), PackageVersion (version, latest), ModuleVersion (version), Site (phpVersion), GitTag (tag) and DockerImageTag (tag).
API Specification - Non-breaking changesAPI Changelog 1.0.0 vs. 1.0.0No changes detected |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #75 +/- ##
==========================================
Coverage ? 40.92%
Complexity ? 862
==========================================
Files ? 125
Lines ? 2710
Branches ? 0
==========================================
Hits ? 1109
Misses ? 1601
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Three independent bugs were hiding behind each other and meant the filter silently no-oped (matched everything). Browser test against ?filters[type]=drupal&filters[frameworkVersion][comparison]=<= &filters[frameworkVersion][value]=10.6.3 returned 11.x rows. 1. SemverFilter::apply() assumed FilterDataDto::getValue() returned the full compound-form array and did `if (!is_array($data)) return;`. In EasyAdmin 5 the DTO already splits compound forms into getValue() (the scalar) and getComparison() (the operator), so the early return fired and no constraint was added. Switched to the documented API. 2. Once the WHERE clause was added, the original draft used a DQL-level REGEXP keyword for the non-semver guard. DQL doesn't recognise REGEXP and Doctrine threw a syntax error. Moved the regex into SEMVER_NUMERIC itself: the function now wraps its arithmetic in CASE WHEN col REGEXP '...' THEN ... ELSE NULL END, so non-semver rows collapse to NULL and are excluded by any comparison. 3. Passing the user input through SEMVER_NUMERIC a second time emitted the argument-placeholder five times (the function references its argument once for the regex and four times for the segments), so PDO saw five `?` but only one bound value. Computed the numeric in PHP (toSemverNumeric) and bind it as a single BIGINT. Multipliers shrunk from 10^6 to 10^4 per segment so the precomputed value fits in PHP_INT_MAX. Adds a regression test against FilterDataDto's actual shape to keep all three from coming back.
Adds two range operators so the admin can filter by a version interval in a single filter: * between (inclusive) → SEMVER_NUMERIC(col) >= a AND <= b * between (exclusive) → SEMVER_NUMERIC(col) > a AND < b The exclusive variant covers the common "greater than X, lower than Y" case directly (e.g. >10 AND <11.3 in one filter). SemverFilterType gains a `value2` text field used as the upper bound for the range operators (ignored otherwise). SemverFilter::apply() validates both ends through composer/semver and binds the precomputed BIGINTs as two parameters. Also fixes a latent bug in the COMPARISON_CHOICES validity check that was using values as keys; it happened to work for =/!=/etc. because the key and value were identical, but broke once between-style entries had distinct labels.
Clicking the column header on a version column (ver., Comp., PHP, Tag, version/latest) now sorts in semver order instead of lexicographic, so 10.5.9 correctly comes above 9.5.1 and 11.2.10 above 11.2.8. Implemented with a tiny SemverSort helper that walks the QueryBuilder's existing orderBy parts and rewrites "entity.<prop> <dir>" into "SEMVER_NUMERIC(entity.<prop>) <dir>" for the listed properties. Each affected CRUD controller overrides createIndexQueryBuilder() to invoke it for its version columns: * InstallationCrudController: frameworkVersion, composerVersion * PackageVersionCrudController: version, latest * ModuleVersionCrudController: version * SiteCrudController: phpVersion * GitTagCrudController: tag * DockerImageTagCrudController: tag Non-matching ORDER BY parts are left untouched, so combined sorts (e.g. ORDER BY type ASC, frameworkVersion DESC) still work.
…ollerTrait
The six CRUD controllers all had the same createIndexQueryBuilder()
override calling SemverSort::apply() with a property list — plus
seven matching use statements each.
Extracted the boilerplate into App\Trait\SemverSortableCrudControllerTrait,
which declares one abstract semverSortedProperties(): array hook and
provides the override. Each controller now reduces to:
use SemverSortableCrudControllerTrait;
protected function semverSortedProperties(): array
{
return ['frameworkVersion', 'composerVersion'];
}
Net −38 lines across the six controllers; behavior unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a reusable EasyAdmin filter to every admin version column so we can filter sites by semver comparison (
=, !=, >, >=, <, <=) instead of lexicographic string comparison — e.g. now we can find "all Drupal sites < 10.0.0" without9.5.1falsely matching> 10.0.0.Origin: in-conversation request — no GitHub issue.
How it works
SEMVER_NUMERIC(version)strips an optional leadingv/Vand packsmajor.minor.patch.extrainto a sortable BIGINT (major * 10^18 + minor * 10^12 + patch * 10^6 + extra) viaSUBSTRING_INDEX+CAST AS UNSIGNED. This is the MariaDB analogue of Postgres'sstring_to_array(...)::int[]trick.SemverFilter+SemverFilterTypeuse EasyAdmin 5'sFilterInterface+FilterTraitpattern (mirrors the existingFrameworkFilter). The filter UI is a compound form with an operator dropdown and a text value field.REGEXP '^[vV]?[0-9]+(\.[0-9]+){0,3}$'guard excludes non-semver values (?,unknown,main,release-1.0, …) from results when the filter is active. Invalid user input short-circuits toWHERE 1 = 0(validated viaComposer\Semver\VersionParser::normalize).QueryBuilder.Files Changed
src/Doctrine/Functions/SemverNumeric.php(new) — DQL function returning a sortable BIGINT for a dotted-version string.src/Form/Type/Admin/SemverFilter.php(new) — EasyAdmin filter applying the DQL function + REGEXP guard.src/Form/Type/Admin/SemverFilterType.php(new) — compound form (operator + value).config/packages/doctrine.yaml— registerSEMVER_NUMERICunderdql.numeric_functions.src/Controller/Admin/InstallationCrudController.php— apply filter toframeworkVersionandcomposerVersion.src/Controller/Admin/PackageVersionCrudController.php— apply filter toversionandlatest.src/Controller/Admin/ModuleVersionCrudController.php— apply filter toversion.src/Controller/Admin/SiteCrudController.php— apply filter tophpVersion.src/Controller/Admin/GitTagCrudController.php— apply filter totag.src/Controller/Admin/DockerImageTagCrudController.php— apply filter totag.CHANGELOG.md—[Unreleased]entry.Test Plan
docker compose exec phpfpm composer tests→ 22/22 green (verified locally).docker compose exec phpfpm composer coding-standards-check→ 0 fixes needed (verified locally).docker compose exec phpfpm vendor/bin/phpstan analyse src→ OK (verified locally)./admin/installation, applyver. > 10.0.0→10.5.9✓,9.5.1✗ (the lexicographic-vs-semver smoke test)./admin/installation, applyver. < 10.0.0→9.5.1✓,10.5.9✗./admin/installation, applyver. = 10.5.9→ exact match only./admin/installation, applyver. = abc→ empty result, no 500./admin/package-version, applyVersion > v5.0.0→v5.5.40✓ (verifiesv-prefix handling)./admin/package-version, combine semver filter withPackage = laravel/framework→ both constraints applied./admin/module-version,/admin/site,/admin/git-tag,/admin/docker-image-tag: filter UI shows operator dropdown + text input, results are correct.Notes
SUBSTRING_INDEX,TRIM(LEADING ...),REGEXP). Project runs only on MariaDB so portability is not a concern.UNSIGNED BIGINT(max ~1.84·10^19).