From f7fdbf811fba42a0ee772d60440312656cfc0f5b Mon Sep 17 00:00:00 2001 From: Viktor Masicek Date: Thu, 11 Dec 2025 14:22:51 +0100 Subject: [PATCH] Add DefaultOrderByIdSubscriber to ensure deterministic ordering of collections --- README.md | 14 +++++ composer.json | 1 + .../DefaultOrderByIdSubscriber.php | 51 +++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 src/Subscribers/DefaultOrderByIdSubscriber.php diff --git a/README.md b/README.md index dfdb4e4..4e83891 100644 --- a/README.md +++ b/README.md @@ -357,6 +357,20 @@ foreach ($profiles as $_profile) { You should always use new `EntityManager` instance, not the default one (because of `EntityManager::clear`). +### Default collections order by `id` + +If you want all collection associations (`OneToMany`, `ManyToMany`) to be automatically ordered by the primary key `id` (ASC), you can use the built-in Doctrine subscriber `DefaultOrderByIdSubscriber`. +It hooks into the metadata loading process and applies `ORDER BY id ASC` to every collection that does not already define its own ordering. +This ensures stable and deterministic ordering across your application without having to manually specify `orderBy` in every entity. + +Simply enable the subscriber in your neon configuration: + +```yaml +services: + - ADT\DoctrineComponents\Subscribers\DefaultOrderByIdSubscriber +``` + + ## Tips - Always have all logic inside a `filter` or `order` callback. This will ensure that all dependencies (like a logged user etc.) are already set. diff --git a/composer.json b/composer.json index 2d74df1..edcaf5b 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "require": { "php": ">=8.4", "doctrine/orm": "^2.18|^3.0", + "doctrine/event-manager": "^1.0|^2.0", "nette/utils": "^3.2|^4.0", "tracy/tracy": "^2.10", "psr/log": "^3.0", diff --git a/src/Subscribers/DefaultOrderByIdSubscriber.php b/src/Subscribers/DefaultOrderByIdSubscriber.php new file mode 100644 index 0000000..b65fabe --- /dev/null +++ b/src/Subscribers/DefaultOrderByIdSubscriber.php @@ -0,0 +1,51 @@ +getClassMetadata(); + + foreach ($metadata->associationMappings as $fieldName => $mapping) { + if (!in_array($mapping['type'], [ClassMetadata::ONE_TO_MANY, ClassMetadata::MANY_TO_MANY], true)) { + continue; + } + + // Add "ORDER BY id ASC", except there is "ORDER BY id ASC/DESC" + $addOrderById = true; + $orderBy = $mapping['orderBy'] ?? []; + foreach ($orderBy as $sort => $order) { + if ($sort === 'id') { + $addOrderById = false; + break; + } + } + + if ($addOrderById) { + $orderBy['id'] = 'ASC'; + $metadata->associationMappings[$fieldName]['orderBy'] = $orderBy; + } + } + } +}