diff --git a/CHANGELOG.md b/CHANGELOG.md
index 337284d18..bc4968012 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,7 +22,7 @@ All notable changes to this project will be documented in this file.
- Aligned with v. 2.5.2.
- Removed themes.
- Added command to migrate config.json files.
-- Fix data fetching bug + tests
+- Fix data fetching bug and tests
- Refactored screen layout commands.
- Moved list components (search and checkboxes) around.
- Aligned environment variable names.
diff --git a/assets/admin/components/slide/slide-manager.jsx b/assets/admin/components/slide/slide-manager.jsx
index 4ff804386..eeee9320b 100644
--- a/assets/admin/components/slide/slide-manager.jsx
+++ b/assets/admin/components/slide/slide-manager.jsx
@@ -4,6 +4,7 @@ import { ulid } from "ulid";
import { useDispatch } from "react-redux";
import dayjs from "dayjs";
import { useNavigate } from "react-router-dom";
+import rebuildMediaFromContent from "./slide-media-utils";
import UserContext from "../../context/user-context";
import {
enhancedApi,
@@ -337,13 +338,11 @@ function SlideManager({
const localFormStateObject = { ...formStateObject };
const localMediaData = { ...mediaData };
// Set field as a field to look into for new references.
- setMediaFields([...new Set([...mediaFields, fieldId])]);
+ const updatedMediaFields = [...new Set([...mediaFields, fieldId])];
+ setMediaFields(updatedMediaFields);
const newField = [];
- if (Array.isArray(fieldValue) && fieldValue.length === 0) {
- localFormStateObject.media = [];
- }
// Handle each entry in field.
if (Array.isArray(fieldValue)) {
fieldValue.forEach((entry) => {
@@ -383,17 +382,19 @@ function SlideManager({
!Object.prototype.hasOwnProperty.call(localMediaData, entry["@id"])
) {
set(localMediaData, entry["@id"], entry);
-
- localFormStateObject.media.push(entry["@id"]);
}
}
});
}
set(localFormStateObject.content, fieldId, newField);
- set(localFormStateObject, "media", [
- ...new Set([...localFormStateObject.media]),
- ]);
+
+ // Rebuild the media array from all content fields to keep it in sync.
+ set(
+ localFormStateObject,
+ "media",
+ rebuildMediaFromContent(localFormStateObject.content),
+ );
setFormStateObject(localFormStateObject);
setMediaData(localMediaData);
diff --git a/assets/admin/components/slide/slide-media-utils.js b/assets/admin/components/slide/slide-media-utils.js
new file mode 100644
index 000000000..8c78bfb41
--- /dev/null
+++ b/assets/admin/components/slide/slide-media-utils.js
@@ -0,0 +1,61 @@
+/**
+ * Rebuild the media array from all content fields that reference media.
+ *
+ * This ensures that the top-level `media` array (sent to the API as slide_media
+ * associations) always matches the media actually referenced in the slide's
+ * `content` object.
+ *
+ * @param {object} content - The slide content object.
+ * @returns {string[]} Deduplicated array of media IRIs.
+ */
+export default function rebuildMediaFromContent(content) {
+ const media = [];
+
+ const mediaIriRegex = /\/v2\/media\/.+/;
+
+ const isMediaIri = (value) =>
+ typeof value === "string" &&
+ !value.startsWith("TEMP--") &&
+ mediaIriRegex.test(value);
+
+ const collectMediaFromValue = (value, seen = new Set()) => {
+ // 1) Ignore empty values early (nothing to scan)
+ if (value === null || value === undefined) return;
+
+ // 2) If it's a string, it might be a media IRI; validate and collect it
+ if (typeof value === "string") {
+ if (isMediaIri(value)) media.push(value);
+ return;
+ }
+
+ // 3) If it's not an object (e.g. number/boolean/function), it cannot contain nested media
+ if (typeof value !== "object") return;
+
+ // 4) Defensive guard against circular references:
+ // - JSON content won't have cycles, but runtime objects might.
+ // - If we've seen this object/array already, stop to avoid infinite recursion.
+ if (seen.has(value)) return;
+ seen.add(value);
+
+ // 5) If it's an array, scan each element (elements can be strings, objects, or more arrays)
+ if (Array.isArray(value)) {
+ value.forEach((item) => collectMediaFromValue(item, seen));
+ return;
+ }
+
+ // 6) Otherwise it's a plain object: scan its property values recursively
+ Object.values(value).forEach((item) => collectMediaFromValue(item, seen));
+ };
+
+ const fieldsToScan = new Set([]);
+
+ // Scan content for media references.
+ if (content && typeof content === "object") {
+ Object.keys(content).forEach((key) => fieldsToScan.add(key));
+ }
+
+ // Scan the entire content object (one traversal)
+ collectMediaFromValue(content);
+
+ return [...new Set(media)];
+}
diff --git a/assets/shared/templates/image-text.jsx b/assets/shared/templates/image-text.jsx
index de32bd6d6..6f6b33baa 100644
--- a/assets/shared/templates/image-text.jsx
+++ b/assets/shared/templates/image-text.jsx
@@ -42,6 +42,7 @@ function renderSlide(slide, run, slideDone) {
*/
function ImageText({ slide, content, run, slideDone, executionId }) {
const imageTimeoutRef = useRef();
+ const imagesRef = useRef([]);
const [images, setImages] = useState([]);
const [currentImage, setCurrentImage] = useState();
const logo = slide?.theme?.logo;
@@ -132,13 +133,15 @@ function ImageText({ slide, content, run, slideDone, executionId }) {
}
const changeImage = (newIndex) => {
- if (newIndex < images.length) {
- setCurrentImage(images[newIndex]);
+ const currentImages = imagesRef.current;
- if (newIndex < images.length - 1) {
+ if (newIndex < currentImages.length) {
+ setCurrentImage(currentImages[newIndex]);
+
+ if (newIndex < currentImages.length - 1) {
imageTimeoutRef.current = setTimeout(
() => changeImage(newIndex + 1),
- duration / images.length,
+ duration / currentImages.length,
);
}
}
@@ -152,33 +155,41 @@ function ImageText({ slide, content, run, slideDone, executionId }) {
);
if (imageUrls?.length > 0) {
- const newImages = imageUrls.map((url) => {
- return {
- url,
- nodeRef: createRef(),
- };
- });
+ const newImages = imageUrls.map((url) => ({
+ url,
+ nodeRef: createRef(),
+ }));
+
+ imagesRef.current = newImages;
setImages(newImages);
+ } else {
+ imagesRef.current = [];
+ setImages([]);
}
}
}, [slide]);
const startTheShow = () => {
- if (images?.length > 0 && !currentImage) {
- setCurrentImage(images[0]);
+ if (imageTimeoutRef.current) {
+ clearTimeout(imageTimeoutRef.current);
}
- // If there are multiple images, we are going to loop through these WITHIN the set duration.
- if (images?.length > 1) {
+ const currentImages = imagesRef.current;
+
+ if (currentImages.length > 1) {
// Kickoff the display of multiple images with the zero indexed
changeImage(0);
+ } else if (currentImages.length === 1) {
+ setCurrentImage(currentImages[0]);
}
};
useEffect(() => {
- if (!currentImage) {
+ if (images.length > 0) {
startTheShow();
+ } else {
+ setCurrentImage(undefined);
}
}, [images]);
diff --git a/assets/tests/admin/admin-slide-media-sync.spec.js b/assets/tests/admin/admin-slide-media-sync.spec.js
new file mode 100644
index 000000000..6f8164888
--- /dev/null
+++ b/assets/tests/admin/admin-slide-media-sync.spec.js
@@ -0,0 +1,116 @@
+import { test, expect } from "@playwright/test";
+import rebuildMediaFromContent from "../../admin/components/slide/slide-media-utils";
+
+test.describe("Slide media sync", () => {
+ test("It returns media IRIs referenced in content fields", () => {
+ const content = {
+ mainImage: ["/v2/media/1", "/v2/media/2"],
+ backgroundVideo: ["/v2/media/3"],
+ };
+
+ const result = rebuildMediaFromContent(content);
+
+ expect(result).toEqual(["/v2/media/1", "/v2/media/2", "/v2/media/3"]);
+ });
+
+ test("It excludes TEMP-- IDs that have not been uploaded yet", () => {
+ const content = {
+ mainImage: ["TEMP--abc123", "/v2/media/1"],
+ };
+
+ const result = rebuildMediaFromContent(content);
+
+ expect(result).toEqual(["/v2/media/1"]);
+ });
+
+ test("It removes media no longer referenced in any content field", () => {
+ const content = {
+ mainImage: ["/v2/media/2"],
+ };
+
+ const result = rebuildMediaFromContent(content);
+
+ expect(result).toEqual(["/v2/media/2"]);
+ expect(result).not.toContain("/v2/media/1");
+ });
+
+ test("It returns empty array when all media is removed from content", () => {
+ const content = {
+ mainImage: [],
+ backgroundVideo: ["/v2/media/3"],
+ };
+
+ const result = rebuildMediaFromContent(content);
+
+ expect(result).toEqual(["/v2/media/3"]);
+ });
+
+ test("It deduplicates media used across multiple content fields", () => {
+ const content = {
+ mainImage: ["/v2/media/1"],
+ thumbnail: ["/v2/media/1"],
+ };
+
+ const result = rebuildMediaFromContent(content);
+
+ expect(result).toEqual(["/v2/media/1"]);
+ });
+
+ test("It handles non-existent content fields gracefully", () => {
+ const content = {};
+
+ const result = rebuildMediaFromContent(content);
+
+ expect(result).toEqual([]);
+ });
+
+ test("It handles nested content field paths", () => {
+ const content = {
+ sections: {
+ hero: ["/v2/media/1"],
+ },
+ };
+
+ const result = rebuildMediaFromContent(content);
+
+ expect(result).toEqual(["/v2/media/1"]);
+ });
+
+ test("It ignores non-media content values when scanning top-level keys", () => {
+ const content = {
+ images: ["/v2/media/1"],
+ title: "Some text",
+ separator: true,
+ contacts: [{ name: "John", image: ["/v2/media/2"], tags: ["news"] }],
+ };
+
+ const result = rebuildMediaFromContent(content);
+
+ expect(result).toContain("/v2/media/1");
+ expect(result).toContain("/v2/media/2");
+ expect(result).not.toContain("news");
+ });
+
+ test("It does not include non-media string arrays from content", () => {
+ const content = {
+ images: ["/v2/media/1"],
+ tags: ["news", "sports"],
+ };
+
+ const result = rebuildMediaFromContent(content);
+
+ expect(result).toEqual(["/v2/media/1"]);
+ expect(result).not.toContain("news");
+ expect(result).not.toContain("sports");
+ });
+
+ test("It avoids infinite recursion when content contains circular references", () => {
+ const circular = { images: ["/v2/media/1"] };
+ circular.self = circular; // create an explicit cycle
+
+ expect(() => rebuildMediaFromContent(circular)).not.toThrow();
+
+ const result = rebuildMediaFromContent(circular);
+ expect(result).toContain("/v2/media/1");
+ });
+});
diff --git a/composer.lock b/composer.lock
index d6fd19db3..846da321c 100644
--- a/composer.lock
+++ b/composer.lock
@@ -755,16 +755,16 @@
},
{
"name": "doctrine/collections",
- "version": "2.4.0",
+ "version": "2.6.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/collections.git",
- "reference": "9acfeea2e8666536edff3d77c531261c63680160"
+ "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/collections/zipball/9acfeea2e8666536edff3d77c531261c63680160",
- "reference": "9acfeea2e8666536edff3d77c531261c63680160",
+ "url": "https://api.github.com/repos/doctrine/collections/zipball/7713da39d8e237f28411d6a616a3dce5e20d5de2",
+ "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2",
"shasum": ""
},
"require": {
@@ -821,7 +821,7 @@
],
"support": {
"issues": "https://github.com/doctrine/collections/issues",
- "source": "https://github.com/doctrine/collections/tree/2.4.0"
+ "source": "https://github.com/doctrine/collections/tree/2.6.0"
},
"funding": [
{
@@ -837,7 +837,7 @@
"type": "tidelift"
}
],
- "time": "2025-10-25T09:18:13+00:00"
+ "time": "2026-01-15T10:01:58+00:00"
},
{
"name": "doctrine/common",
@@ -932,16 +932,16 @@
},
{
"name": "doctrine/dbal",
- "version": "3.10.4",
+ "version": "3.10.5",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
- "reference": "63a46cb5aa6f60991186cc98c1d1b50c09311868"
+ "reference": "95d84866bf3c04b2ddca1df7c049714660959aef"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/dbal/zipball/63a46cb5aa6f60991186cc98c1d1b50c09311868",
- "reference": "63a46cb5aa6f60991186cc98c1d1b50c09311868",
+ "url": "https://api.github.com/repos/doctrine/dbal/zipball/95d84866bf3c04b2ddca1df7c049714660959aef",
+ "reference": "95d84866bf3c04b2ddca1df7c049714660959aef",
"shasum": ""
},
"require": {
@@ -962,9 +962,9 @@
"jetbrains/phpstorm-stubs": "2023.1",
"phpstan/phpstan": "2.1.30",
"phpstan/phpstan-strict-rules": "^2",
- "phpunit/phpunit": "9.6.29",
- "slevomat/coding-standard": "8.24.0",
- "squizlabs/php_codesniffer": "4.0.0",
+ "phpunit/phpunit": "9.6.34",
+ "slevomat/coding-standard": "8.27.1",
+ "squizlabs/php_codesniffer": "4.0.1",
"symfony/cache": "^5.4|^6.0|^7.0|^8.0",
"symfony/console": "^4.4|^5.4|^6.0|^7.0|^8.0"
},
@@ -1026,7 +1026,7 @@
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
- "source": "https://github.com/doctrine/dbal/tree/3.10.4"
+ "source": "https://github.com/doctrine/dbal/tree/3.10.5"
},
"funding": [
{
@@ -1042,33 +1042,33 @@
"type": "tidelift"
}
],
- "time": "2025-11-29T10:46:08+00:00"
+ "time": "2026-02-24T08:03:57+00:00"
},
{
"name": "doctrine/deprecations",
- "version": "1.1.5",
+ "version": "1.1.6",
"source": {
"type": "git",
"url": "https://github.com/doctrine/deprecations.git",
- "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38"
+ "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
- "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
+ "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
+ "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
- "phpunit/phpunit": "<=7.5 || >=13"
+ "phpunit/phpunit": "<=7.5 || >=14"
},
"require-dev": {
- "doctrine/coding-standard": "^9 || ^12 || ^13",
- "phpstan/phpstan": "1.4.10 || 2.1.11",
+ "doctrine/coding-standard": "^9 || ^12 || ^14",
+ "phpstan/phpstan": "1.4.10 || 2.1.30",
"phpstan/phpstan-phpunit": "^1.0 || ^2",
- "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
"psr/log": "^1 || ^2 || ^3"
},
"suggest": {
@@ -1088,9 +1088,9 @@
"homepage": "https://www.doctrine-project.org/",
"support": {
"issues": "https://github.com/doctrine/deprecations/issues",
- "source": "https://github.com/doctrine/deprecations/tree/1.1.5"
+ "source": "https://github.com/doctrine/deprecations/tree/1.1.6"
},
- "time": "2025-04-07T20:06:18+00:00"
+ "time": "2026-02-07T07:09:04+00:00"
},
{
"name": "doctrine/doctrine-bundle",
@@ -1300,16 +1300,16 @@
},
{
"name": "doctrine/event-manager",
- "version": "2.0.1",
+ "version": "2.1.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/event-manager.git",
- "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e"
+ "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e",
- "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e",
+ "url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf",
+ "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf",
"shasum": ""
},
"require": {
@@ -1319,10 +1319,10 @@
"doctrine/common": "<2.9"
},
"require-dev": {
- "doctrine/coding-standard": "^12",
- "phpstan/phpstan": "^1.8.8",
- "phpunit/phpunit": "^10.5",
- "vimeo/psalm": "^5.24"
+ "doctrine/coding-standard": "^14",
+ "phpdocumentor/guides-cli": "^1.4",
+ "phpstan/phpstan": "^2.1.32",
+ "phpunit/phpunit": "^10.5.58"
},
"type": "library",
"autoload": {
@@ -1371,7 +1371,7 @@
],
"support": {
"issues": "https://github.com/doctrine/event-manager/issues",
- "source": "https://github.com/doctrine/event-manager/tree/2.0.1"
+ "source": "https://github.com/doctrine/event-manager/tree/2.1.1"
},
"funding": [
{
@@ -1387,7 +1387,7 @@
"type": "tidelift"
}
],
- "time": "2024-05-22T20:47:39+00:00"
+ "time": "2026-01-29T07:11:08+00:00"
},
{
"name": "doctrine/inflector",
@@ -1628,16 +1628,16 @@
},
{
"name": "doctrine/migrations",
- "version": "3.9.5",
+ "version": "3.9.6",
"source": {
"type": "git",
"url": "https://github.com/doctrine/migrations.git",
- "reference": "1b823afbc40f932dae8272574faee53f2755eac5"
+ "reference": "ffd8355cdd8505fc650d9604f058bf62aedd80a1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/migrations/zipball/1b823afbc40f932dae8272574faee53f2755eac5",
- "reference": "1b823afbc40f932dae8272574faee53f2755eac5",
+ "url": "https://api.github.com/repos/doctrine/migrations/zipball/ffd8355cdd8505fc650d9604f058bf62aedd80a1",
+ "reference": "ffd8355cdd8505fc650d9604f058bf62aedd80a1",
"shasum": ""
},
"require": {
@@ -1711,7 +1711,7 @@
],
"support": {
"issues": "https://github.com/doctrine/migrations/issues",
- "source": "https://github.com/doctrine/migrations/tree/3.9.5"
+ "source": "https://github.com/doctrine/migrations/tree/3.9.6"
},
"funding": [
{
@@ -1727,7 +1727,7 @@
"type": "tidelift"
}
],
- "time": "2025-11-20T11:15:36+00:00"
+ "time": "2026-02-11T06:46:11+00:00"
},
{
"name": "doctrine/orm",
@@ -1928,16 +1928,16 @@
},
{
"name": "doctrine/sql-formatter",
- "version": "1.5.3",
+ "version": "1.5.4",
"source": {
"type": "git",
"url": "https://github.com/doctrine/sql-formatter.git",
- "reference": "a8af23a8e9d622505baa2997465782cbe8bb7fc7"
+ "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/a8af23a8e9d622505baa2997465782cbe8bb7fc7",
- "reference": "a8af23a8e9d622505baa2997465782cbe8bb7fc7",
+ "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/9563949f5cd3bd12a17d12fb980528bc141c5806",
+ "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806",
"shasum": ""
},
"require": {
@@ -1977,9 +1977,9 @@
],
"support": {
"issues": "https://github.com/doctrine/sql-formatter/issues",
- "source": "https://github.com/doctrine/sql-formatter/tree/1.5.3"
+ "source": "https://github.com/doctrine/sql-formatter/tree/1.5.4"
},
- "time": "2025-10-26T09:35:14+00:00"
+ "time": "2026-02-08T16:21:46+00:00"
},
{
"name": "evenement/evenement",
@@ -2154,16 +2154,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
- "version": "v3.92.4",
+ "version": "v3.94.2",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
- "reference": "9e7488b19403423e02e8403cc1eb596baf4673b0"
+ "reference": "7787ceff91365ba7d623ec410b8f429cdebb4f63"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/9e7488b19403423e02e8403cc1eb596baf4673b0",
- "reference": "9e7488b19403423e02e8403cc1eb596baf4673b0",
+ "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7787ceff91365ba7d623ec410b8f429cdebb4f63",
+ "reference": "7787ceff91365ba7d623ec410b8f429cdebb4f63",
"shasum": ""
},
"require": {
@@ -2180,7 +2180,7 @@
"react/event-loop": "^1.5",
"react/socket": "^1.16",
"react/stream": "^1.4",
- "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0",
+ "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0",
"symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0",
"symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0",
"symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0",
@@ -2194,18 +2194,18 @@
"symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0"
},
"require-dev": {
- "facile-it/paraunit": "^1.3.1 || ^2.7",
- "infection/infection": "^0.31",
- "justinrainbow/json-schema": "^6.6",
+ "facile-it/paraunit": "^1.3.1 || ^2.7.1",
+ "infection/infection": "^0.32.3",
+ "justinrainbow/json-schema": "^6.6.4",
"keradus/cli-executor": "^2.3",
"mikey179/vfsstream": "^1.6.12",
- "php-coveralls/php-coveralls": "^2.9",
- "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6",
- "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6",
- "phpunit/phpunit": "^9.6.31 || ^10.5.60 || ^11.5.46",
+ "php-coveralls/php-coveralls": "^2.9.1",
+ "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.7",
+ "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.7",
+ "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.51",
"symfony/polyfill-php85": "^1.33",
- "symfony/var-dumper": "^5.4.48 || ^6.4.26 || ^7.4.0 || ^8.0",
- "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0"
+ "symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.4",
+ "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.1"
},
"suggest": {
"ext-dom": "For handling output formats in XML",
@@ -2246,7 +2246,7 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
- "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.92.4"
+ "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.94.2"
},
"funding": [
{
@@ -2254,7 +2254,7 @@
"type": "github"
}
],
- "time": "2026-01-04T00:38:52+00:00"
+ "time": "2026-02-20T16:13:53+00:00"
},
{
"name": "friendsofphp/proxy-manager-lts",
@@ -2745,16 +2745,16 @@
},
{
"name": "imagine/imagine",
- "version": "1.5.1",
+ "version": "1.5.2",
"source": {
"type": "git",
"url": "https://github.com/php-imagine/Imagine.git",
- "reference": "8b130cd281efdea67e52d5f0f998572eb62d2f04"
+ "reference": "f9ed796eefb77c2f0f2167e1d4e36bc2b5ed6b0c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-imagine/Imagine/zipball/8b130cd281efdea67e52d5f0f998572eb62d2f04",
- "reference": "8b130cd281efdea67e52d5f0f998572eb62d2f04",
+ "url": "https://api.github.com/repos/php-imagine/Imagine/zipball/f9ed796eefb77c2f0f2167e1d4e36bc2b5ed6b0c",
+ "reference": "f9ed796eefb77c2f0f2167e1d4e36bc2b5ed6b0c",
"shasum": ""
},
"require": {
@@ -2801,9 +2801,9 @@
],
"support": {
"issues": "https://github.com/php-imagine/Imagine/issues",
- "source": "https://github.com/php-imagine/Imagine/tree/1.5.1"
+ "source": "https://github.com/php-imagine/Imagine/tree/1.5.2"
},
- "time": "2025-12-09T15:27:47+00:00"
+ "time": "2026-01-09T10:45:12+00:00"
},
{
"name": "itk-dev/openid-connect",
@@ -2997,16 +2997,16 @@
},
{
"name": "justinrainbow/json-schema",
- "version": "5.3.1",
+ "version": "5.3.2",
"source": {
"type": "git",
"url": "https://github.com/jsonrainbow/json-schema.git",
- "reference": "b5a44b6391a3bbb75c9f2b73e1ef03d6045e1e20"
+ "reference": "2f7abf648939847a789c55c206d4cb9dd0d53e2c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/b5a44b6391a3bbb75c9f2b73e1ef03d6045e1e20",
- "reference": "b5a44b6391a3bbb75c9f2b73e1ef03d6045e1e20",
+ "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/2f7abf648939847a789c55c206d4cb9dd0d53e2c",
+ "reference": "2f7abf648939847a789c55c206d4cb9dd0d53e2c",
"shasum": ""
},
"require": {
@@ -3056,22 +3056,22 @@
],
"support": {
"issues": "https://github.com/jsonrainbow/json-schema/issues",
- "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.1"
+ "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.2"
},
- "time": "2025-12-12T08:56:22+00:00"
+ "time": "2026-02-27T12:33:19+00:00"
},
{
"name": "kubawerlos/php-cs-fixer-custom-fixers",
- "version": "v3.35.1",
+ "version": "v3.36.0",
"source": {
"type": "git",
"url": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers.git",
- "reference": "2a35f80ae24ca77443a7af1599c3a3db1b6bd395"
+ "reference": "e1f97f6463f0b2a22e0dd320948a04132ff9c501"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/kubawerlos/php-cs-fixer-custom-fixers/zipball/2a35f80ae24ca77443a7af1599c3a3db1b6bd395",
- "reference": "2a35f80ae24ca77443a7af1599c3a3db1b6bd395",
+ "url": "https://api.github.com/repos/kubawerlos/php-cs-fixer-custom-fixers/zipball/e1f97f6463f0b2a22e0dd320948a04132ff9c501",
+ "reference": "e1f97f6463f0b2a22e0dd320948a04132ff9c501",
"shasum": ""
},
"require": {
@@ -3081,7 +3081,7 @@
"php": "^7.4 || ^8.0"
},
"require-dev": {
- "phpunit/phpunit": "^9.6.24 || ^10.5.51 || ^11.5.32"
+ "phpunit/phpunit": "^9.6.24 || ^10.5.51 || ^11.5.44"
},
"type": "library",
"autoload": {
@@ -3102,7 +3102,7 @@
"description": "A set of custom fixers for PHP CS Fixer",
"support": {
"issues": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/issues",
- "source": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/tree/v3.35.1"
+ "source": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/tree/v3.36.0"
},
"funding": [
{
@@ -3110,7 +3110,7 @@
"type": "github"
}
],
- "time": "2025-09-28T18:43:35+00:00"
+ "time": "2026-01-31T07:02:11+00:00"
},
{
"name": "laminas/laminas-code",
@@ -3498,22 +3498,22 @@
},
{
"name": "liip/imagine-bundle",
- "version": "2.16.0",
+ "version": "2.17.1",
"source": {
"type": "git",
"url": "https://github.com/liip/LiipImagineBundle.git",
- "reference": "335121ef65d9841af9b40a850aa143cd6b61f847"
+ "reference": "69d2df3c6606495d1878fa190d6c3dc4bc5623b6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/liip/LiipImagineBundle/zipball/335121ef65d9841af9b40a850aa143cd6b61f847",
- "reference": "335121ef65d9841af9b40a850aa143cd6b61f847",
+ "url": "https://api.github.com/repos/liip/LiipImagineBundle/zipball/69d2df3c6606495d1878fa190d6c3dc4bc5623b6",
+ "reference": "69d2df3c6606495d1878fa190d6c3dc4bc5623b6",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"imagine/imagine": "^1.3.2",
- "php": "^7.2|^8.0",
+ "php": "^8.0",
"symfony/dependency-injection": "^5.4|^6.4|^7.4|^8.0",
"symfony/deprecation-contracts": "^2.5 || ^3",
"symfony/filesystem": "^5.4|^6.4|^7.3|^8.0",
@@ -3542,6 +3542,7 @@
"symfony/form": "^5.4|^6.4|^7.3|^8.0",
"symfony/messenger": "^5.4|^6.4|^7.3|^8.0",
"symfony/phpunit-bridge": "^7.3",
+ "symfony/runtime": "^5.4|^6.4|^7.3|^8.0",
"symfony/templating": "^5.4|^6.4|^7.3|^8.0",
"symfony/validator": "^5.4|^6.4|^7.3|^8.0",
"symfony/yaml": "^5.4|^6.4|^7.3|^8.0"
@@ -3599,9 +3600,9 @@
],
"support": {
"issues": "https://github.com/liip/LiipImagineBundle/issues",
- "source": "https://github.com/liip/LiipImagineBundle/tree/2.16.0"
+ "source": "https://github.com/liip/LiipImagineBundle/tree/2.17.1"
},
- "time": "2025-12-01T10:49:05+00:00"
+ "time": "2026-01-06T09:34:48+00:00"
},
{
"name": "masterminds/html5",
@@ -3842,16 +3843,16 @@
},
{
"name": "nelmio/cors-bundle",
- "version": "2.6.0",
+ "version": "2.6.1",
"source": {
"type": "git",
"url": "https://github.com/nelmio/NelmioCorsBundle.git",
- "reference": "530217472204881cacd3671909f634b960c7b948"
+ "reference": "3d80dbcd5d1eb5f8b20ed5199e1778d44c2e4d1c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/530217472204881cacd3671909f634b960c7b948",
- "reference": "530217472204881cacd3671909f634b960c7b948",
+ "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/3d80dbcd5d1eb5f8b20ed5199e1778d44c2e4d1c",
+ "reference": "3d80dbcd5d1eb5f8b20ed5199e1778d44c2e4d1c",
"shasum": ""
},
"require": {
@@ -3901,9 +3902,9 @@
],
"support": {
"issues": "https://github.com/nelmio/NelmioCorsBundle/issues",
- "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.6.0"
+ "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.6.1"
},
- "time": "2025-10-23T06:57:22+00:00"
+ "time": "2026-01-12T15:59:08+00:00"
},
{
"name": "nyholm/psr7",
@@ -5649,16 +5650,16 @@
},
{
"name": "symfony/asset",
- "version": "v6.4.24",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/asset.git",
- "reference": "cfee7c0d64be113383db74a2fdd65d426b7f3aab"
+ "reference": "1bd59aa278691b6310ca56b996cf6e2619a6a347"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/asset/zipball/cfee7c0d64be113383db74a2fdd65d426b7f3aab",
- "reference": "cfee7c0d64be113383db74a2fdd65d426b7f3aab",
+ "url": "https://api.github.com/repos/symfony/asset/zipball/1bd59aa278691b6310ca56b996cf6e2619a6a347",
+ "reference": "1bd59aa278691b6310ca56b996cf6e2619a6a347",
"shasum": ""
},
"require": {
@@ -5698,7 +5699,7 @@
"description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/asset/tree/v6.4.24"
+ "source": "https://github.com/symfony/asset/tree/v6.4.34"
},
"funding": [
{
@@ -5718,20 +5719,20 @@
"type": "tidelift"
}
],
- "time": "2025-07-10T08:14:14+00:00"
+ "time": "2026-02-07T09:15:39+00:00"
},
{
"name": "symfony/cache",
- "version": "v6.4.31",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache.git",
- "reference": "a1b306757c34b96fe97c0c586f50dceed05c7adb"
+ "reference": "a0a1690543329685c044362c873b78c6de9d4faa"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/cache/zipball/a1b306757c34b96fe97c0c586f50dceed05c7adb",
- "reference": "a1b306757c34b96fe97c0c586f50dceed05c7adb",
+ "url": "https://api.github.com/repos/symfony/cache/zipball/a0a1690543329685c044362c873b78c6de9d4faa",
+ "reference": "a0a1690543329685c044362c873b78c6de9d4faa",
"shasum": ""
},
"require": {
@@ -5798,7 +5799,7 @@
"psr6"
],
"support": {
- "source": "https://github.com/symfony/cache/tree/v6.4.31"
+ "source": "https://github.com/symfony/cache/tree/v6.4.34"
},
"funding": [
{
@@ -5818,7 +5819,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-27T18:26:25+00:00"
+ "time": "2026-02-20T15:06:30+00:00"
},
{
"name": "symfony/cache-contracts",
@@ -5976,16 +5977,16 @@
},
{
"name": "symfony/config",
- "version": "v6.4.28",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
- "reference": "15947c18ef3ddb0b2f4ec936b9e90e2520979f62"
+ "reference": "ce9cb0c0d281aaf188b802d4968e42bfb60701e9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/config/zipball/15947c18ef3ddb0b2f4ec936b9e90e2520979f62",
- "reference": "15947c18ef3ddb0b2f4ec936b9e90e2520979f62",
+ "url": "https://api.github.com/repos/symfony/config/zipball/ce9cb0c0d281aaf188b802d4968e42bfb60701e9",
+ "reference": "ce9cb0c0d281aaf188b802d4968e42bfb60701e9",
"shasum": ""
},
"require": {
@@ -6031,7 +6032,7 @@
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/config/tree/v6.4.28"
+ "source": "https://github.com/symfony/config/tree/v6.4.34"
},
"funding": [
{
@@ -6051,20 +6052,20 @@
"type": "tidelift"
}
],
- "time": "2025-11-01T19:52:02+00:00"
+ "time": "2026-02-24T17:34:50+00:00"
},
{
"name": "symfony/console",
- "version": "v6.4.31",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "f9f8a889f54c264f9abac3fc0f7a371ffca51997"
+ "reference": "7b1f1c37eff5910ddda2831345467e593a5120ad"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/f9f8a889f54c264f9abac3fc0f7a371ffca51997",
- "reference": "f9f8a889f54c264f9abac3fc0f7a371ffca51997",
+ "url": "https://api.github.com/repos/symfony/console/zipball/7b1f1c37eff5910ddda2831345467e593a5120ad",
+ "reference": "7b1f1c37eff5910ddda2831345467e593a5120ad",
"shasum": ""
},
"require": {
@@ -6129,7 +6130,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v6.4.31"
+ "source": "https://github.com/symfony/console/tree/v6.4.34"
},
"funding": [
{
@@ -6149,20 +6150,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-22T08:30:34+00:00"
+ "time": "2026-02-23T15:42:15+00:00"
},
{
"name": "symfony/dependency-injection",
- "version": "v6.4.31",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/dependency-injection.git",
- "reference": "10058832a74a33648870aa2057e3fdc8796a6566"
+ "reference": "91e49958b8a6092e48e4711894a1aeb1b151c62a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/10058832a74a33648870aa2057e3fdc8796a6566",
- "reference": "10058832a74a33648870aa2057e3fdc8796a6566",
+ "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/91e49958b8a6092e48e4711894a1aeb1b151c62a",
+ "reference": "91e49958b8a6092e48e4711894a1aeb1b151c62a",
"shasum": ""
},
"require": {
@@ -6214,7 +6215,7 @@
"description": "Allows you to standardize and centralize the way objects are constructed in your application",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/dependency-injection/tree/v6.4.31"
+ "source": "https://github.com/symfony/dependency-injection/tree/v6.4.34"
},
"funding": [
{
@@ -6234,7 +6235,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-23T13:34:50+00:00"
+ "time": "2026-02-24T15:33:38+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -6305,16 +6306,16 @@
},
{
"name": "symfony/doctrine-bridge",
- "version": "v6.4.26",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/doctrine-bridge.git",
- "reference": "c14bb5a9125c411e73354954940e06b6e7fcc344"
+ "reference": "9e82991eda36e85b640644e1d8d34d89eff498a6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/c14bb5a9125c411e73354954940e06b6e7fcc344",
- "reference": "c14bb5a9125c411e73354954940e06b6e7fcc344",
+ "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/9e82991eda36e85b640644e1d8d34d89eff498a6",
+ "reference": "9e82991eda36e85b640644e1d8d34d89eff498a6",
"shasum": ""
},
"require": {
@@ -6393,7 +6394,7 @@
"description": "Provides integration for Doctrine with various Symfony components",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.26"
+ "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.34"
},
"funding": [
{
@@ -6413,20 +6414,20 @@
"type": "tidelift"
}
],
- "time": "2025-09-26T15:07:38+00:00"
+ "time": "2026-02-06T08:53:22+00:00"
},
{
"name": "symfony/dom-crawler",
- "version": "v6.4.25",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
- "reference": "976302990f9f2a6d4c07206836dd4ca77cae9524"
+ "reference": "ec0d22e1b89d5767a44f7abb63a1f1439bd9c735"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/976302990f9f2a6d4c07206836dd4ca77cae9524",
- "reference": "976302990f9f2a6d4c07206836dd4ca77cae9524",
+ "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/ec0d22e1b89d5767a44f7abb63a1f1439bd9c735",
+ "reference": "ec0d22e1b89d5767a44f7abb63a1f1439bd9c735",
"shasum": ""
},
"require": {
@@ -6464,7 +6465,7 @@
"description": "Eases DOM navigation for HTML and XML documents",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/dom-crawler/tree/v6.4.25"
+ "source": "https://github.com/symfony/dom-crawler/tree/v6.4.34"
},
"funding": [
{
@@ -6484,7 +6485,7 @@
"type": "tidelift"
}
],
- "time": "2025-08-05T18:56:08+00:00"
+ "time": "2026-02-16T20:44:03+00:00"
},
{
"name": "symfony/dotenv",
@@ -6566,16 +6567,16 @@
},
{
"name": "symfony/error-handler",
- "version": "v6.4.26",
+ "version": "v6.4.32",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
- "reference": "41bedcaec5b72640b0ec2096547b75fda72ead6c"
+ "reference": "8c18400784fcb014dc73c8d5601a9576af7f8ad4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/error-handler/zipball/41bedcaec5b72640b0ec2096547b75fda72ead6c",
- "reference": "41bedcaec5b72640b0ec2096547b75fda72ead6c",
+ "url": "https://api.github.com/repos/symfony/error-handler/zipball/8c18400784fcb014dc73c8d5601a9576af7f8ad4",
+ "reference": "8c18400784fcb014dc73c8d5601a9576af7f8ad4",
"shasum": ""
},
"require": {
@@ -6621,7 +6622,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/error-handler/tree/v6.4.26"
+ "source": "https://github.com/symfony/error-handler/tree/v6.4.32"
},
"funding": [
{
@@ -6641,20 +6642,20 @@
"type": "tidelift"
}
],
- "time": "2025-09-11T09:57:09+00:00"
+ "time": "2026-01-19T19:28:19+00:00"
},
{
"name": "symfony/event-dispatcher",
- "version": "v6.4.25",
+ "version": "v6.4.32",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "b0cf3162020603587363f0551cd3be43958611ff"
+ "reference": "99d7e101826e6610606b9433248f80c1997cd20b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b0cf3162020603587363f0551cd3be43958611ff",
- "reference": "b0cf3162020603587363f0551cd3be43958611ff",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99d7e101826e6610606b9433248f80c1997cd20b",
+ "reference": "99d7e101826e6610606b9433248f80c1997cd20b",
"shasum": ""
},
"require": {
@@ -6705,7 +6706,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.25"
+ "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.32"
},
"funding": [
{
@@ -6725,7 +6726,7 @@
"type": "tidelift"
}
],
- "time": "2025-08-13T09:41:44+00:00"
+ "time": "2026-01-05T11:13:48+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -6805,16 +6806,16 @@
},
{
"name": "symfony/expression-language",
- "version": "v6.4.30",
+ "version": "v6.4.32",
"source": {
"type": "git",
"url": "https://github.com/symfony/expression-language.git",
- "reference": "01906f3b379833b347de9abc8ddc326593e9122b"
+ "reference": "89c10ef5ca65968ec7ce7ce033c7f36eeb1b0312"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/expression-language/zipball/01906f3b379833b347de9abc8ddc326593e9122b",
- "reference": "01906f3b379833b347de9abc8ddc326593e9122b",
+ "url": "https://api.github.com/repos/symfony/expression-language/zipball/89c10ef5ca65968ec7ce7ce033c7f36eeb1b0312",
+ "reference": "89c10ef5ca65968ec7ce7ce033c7f36eeb1b0312",
"shasum": ""
},
"require": {
@@ -6849,7 +6850,7 @@
"description": "Provides an engine that can compile and evaluate expressions",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/expression-language/tree/v6.4.30"
+ "source": "https://github.com/symfony/expression-language/tree/v6.4.32"
},
"funding": [
{
@@ -6869,20 +6870,20 @@
"type": "tidelift"
}
],
- "time": "2025-11-09T10:02:06+00:00"
+ "time": "2026-01-04T11:52:13+00:00"
},
{
"name": "symfony/filesystem",
- "version": "v6.4.30",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "441c6b69f7222aadae7cbf5df588496d5ee37789"
+ "reference": "01ffe0411b842f93c571e5c391f289c3fdd498c3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/441c6b69f7222aadae7cbf5df588496d5ee37789",
- "reference": "441c6b69f7222aadae7cbf5df588496d5ee37789",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/01ffe0411b842f93c571e5c391f289c3fdd498c3",
+ "reference": "01ffe0411b842f93c571e5c391f289c3fdd498c3",
"shasum": ""
},
"require": {
@@ -6919,7 +6920,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v6.4.30"
+ "source": "https://github.com/symfony/filesystem/tree/v6.4.34"
},
"funding": [
{
@@ -6939,20 +6940,20 @@
"type": "tidelift"
}
],
- "time": "2025-11-26T14:43:45+00:00"
+ "time": "2026-02-24T17:51:06+00:00"
},
{
"name": "symfony/finder",
- "version": "v6.4.31",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "5547f2e1f0ca8e2e7abe490156b62da778cfbe2b"
+ "reference": "9590e86be1d1c57bfbb16d0dd040345378c20896"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/5547f2e1f0ca8e2e7abe490156b62da778cfbe2b",
- "reference": "5547f2e1f0ca8e2e7abe490156b62da778cfbe2b",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/9590e86be1d1c57bfbb16d0dd040345378c20896",
+ "reference": "9590e86be1d1c57bfbb16d0dd040345378c20896",
"shasum": ""
},
"require": {
@@ -6987,7 +6988,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v6.4.31"
+ "source": "https://github.com/symfony/finder/tree/v6.4.34"
},
"funding": [
{
@@ -7007,7 +7008,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-11T14:52:17+00:00"
+ "time": "2026-01-28T15:16:37+00:00"
},
{
"name": "symfony/flex",
@@ -7084,16 +7085,16 @@
},
{
"name": "symfony/framework-bundle",
- "version": "v6.4.31",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/framework-bundle.git",
- "reference": "0ab60c05570b9e2bfab92b9944b938b8ffb5ba96"
+ "reference": "5b5d19473f22d699811a41b01cef2462bc42b238"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/0ab60c05570b9e2bfab92b9944b938b8ffb5ba96",
- "reference": "0ab60c05570b9e2bfab92b9944b938b8ffb5ba96",
+ "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/5b5d19473f22d699811a41b01cef2462bc42b238",
+ "reference": "5b5d19473f22d699811a41b01cef2462bc42b238",
"shasum": ""
},
"require": {
@@ -7213,7 +7214,7 @@
"description": "Provides a tight integration between Symfony components and the Symfony full-stack framework",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/framework-bundle/tree/v6.4.31"
+ "source": "https://github.com/symfony/framework-bundle/tree/v6.4.34"
},
"funding": [
{
@@ -7233,20 +7234,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-23T14:16:13+00:00"
+ "time": "2026-02-24T16:00:52+00:00"
},
{
"name": "symfony/http-client",
- "version": "v6.4.31",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
- "reference": "f166fe476c996237666bcf7ec2cf827cd82ad573"
+ "reference": "0dc71f52e5d35bb045fd0f82b1a80c027971d551"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-client/zipball/f166fe476c996237666bcf7ec2cf827cd82ad573",
- "reference": "f166fe476c996237666bcf7ec2cf827cd82ad573",
+ "url": "https://api.github.com/repos/symfony/http-client/zipball/0dc71f52e5d35bb045fd0f82b1a80c027971d551",
+ "reference": "0dc71f52e5d35bb045fd0f82b1a80c027971d551",
"shasum": ""
},
"require": {
@@ -7311,7 +7312,7 @@
"http"
],
"support": {
- "source": "https://github.com/symfony/http-client/tree/v6.4.31"
+ "source": "https://github.com/symfony/http-client/tree/v6.4.34"
},
"funding": [
{
@@ -7331,7 +7332,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-23T14:19:38+00:00"
+ "time": "2026-02-18T07:27:25+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -7413,16 +7414,16 @@
},
{
"name": "symfony/http-foundation",
- "version": "v6.4.31",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "a35ee6f47e4775179704d7877a8b0da3cb09241a"
+ "reference": "5bb346d1b4b2a616e5c3d99b3ee4d5810735c535"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a35ee6f47e4775179704d7877a8b0da3cb09241a",
- "reference": "a35ee6f47e4775179704d7877a8b0da3cb09241a",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5bb346d1b4b2a616e5c3d99b3ee4d5810735c535",
+ "reference": "5bb346d1b4b2a616e5c3d99b3ee4d5810735c535",
"shasum": ""
},
"require": {
@@ -7470,7 +7471,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-foundation/tree/v6.4.31"
+ "source": "https://github.com/symfony/http-foundation/tree/v6.4.34"
},
"funding": [
{
@@ -7490,20 +7491,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-17T10:10:57+00:00"
+ "time": "2026-02-21T15:48:41+00:00"
},
{
"name": "symfony/http-kernel",
- "version": "v6.4.31",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "16b0d46d8e11f480345c15b229cfc827a8a0f731"
+ "reference": "006a49fc4f41ee21a6ca61e69caed1c30b29f07c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-kernel/zipball/16b0d46d8e11f480345c15b229cfc827a8a0f731",
- "reference": "16b0d46d8e11f480345c15b229cfc827a8a0f731",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/006a49fc4f41ee21a6ca61e69caed1c30b29f07c",
+ "reference": "006a49fc4f41ee21a6ca61e69caed1c30b29f07c",
"shasum": ""
},
"require": {
@@ -7544,7 +7545,7 @@
"symfony/config": "^6.1|^7.0",
"symfony/console": "^5.4|^6.0|^7.0",
"symfony/css-selector": "^5.4|^6.0|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
+ "symfony/dependency-injection": "^6.4.1|^7.0.1",
"symfony/dom-crawler": "^5.4|^6.0|^7.0",
"symfony/expression-language": "^5.4|^6.0|^7.0",
"symfony/finder": "^5.4|^6.0|^7.0",
@@ -7588,7 +7589,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-kernel/tree/v6.4.31"
+ "source": "https://github.com/symfony/http-kernel/tree/v6.4.34"
},
"funding": [
{
@@ -7608,20 +7609,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-31T08:27:27+00:00"
+ "time": "2026-02-26T08:27:11+00:00"
},
{
"name": "symfony/mime",
- "version": "v6.4.30",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "69aeef5d2692bb7c18ce133b09f67b27260b7acf"
+ "reference": "2b32fbbe10b36a8379efab6e702ad8b917151839"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/69aeef5d2692bb7c18ce133b09f67b27260b7acf",
- "reference": "69aeef5d2692bb7c18ce133b09f67b27260b7acf",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/2b32fbbe10b36a8379efab6e702ad8b917151839",
+ "reference": "2b32fbbe10b36a8379efab6e702ad8b917151839",
"shasum": ""
},
"require": {
@@ -7677,7 +7678,7 @@
"mime-type"
],
"support": {
- "source": "https://github.com/symfony/mime/tree/v6.4.30"
+ "source": "https://github.com/symfony/mime/tree/v6.4.34"
},
"funding": [
{
@@ -7697,20 +7698,20 @@
"type": "tidelift"
}
],
- "time": "2025-11-16T09:57:53+00:00"
+ "time": "2026-02-02T17:01:23+00:00"
},
{
"name": "symfony/monolog-bridge",
- "version": "v6.4.28",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/monolog-bridge.git",
- "reference": "d2f4b68e3247cf44d93f48545c8c072a75c17e5b"
+ "reference": "ee2d0150031b7c6ee2a1149fddddef3e7cdec117"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/d2f4b68e3247cf44d93f48545c8c072a75c17e5b",
- "reference": "d2f4b68e3247cf44d93f48545c8c072a75c17e5b",
+ "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/ee2d0150031b7c6ee2a1149fddddef3e7cdec117",
+ "reference": "ee2d0150031b7c6ee2a1149fddddef3e7cdec117",
"shasum": ""
},
"require": {
@@ -7760,7 +7761,7 @@
"description": "Provides integration for Monolog with various Symfony components",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/monolog-bridge/tree/v6.4.28"
+ "source": "https://github.com/symfony/monolog-bridge/tree/v6.4.34"
},
"funding": [
{
@@ -7780,7 +7781,7 @@
"type": "tidelift"
}
],
- "time": "2025-10-30T19:57:08+00:00"
+ "time": "2026-02-16T20:44:03+00:00"
},
{
"name": "symfony/monolog-bundle",
@@ -7935,16 +7936,16 @@
},
{
"name": "symfony/password-hasher",
- "version": "v6.4.24",
+ "version": "v6.4.32",
"source": {
"type": "git",
"url": "https://github.com/symfony/password-hasher.git",
- "reference": "dcab5ac87450aaed26483ba49c2ce86808da7557"
+ "reference": "fbdfa5a2ca218ec8bb9029517426df2d780bdba9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/password-hasher/zipball/dcab5ac87450aaed26483ba49c2ce86808da7557",
- "reference": "dcab5ac87450aaed26483ba49c2ce86808da7557",
+ "url": "https://api.github.com/repos/symfony/password-hasher/zipball/fbdfa5a2ca218ec8bb9029517426df2d780bdba9",
+ "reference": "fbdfa5a2ca218ec8bb9029517426df2d780bdba9",
"shasum": ""
},
"require": {
@@ -7987,7 +7988,7 @@
"password"
],
"support": {
- "source": "https://github.com/symfony/password-hasher/tree/v6.4.24"
+ "source": "https://github.com/symfony/password-hasher/tree/v6.4.32"
},
"funding": [
{
@@ -8007,7 +8008,7 @@
"type": "tidelift"
}
],
- "time": "2025-07-10T08:14:14+00:00"
+ "time": "2026-01-01T21:24:53+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
@@ -8825,16 +8826,16 @@
},
{
"name": "symfony/process",
- "version": "v6.4.31",
+ "version": "v6.4.33",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "8541b7308fca001320e90bca8a73a28aa5604a6e"
+ "reference": "c46e854e79b52d07666e43924a20cb6dc546644e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/8541b7308fca001320e90bca8a73a28aa5604a6e",
- "reference": "8541b7308fca001320e90bca8a73a28aa5604a6e",
+ "url": "https://api.github.com/repos/symfony/process/zipball/c46e854e79b52d07666e43924a20cb6dc546644e",
+ "reference": "c46e854e79b52d07666e43924a20cb6dc546644e",
"shasum": ""
},
"require": {
@@ -8866,7 +8867,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v6.4.31"
+ "source": "https://github.com/symfony/process/tree/v6.4.33"
},
"funding": [
{
@@ -8886,26 +8887,26 @@
"type": "tidelift"
}
],
- "time": "2025-12-15T19:26:35+00:00"
+ "time": "2026-01-23T16:02:12+00:00"
},
{
"name": "symfony/property-access",
- "version": "v6.4.31",
+ "version": "v6.4.32",
"source": {
"type": "git",
"url": "https://github.com/symfony/property-access.git",
- "reference": "1b1044599d7fb93cdb82f5a1291ba66f1caf6119"
+ "reference": "6dfa655ac9e9860c05cabb287f34da86b18c237e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/property-access/zipball/1b1044599d7fb93cdb82f5a1291ba66f1caf6119",
- "reference": "1b1044599d7fb93cdb82f5a1291ba66f1caf6119",
+ "url": "https://api.github.com/repos/symfony/property-access/zipball/6dfa655ac9e9860c05cabb287f34da86b18c237e",
+ "reference": "6dfa655ac9e9860c05cabb287f34da86b18c237e",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.5|^3",
- "symfony/property-info": "^6.4.31|~7.3.9|^7.4.2"
+ "symfony/property-info": "^6.4.32|~7.3.10|^7.4.4"
},
"require-dev": {
"symfony/cache": "^5.4|^6.0|^7.0"
@@ -8947,7 +8948,7 @@
"reflection"
],
"support": {
- "source": "https://github.com/symfony/property-access/tree/v6.4.31"
+ "source": "https://github.com/symfony/property-access/tree/v6.4.32"
},
"funding": [
{
@@ -8967,20 +8968,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-18T08:11:26+00:00"
+ "time": "2026-01-05T08:25:17+00:00"
},
{
"name": "symfony/property-info",
- "version": "v6.4.31",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/property-info.git",
- "reference": "f155cef234af1c16ed47791d182e146df237b35f"
+ "reference": "916455e4c9dcddbebfd101f29d7983841c3564e0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/property-info/zipball/f155cef234af1c16ed47791d182e146df237b35f",
- "reference": "f155cef234af1c16ed47791d182e146df237b35f",
+ "url": "https://api.github.com/repos/symfony/property-info/zipball/916455e4c9dcddbebfd101f29d7983841c3564e0",
+ "reference": "916455e4c9dcddbebfd101f29d7983841c3564e0",
"shasum": ""
},
"require": {
@@ -8989,7 +8990,7 @@
},
"conflict": {
"doctrine/annotations": "<1.12",
- "phpdocumentor/reflection-docblock": "<5.2",
+ "phpdocumentor/reflection-docblock": "<5.2|>=6",
"phpdocumentor/type-resolver": "<1.5.1",
"symfony/cache": "<5.4",
"symfony/dependency-injection": "<5.4|>=6.0,<6.4",
@@ -9037,7 +9038,7 @@
"validator"
],
"support": {
- "source": "https://github.com/symfony/property-info/tree/v6.4.31"
+ "source": "https://github.com/symfony/property-info/tree/v6.4.34"
},
"funding": [
{
@@ -9057,7 +9058,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-16T19:55:30+00:00"
+ "time": "2026-02-13T09:42:46+00:00"
},
{
"name": "symfony/proxy-manager-bridge",
@@ -9132,16 +9133,16 @@
},
{
"name": "symfony/routing",
- "version": "v6.4.30",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
- "reference": "ea50a13c2711eebcbb66b38ef6382e62e3262859"
+ "reference": "5ab3a3e1a03535ec5ca6ce2d39e4369a1096ae47"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/routing/zipball/ea50a13c2711eebcbb66b38ef6382e62e3262859",
- "reference": "ea50a13c2711eebcbb66b38ef6382e62e3262859",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/5ab3a3e1a03535ec5ca6ce2d39e4369a1096ae47",
+ "reference": "5ab3a3e1a03535ec5ca6ce2d39e4369a1096ae47",
"shasum": ""
},
"require": {
@@ -9195,7 +9196,7 @@
"url"
],
"support": {
- "source": "https://github.com/symfony/routing/tree/v6.4.30"
+ "source": "https://github.com/symfony/routing/tree/v6.4.34"
},
"funding": [
{
@@ -9215,7 +9216,7 @@
"type": "tidelift"
}
],
- "time": "2025-11-22T09:51:35+00:00"
+ "time": "2026-02-24T17:34:50+00:00"
},
{
"name": "symfony/runtime",
@@ -9302,16 +9303,16 @@
},
{
"name": "symfony/security-bundle",
- "version": "v6.4.30",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/security-bundle.git",
- "reference": "508e67e68156cf3cb2b982504191b2ce34daa921"
+ "reference": "f67bd24782a80095e9b8953e18d01983b9fe8e34"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/security-bundle/zipball/508e67e68156cf3cb2b982504191b2ce34daa921",
- "reference": "508e67e68156cf3cb2b982504191b2ce34daa921",
+ "url": "https://api.github.com/repos/symfony/security-bundle/zipball/f67bd24782a80095e9b8953e18d01983b9fe8e34",
+ "reference": "f67bd24782a80095e9b8953e18d01983b9fe8e34",
"shasum": ""
},
"require": {
@@ -9394,7 +9395,7 @@
"description": "Provides a tight integration of the Security component into the Symfony full-stack framework",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/security-bundle/tree/v6.4.30"
+ "source": "https://github.com/symfony/security-bundle/tree/v6.4.34"
},
"funding": [
{
@@ -9414,7 +9415,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-04T18:05:02+00:00"
+ "time": "2026-02-22T21:48:58+00:00"
},
{
"name": "symfony/security-core",
@@ -9580,16 +9581,16 @@
},
{
"name": "symfony/security-http",
- "version": "v6.4.31",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/security-http.git",
- "reference": "2f4ddc4a79b0e6f7dcc72e5ebd7109b3436b967a"
+ "reference": "894bb42b39bfb240a1f82c9a46a1ce9402a31a6f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/security-http/zipball/2f4ddc4a79b0e6f7dcc72e5ebd7109b3436b967a",
- "reference": "2f4ddc4a79b0e6f7dcc72e5ebd7109b3436b967a",
+ "url": "https://api.github.com/repos/symfony/security-http/zipball/894bb42b39bfb240a1f82c9a46a1ce9402a31a6f",
+ "reference": "894bb42b39bfb240a1f82c9a46a1ce9402a31a6f",
"shasum": ""
},
"require": {
@@ -9648,7 +9649,7 @@
"description": "Symfony Security Component - HTTP Integration",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/security-http/tree/v6.4.31"
+ "source": "https://github.com/symfony/security-http/tree/v6.4.34"
},
"funding": [
{
@@ -9668,20 +9669,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-17T22:32:13+00:00"
+ "time": "2026-02-16T20:44:03+00:00"
},
{
"name": "symfony/serializer",
- "version": "v6.4.31",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/serializer.git",
- "reference": "abf80f880943224afca831d7da6eff584c3af751"
+ "reference": "26446be5ec3d84c2aa16a08e195c783e3d4c2af7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/serializer/zipball/abf80f880943224afca831d7da6eff584c3af751",
- "reference": "abf80f880943224afca831d7da6eff584c3af751",
+ "url": "https://api.github.com/repos/symfony/serializer/zipball/26446be5ec3d84c2aa16a08e195c783e3d4c2af7",
+ "reference": "26446be5ec3d84c2aa16a08e195c783e3d4c2af7",
"shasum": ""
},
"require": {
@@ -9750,7 +9751,7 @@
"description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/serializer/tree/v6.4.31"
+ "source": "https://github.com/symfony/serializer/tree/v6.4.34"
},
"funding": [
{
@@ -9770,7 +9771,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-19T17:17:42+00:00"
+ "time": "2026-02-24T16:55:33+00:00"
},
{
"name": "symfony/service-contracts",
@@ -9927,16 +9928,16 @@
},
{
"name": "symfony/string",
- "version": "v6.4.30",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "50590a057841fa6bf69d12eceffce3465b9e32cb"
+ "reference": "2adaf4106f2ef4c67271971bde6d3fe0a6936432"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/50590a057841fa6bf69d12eceffce3465b9e32cb",
- "reference": "50590a057841fa6bf69d12eceffce3465b9e32cb",
+ "url": "https://api.github.com/repos/symfony/string/zipball/2adaf4106f2ef4c67271971bde6d3fe0a6936432",
+ "reference": "2adaf4106f2ef4c67271971bde6d3fe0a6936432",
"shasum": ""
},
"require": {
@@ -9992,7 +9993,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v6.4.30"
+ "source": "https://github.com/symfony/string/tree/v6.4.34"
},
"funding": [
{
@@ -10012,7 +10013,7 @@
"type": "tidelift"
}
],
- "time": "2025-11-21T18:03:05+00:00"
+ "time": "2026-02-08T20:44:54+00:00"
},
{
"name": "symfony/translation-contracts",
@@ -10098,16 +10099,16 @@
},
{
"name": "symfony/twig-bridge",
- "version": "v6.4.31",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/twig-bridge.git",
- "reference": "24a498d80fd2a28087fbac0a96e0721ce2756b65"
+ "reference": "5169074f4a88dfb02eeccddaba78edfdf212a9b2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/24a498d80fd2a28087fbac0a96e0721ce2756b65",
- "reference": "24a498d80fd2a28087fbac0a96e0721ce2756b65",
+ "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/5169074f4a88dfb02eeccddaba78edfdf212a9b2",
+ "reference": "5169074f4a88dfb02eeccddaba78edfdf212a9b2",
"shasum": ""
},
"require": {
@@ -10120,7 +10121,7 @@
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"symfony/console": "<5.4",
- "symfony/form": "<6.4",
+ "symfony/form": "<6.4.32|>7,<7.3.10|>7.4,<7.4.4",
"symfony/http-foundation": "<5.4",
"symfony/http-kernel": "<6.4",
"symfony/mime": "<6.2",
@@ -10138,7 +10139,7 @@
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
"symfony/expression-language": "^5.4|^6.0|^7.0",
"symfony/finder": "^5.4|^6.0|^7.0",
- "symfony/form": "^6.4.30|~7.3.8|^7.4.1",
+ "symfony/form": "^6.4.32|~7.3.10|^7.4.4",
"symfony/html-sanitizer": "^6.1|^7.0",
"symfony/http-foundation": "^5.4|^6.0|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
@@ -10187,7 +10188,7 @@
"description": "Provides integration for Twig with various Symfony components",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/twig-bridge/tree/v6.4.31"
+ "source": "https://github.com/symfony/twig-bridge/tree/v6.4.34"
},
"funding": [
{
@@ -10207,20 +10208,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-11T18:16:47+00:00"
+ "time": "2026-02-23T18:17:33+00:00"
},
{
"name": "symfony/twig-bundle",
- "version": "v6.4.31",
+ "version": "v6.4.32",
"source": {
"type": "git",
"url": "https://github.com/symfony/twig-bundle.git",
- "reference": "df14e1b81483b04534317e8f61617560ab60777d"
+ "reference": "a5c8dcc11a5bf9c96320da20070d2e158a4e0b30"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/df14e1b81483b04534317e8f61617560ab60777d",
- "reference": "df14e1b81483b04534317e8f61617560ab60777d",
+ "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/a5c8dcc11a5bf9c96320da20070d2e158a4e0b30",
+ "reference": "a5c8dcc11a5bf9c96320da20070d2e158a4e0b30",
"shasum": ""
},
"require": {
@@ -10275,7 +10276,7 @@
"description": "Provides a tight integration of Twig into the Symfony full-stack framework",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/twig-bundle/tree/v6.4.31"
+ "source": "https://github.com/symfony/twig-bundle/tree/v6.4.32"
},
"funding": [
{
@@ -10295,20 +10296,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-18T03:19:37+00:00"
+ "time": "2026-01-05T12:44:39+00:00"
},
{
"name": "symfony/uid",
- "version": "v6.4.24",
+ "version": "v6.4.32",
"source": {
"type": "git",
"url": "https://github.com/symfony/uid.git",
- "reference": "17da16a750541a42cf2183935e0f6008316c23f7"
+ "reference": "6b973c385f00341b246f697d82dc01a09107acdd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/uid/zipball/17da16a750541a42cf2183935e0f6008316c23f7",
- "reference": "17da16a750541a42cf2183935e0f6008316c23f7",
+ "url": "https://api.github.com/repos/symfony/uid/zipball/6b973c385f00341b246f697d82dc01a09107acdd",
+ "reference": "6b973c385f00341b246f697d82dc01a09107acdd",
"shasum": ""
},
"require": {
@@ -10353,7 +10354,7 @@
"uuid"
],
"support": {
- "source": "https://github.com/symfony/uid/tree/v6.4.24"
+ "source": "https://github.com/symfony/uid/tree/v6.4.32"
},
"funding": [
{
@@ -10373,20 +10374,20 @@
"type": "tidelift"
}
],
- "time": "2025-07-10T08:14:14+00:00"
+ "time": "2025-12-23T15:07:59+00:00"
},
{
"name": "symfony/validator",
- "version": "v6.4.31",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/validator.git",
- "reference": "0c3f60adce4e6fc86583b0c7e363ce90fe3ca3e7"
+ "reference": "7c3897b7f739d4ab913481e680405ca82d08084d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/validator/zipball/0c3f60adce4e6fc86583b0c7e363ce90fe3ca3e7",
- "reference": "0c3f60adce4e6fc86583b0c7e363ce90fe3ca3e7",
+ "url": "https://api.github.com/repos/symfony/validator/zipball/7c3897b7f739d4ab913481e680405ca82d08084d",
+ "reference": "7c3897b7f739d4ab913481e680405ca82d08084d",
"shasum": ""
},
"require": {
@@ -10454,7 +10455,7 @@
"description": "Provides tools to validate values",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/validator/tree/v6.4.31"
+ "source": "https://github.com/symfony/validator/tree/v6.4.34"
},
"funding": [
{
@@ -10474,20 +10475,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-24T09:35:58+00:00"
+ "time": "2026-02-23T17:49:24+00:00"
},
{
"name": "symfony/var-dumper",
- "version": "v6.4.26",
+ "version": "v6.4.32",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "cfae1497a2f1eaad78dbc0590311c599c7178d4a"
+ "reference": "131fc9915e0343052af5ed5040401b481ca192aa"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cfae1497a2f1eaad78dbc0590311c599c7178d4a",
- "reference": "cfae1497a2f1eaad78dbc0590311c599c7178d4a",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/131fc9915e0343052af5ed5040401b481ca192aa",
+ "reference": "131fc9915e0343052af5ed5040401b481ca192aa",
"shasum": ""
},
"require": {
@@ -10542,7 +10543,7 @@
"dump"
],
"support": {
- "source": "https://github.com/symfony/var-dumper/tree/v6.4.26"
+ "source": "https://github.com/symfony/var-dumper/tree/v6.4.32"
},
"funding": [
{
@@ -10562,7 +10563,7 @@
"type": "tidelift"
}
],
- "time": "2025-09-25T15:37:27+00:00"
+ "time": "2026-01-01T13:34:06+00:00"
},
{
"name": "symfony/var-exporter",
@@ -10647,16 +10648,16 @@
},
{
"name": "symfony/web-link",
- "version": "v6.4.24",
+ "version": "v6.4.32",
"source": {
"type": "git",
"url": "https://github.com/symfony/web-link.git",
- "reference": "75ffbb304f26a716969863328c8c6a11eadcfa5a"
+ "reference": "636d5e34cd5c4a2538b02ba48a3c02989bfdf06b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/web-link/zipball/75ffbb304f26a716969863328c8c6a11eadcfa5a",
- "reference": "75ffbb304f26a716969863328c8c6a11eadcfa5a",
+ "url": "https://api.github.com/repos/symfony/web-link/zipball/636d5e34cd5c4a2538b02ba48a3c02989bfdf06b",
+ "reference": "636d5e34cd5c4a2538b02ba48a3c02989bfdf06b",
"shasum": ""
},
"require": {
@@ -10710,7 +10711,7 @@
"push"
],
"support": {
- "source": "https://github.com/symfony/web-link/tree/v6.4.24"
+ "source": "https://github.com/symfony/web-link/tree/v6.4.32"
},
"funding": [
{
@@ -10730,20 +10731,20 @@
"type": "tidelift"
}
],
- "time": "2025-07-10T08:14:14+00:00"
+ "time": "2026-01-01T13:45:34+00:00"
},
{
"name": "symfony/yaml",
- "version": "v6.4.30",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "8207ae83da19ee3748d6d4f567b4d9a7c656e331"
+ "reference": "7bca30dabed7900a08c5ad4f1d6483f881a64d0f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/8207ae83da19ee3748d6d4f567b4d9a7c656e331",
- "reference": "8207ae83da19ee3748d6d4f567b4d9a7c656e331",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/7bca30dabed7900a08c5ad4f1d6483f881a64d0f",
+ "reference": "7bca30dabed7900a08c5ad4f1d6483f881a64d0f",
"shasum": ""
},
"require": {
@@ -10786,7 +10787,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/yaml/tree/v6.4.30"
+ "source": "https://github.com/symfony/yaml/tree/v6.4.34"
},
"funding": [
{
@@ -10806,20 +10807,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-02T11:50:18+00:00"
+ "time": "2026-02-06T18:32:11+00:00"
},
{
"name": "twig/twig",
- "version": "v3.22.2",
+ "version": "v3.23.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
- "reference": "946ddeafa3c9f4ce279d1f34051af041db0e16f2"
+ "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/twigphp/Twig/zipball/946ddeafa3c9f4ce279d1f34051af041db0e16f2",
- "reference": "946ddeafa3c9f4ce279d1f34051af041db0e16f2",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
+ "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
"shasum": ""
},
"require": {
@@ -10873,7 +10874,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
- "source": "https://github.com/twigphp/Twig/tree/v3.22.2"
+ "source": "https://github.com/twigphp/Twig/tree/v3.23.0"
},
"funding": [
{
@@ -10885,7 +10886,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-14T11:28:47+00:00"
+ "time": "2026-01-23T21:00:41+00:00"
},
{
"name": "vich/uploader-bundle",
@@ -11391,16 +11392,16 @@
},
{
"name": "ergebnis/composer-normalize",
- "version": "2.48.2",
+ "version": "2.50.0",
"source": {
"type": "git",
"url": "https://github.com/ergebnis/composer-normalize.git",
- "reference": "86dc9731b8320f49e9be9ad6d8e4de9b8b0e9b8b"
+ "reference": "80971fe24ff10709789942bcbe9368b2c704097c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ergebnis/composer-normalize/zipball/86dc9731b8320f49e9be9ad6d8e4de9b8b0e9b8b",
- "reference": "86dc9731b8320f49e9be9ad6d8e4de9b8b0e9b8b",
+ "url": "https://api.github.com/repos/ergebnis/composer-normalize/zipball/80971fe24ff10709789942bcbe9368b2c704097c",
+ "reference": "80971fe24ff10709789942bcbe9368b2c704097c",
"shasum": ""
},
"require": {
@@ -11414,20 +11415,20 @@
"php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
},
"require-dev": {
- "composer/composer": "^2.8.3",
+ "composer/composer": "^2.9.4",
"ergebnis/license": "^2.7.0",
- "ergebnis/php-cs-fixer-config": "^6.53.0",
- "ergebnis/phpstan-rules": "^2.11.0",
+ "ergebnis/php-cs-fixer-config": "^6.59.0",
+ "ergebnis/phpstan-rules": "^2.13.1",
"ergebnis/phpunit-slow-test-detector": "^2.20.0",
+ "ergebnis/rector-rules": "^1.9.0",
"fakerphp/faker": "^1.24.1",
- "infection/infection": "~0.26.6",
"phpstan/extension-installer": "^1.4.3",
- "phpstan/phpstan": "^2.1.17",
+ "phpstan/phpstan": "^2.1.38",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
- "phpstan/phpstan-phpunit": "^2.0.7",
- "phpstan/phpstan-strict-rules": "^2.0.6",
- "phpunit/phpunit": "^9.6.20",
- "rector/rector": "^2.1.4",
+ "phpstan/phpstan-phpunit": "^2.0.12",
+ "phpstan/phpstan-strict-rules": "^2.0.8",
+ "phpunit/phpunit": "^9.6.33",
+ "rector/rector": "^2.3.5",
"symfony/filesystem": "^5.4.41"
},
"type": "composer-plugin",
@@ -11471,7 +11472,7 @@
"security": "https://github.com/ergebnis/composer-normalize/blob/main/.github/SECURITY.md",
"source": "https://github.com/ergebnis/composer-normalize"
},
- "time": "2025-09-06T11:42:34+00:00"
+ "time": "2026-02-09T20:57:47+00:00"
},
{
"name": "ergebnis/json",
@@ -12523,11 +12524,11 @@
},
{
"name": "phpstan/phpstan",
- "version": "1.12.32",
+ "version": "1.12.33",
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8",
- "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/37982d6fc7cbb746dda7773530cda557cdf119e1",
+ "reference": "37982d6fc7cbb746dda7773530cda557cdf119e1",
"shasum": ""
},
"require": {
@@ -12572,7 +12573,7 @@
"type": "github"
}
],
- "time": "2025-09-30T10:16:31+00:00"
+ "time": "2026-02-28T20:30:03+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -12895,16 +12896,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "9.6.31",
+ "version": "9.6.34",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "945d0b7f346a084ce5549e95289962972c4272e5"
+ "reference": "b36f02317466907a230d3aa1d34467041271ef4a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/945d0b7f346a084ce5549e95289962972c4272e5",
- "reference": "945d0b7f346a084ce5549e95289962972c4272e5",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a",
+ "reference": "b36f02317466907a230d3aa1d34467041271ef4a",
"shasum": ""
},
"require": {
@@ -12926,7 +12927,7 @@
"phpunit/php-timer": "^5.0.3",
"sebastian/cli-parser": "^1.0.2",
"sebastian/code-unit": "^1.0.8",
- "sebastian/comparator": "^4.0.9",
+ "sebastian/comparator": "^4.0.10",
"sebastian/diff": "^4.0.6",
"sebastian/environment": "^5.1.5",
"sebastian/exporter": "^4.0.8",
@@ -12978,7 +12979,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.31"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34"
},
"funding": [
{
@@ -13002,7 +13003,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-06T07:45:52+00:00"
+ "time": "2026-01-27T05:45:00+00:00"
},
{
"name": "psalm/plugin-symfony",
@@ -13297,16 +13298,16 @@
},
{
"name": "sebastian/comparator",
- "version": "4.0.9",
+ "version": "4.0.10",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5"
+ "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5",
- "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d",
+ "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d",
"shasum": ""
},
"require": {
@@ -13359,7 +13360,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
- "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9"
+ "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10"
},
"funding": [
{
@@ -13379,7 +13380,7 @@
"type": "tidelift"
}
],
- "time": "2025-08-10T06:51:50+00:00"
+ "time": "2026-01-24T09:22:56+00:00"
},
{
"name": "sebastian/complexity",
@@ -14143,16 +14144,16 @@
},
{
"name": "symfony/browser-kit",
- "version": "v6.4.31",
+ "version": "v6.4.32",
"source": {
"type": "git",
"url": "https://github.com/symfony/browser-kit.git",
- "reference": "5b8564c882ca8eb9a06ed2840abc9b2a40f1e12a"
+ "reference": "f49947cf0cbd7d685281ef74e05b98f5e75b181f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/browser-kit/zipball/5b8564c882ca8eb9a06ed2840abc9b2a40f1e12a",
- "reference": "5b8564c882ca8eb9a06ed2840abc9b2a40f1e12a",
+ "url": "https://api.github.com/repos/symfony/browser-kit/zipball/f49947cf0cbd7d685281ef74e05b98f5e75b181f",
+ "reference": "f49947cf0cbd7d685281ef74e05b98f5e75b181f",
"shasum": ""
},
"require": {
@@ -14191,7 +14192,7 @@
"description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/browser-kit/tree/v6.4.31"
+ "source": "https://github.com/symfony/browser-kit/tree/v6.4.32"
},
"funding": [
{
@@ -14211,20 +14212,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-12T07:51:57+00:00"
+ "time": "2026-01-13T10:09:10+00:00"
},
{
"name": "symfony/css-selector",
- "version": "v6.4.24",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
- "reference": "9b784413143701aa3c94ac1869a159a9e53e8761"
+ "reference": "b0314c186f1464de048cce58979ff1625ca88bbb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/css-selector/zipball/9b784413143701aa3c94ac1869a159a9e53e8761",
- "reference": "9b784413143701aa3c94ac1869a159a9e53e8761",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/b0314c186f1464de048cce58979ff1625ca88bbb",
+ "reference": "b0314c186f1464de048cce58979ff1625ca88bbb",
"shasum": ""
},
"require": {
@@ -14260,7 +14261,7 @@
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/css-selector/tree/v6.4.24"
+ "source": "https://github.com/symfony/css-selector/tree/v6.4.34"
},
"funding": [
{
@@ -14280,7 +14281,7 @@
"type": "tidelift"
}
],
- "time": "2025-07-10T08:14:14+00:00"
+ "time": "2026-02-16T08:37:21+00:00"
},
{
"name": "symfony/debug-bundle",
@@ -14543,16 +14544,16 @@
},
{
"name": "symfony/web-profiler-bundle",
- "version": "v6.4.27",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/web-profiler-bundle.git",
- "reference": "4c2ab411372e8bd854678cd7c81f1a9bfd6914aa"
+ "reference": "848bc5d5745500f855bb201d57ae066fd7e67448"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/4c2ab411372e8bd854678cd7c81f1a9bfd6914aa",
- "reference": "4c2ab411372e8bd854678cd7c81f1a9bfd6914aa",
+ "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/848bc5d5745500f855bb201d57ae066fd7e67448",
+ "reference": "848bc5d5745500f855bb201d57ae066fd7e67448",
"shasum": ""
},
"require": {
@@ -14605,7 +14606,7 @@
"dev"
],
"support": {
- "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.4.27"
+ "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.4.34"
},
"funding": [
{
@@ -14625,7 +14626,7 @@
"type": "tidelift"
}
],
- "time": "2025-10-05T13:55:43+00:00"
+ "time": "2026-02-05T15:19:06+00:00"
},
{
"name": "theofidry/alice-data-fixtures",
@@ -14890,37 +14891,37 @@
},
{
"name": "vincentlanglet/twig-cs-fixer",
- "version": "3.11.0",
+ "version": "3.14.0",
"source": {
"type": "git",
"url": "https://github.com/VincentLanglet/Twig-CS-Fixer.git",
- "reference": "866af065fd09980b6390ee5c69e45b08053101e8"
+ "reference": "599f110f192c31af5deb5736d6c1a970afdf51f3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/VincentLanglet/Twig-CS-Fixer/zipball/866af065fd09980b6390ee5c69e45b08053101e8",
- "reference": "866af065fd09980b6390ee5c69e45b08053101e8",
+ "url": "https://api.github.com/repos/VincentLanglet/Twig-CS-Fixer/zipball/599f110f192c31af5deb5736d6c1a970afdf51f3",
+ "reference": "599f110f192c31af5deb5736d6c1a970afdf51f3",
"shasum": ""
},
"require": {
"composer-runtime-api": "^2.0.0",
"ext-ctype": "*",
- "ext-json": "*",
- "php": ">=8.0",
+ "php": ">=8.1",
"symfony/console": "^5.4.9 || ^6.4 || ^7.0 || ^8.0",
"symfony/filesystem": "^5.4 || ^6.4 || ^7.0 || ^8.0",
"symfony/finder": "^5.4 || ^6.4 || ^7.0 || ^8.0",
"symfony/string": "^5.4.42 || ^6.4.10 || ~7.0.10 || ^7.1.3 || ^8.0",
"twig/twig": "^3.4",
- "webmozart/assert": "^1.10"
+ "webmozart/assert": "^1.10 || ^2.0"
},
"require-dev": {
"composer/semver": "^3.2.0",
"dereuromark/composer-prefer-lowest": "^0.1.10",
"ergebnis/composer-normalize": "^2.29",
"friendsofphp/php-cs-fixer": "^3.13.0",
- "infection/infection": "^0.26.16 || ^0.29.14",
+ "infection/infection": "^0.26.16 || ^0.32.0",
"phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpstan/phpstan-symfony": "^2.0",
@@ -14955,7 +14956,7 @@
"homepage": "https://github.com/VincentLanglet/Twig-CS-Fixer",
"support": {
"issues": "https://github.com/VincentLanglet/Twig-CS-Fixer/issues",
- "source": "https://github.com/VincentLanglet/Twig-CS-Fixer/tree/3.11.0"
+ "source": "https://github.com/VincentLanglet/Twig-CS-Fixer/tree/3.14.0"
},
"funding": [
{
@@ -14963,7 +14964,7 @@
"type": "github"
}
],
- "time": "2025-11-24T18:13:18+00:00"
+ "time": "2026-02-23T13:21:35+00:00"
},
{
"name": "weirdan/doctrine-psalm-plugin",
diff --git a/docs/v2-changelogs/admin.md b/docs/v2-changelogs/admin.md
index fdb6189a9..861678f6c 100644
--- a/docs/v2-changelogs/admin.md
+++ b/docs/v2-changelogs/admin.md
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+## [2.6.1] - 2026-03-06
+
+- [#295](https://github.com/os2display/display-admin-client/pull/295)
+ - Fixed slide media array not syncing with content on media removal.
+
## [2.6.0] - 2025-12-05
- [#293](https://github.com/os2display/display-admin-client/pull/293)
diff --git a/docs/v2-changelogs/api.md b/docs/v2-changelogs/api.md
index e50d76405..d4e91037a 100644
--- a/docs/v2-changelogs/api.md
+++ b/docs/v2-changelogs/api.md
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+## [2.6.1] - 2026-03-10
+
+- [#347](https://github.com/os2display/display-api-service/pull/347)
+ - Added onFlush listener to handle ManyToMany collection changes for relations checksum propagation.
+ - Added command to refresh relation checksums.
+
## [2.6.0] - 2025-12-05
- [#330](https://github.com/os2display/display-api-service/pull/330)
diff --git a/docs/v2-changelogs/template.md b/docs/v2-changelogs/template.md
index 43ab7f98a..793c82992 100644
--- a/docs/v2-changelogs/template.md
+++ b/docs/v2-changelogs/template.md
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
## Unreleased
+## [2.6.1] - 2026-03-06
+
+- [#196](https://github.com/os2display/display-templates/pull/196)
+ - Fixed image-text carousel not updating when images array changes.
+
## [2.6.0] - 2025-12-05
- [#194](https://github.com/os2display/display-templates/pull/194)
diff --git a/src/Command/Checksum/RecalculateChecksumCommand.php b/src/Command/Checksum/RecalculateChecksumCommand.php
new file mode 100644
index 000000000..43d5b53a9
--- /dev/null
+++ b/src/Command/Checksum/RecalculateChecksumCommand.php
@@ -0,0 +1,158 @@
+addOption(self::OPTION_TENANT, null, InputOption::VALUE_REQUIRED, 'Filter by tenant key')
+ ->addOption(self::OPTION_MODIFIED_AFTER, null, InputOption::VALUE_REQUIRED, 'Filter by modified_at >= date (e.g. "2024-01-01" or "2024-01-01 12:00:00")')
+ ->setHelp(<<<'HELP'
+The %command.name% command recalculates relation checksums for slides and media,
+then propagates the changes up the entity tree.
+
+ php %command.full_name%
+
+You can filter by tenant key and/or modification date:
+
+ php %command.full_name% --tenant=ABC
+ php %command.full_name% --modified-after="2024-01-01"
+ php %command.full_name% --tenant=ABC --modified-after="2024-01-01 12:00:00"
+
+Without any filters, all slides and media will be recalculated.
+HELP)
+ ;
+ }
+
+ #[\Override]
+ public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
+ {
+ if ($input->mustSuggestOptionValuesFor(self::OPTION_TENANT)) {
+ $tenants = $this->tenantRepository->findAll();
+ foreach ($tenants as $tenant) {
+ $suggestions->suggestValue($tenant->getTenantKey());
+ }
+ }
+ }
+
+ #[\Override]
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+ $stopwatch = new Stopwatch();
+ $stopwatch->start('checksum-recalculate');
+
+ $tenantKey = $input->getOption(self::OPTION_TENANT);
+ $modifiedAfterStr = $input->getOption(self::OPTION_MODIFIED_AFTER);
+
+ // Resolve tenant
+ $tenant = null;
+ if (null !== $tenantKey) {
+ $tenant = $this->tenantRepository->findOneBy(['tenantKey' => $tenantKey]);
+ if (null === $tenant) {
+ $io->error(sprintf('Tenant with key "%s" not found.', $tenantKey));
+
+ return Command::FAILURE;
+ }
+ $io->info(sprintf('Filtering by tenant: %s', $tenantKey));
+ }
+
+ // Parse date
+ $modifiedAfter = null;
+ if (null !== $modifiedAfterStr) {
+ try {
+ $modifiedAfter = new \DateTimeImmutable($modifiedAfterStr);
+ } catch (\Exception) {
+ $io->error(sprintf('Invalid date format: "%s". Use formats like "Y-m-d" or "Y-m-d H:i:s".', $modifiedAfterStr));
+
+ return Command::FAILURE;
+ }
+ $io->info(sprintf('Filtering by modified after: %s', $modifiedAfter->format('Y-m-d H:i:s')));
+ }
+
+ // Mark matching slides and media as changed using DQL UPDATE
+ $targetEntities = [
+ 'slide' => Slide::class,
+ 'media' => Media::class,
+ ];
+ $totalAffected = 0;
+
+ foreach ($targetEntities as $label => $entityClass) {
+ $qb = $this->entityManager->createQueryBuilder()
+ ->update($entityClass, 'e')
+ ->set('e.changed', ':changed')
+ ->setParameter('changed', true);
+
+ if (null !== $tenant) {
+ $qb->andWhere('e.tenant = :tenant')
+ ->setParameter('tenant', $tenant, Tenant::class);
+ }
+
+ if (null !== $modifiedAfter) {
+ $qb->andWhere('e.modifiedAt >= :modifiedAfter')
+ ->setParameter('modifiedAfter', $modifiedAfter, 'datetime_immutable');
+ }
+
+ $affected = $qb->getQuery()->execute();
+ $totalAffected += $affected;
+ $io->info(sprintf('Marked %d rows in "%s" as changed.', $affected, $label));
+ }
+
+ if (0 === $totalAffected) {
+ $io->warning('No rows matched the given filters. Nothing to recalculate.');
+
+ return Command::SUCCESS;
+ }
+
+ // Propagate checksums through entity tree
+ $io->info('Propagating checksums through entity tree...');
+ $this->calculator->execute(withWhereClause: true);
+
+ $event = $stopwatch->stop('checksum-recalculate');
+
+ $io->success(sprintf(
+ 'Checksums recalculated. %d rows marked. Elapsed: %.2f ms, Memory: %.2f MB',
+ $totalAffected,
+ $event->getDuration(),
+ $event->getMemory() / (1024 ** 2)
+ ));
+
+ return Command::SUCCESS;
+ }
+}
diff --git a/src/Controller/Api/InteractiveController.php b/src/Controller/Api/InteractiveController.php
index edd3ee460..6cdde602a 100644
--- a/src/Controller/Api/InteractiveController.php
+++ b/src/Controller/Api/InteractiveController.php
@@ -35,7 +35,7 @@ public function __invoke(Request $request, Slide $slide): JsonResponse
{
$user = $this->security->getUser();
- if (!($user instanceof ScreenUser)) {
+ if (!$user instanceof ScreenUser) {
throw new AccessDeniedHttpException('Only screen user can perform action.');
}
diff --git a/src/DataFixtures/Loader/DoctrineOrmLoaderDecorator.php b/src/DataFixtures/Loader/DoctrineOrmLoaderDecorator.php
index f6c21aa4b..85ef46bfc 100644
--- a/src/DataFixtures/Loader/DoctrineOrmLoaderDecorator.php
+++ b/src/DataFixtures/Loader/DoctrineOrmLoaderDecorator.php
@@ -6,6 +6,7 @@
use App\EventListener\RelationsChecksumListener;
use App\EventListener\TimestampableListener;
+use App\Service\RelationsChecksumCalculator;
use Doctrine\ORM\EntityManagerInterface;
use Hautelook\AliceBundle\Loader\DoctrineOrmLoader;
use Hautelook\AliceBundle\LoaderInterface as AliceBundleLoaderInterface;
@@ -25,7 +26,8 @@
readonly class DoctrineOrmLoaderDecorator implements AliceBundleLoaderInterface, LoggerAwareInterface
{
public function __construct(
- private DoctrineOrmLoader $decorated,
+ private readonly DoctrineOrmLoader $decorated,
+ private readonly RelationsChecksumCalculator $calculator,
) {}
public function load(Application $application, EntityManagerInterface $manager, array $bundles, string $environment, bool $append, bool $purgeWithTruncate, bool $noBundles = false): array
@@ -57,7 +59,7 @@ public function load(Application $application, EntityManagerInterface $manager,
$result = $this->decorated->load($application, $manager, $bundles, $environment, $append, $purgeWithTruncate, $noBundles);
// Apply the SQL statements from the disabled "postFlush" listener
- $this->applyRelationsModified($manager);
+ $this->applyRelationsModified();
// Re-enable listeners
$eventManager->addEventListener('postFlush', $relationsModifiedAtListener);
@@ -75,16 +77,8 @@ public function withLogger(LoggerInterface $logger): static
$this->decorated->withLogger($logger);
}
- private function applyRelationsModified(EntityManagerInterface $manager): void
+ private function applyRelationsModified(): void
{
- $connection = $manager->getConnection();
-
- $sqlQueries = RelationsChecksumListener::getUpdateRelationsAtQueries(withWhereClause: false);
-
- $rows = 0;
- foreach ($sqlQueries as $sqlQuery) {
- $stm = $connection->prepare($sqlQuery);
- $rows += $stm->executeStatement();
- }
+ $this->calculator->execute(withWhereClause: false);
}
}
diff --git a/src/EventListener/RelationsChecksumListener.php b/src/EventListener/RelationsChecksumListener.php
index ce78efb3d..17b7d98b4 100644
--- a/src/EventListener/RelationsChecksumListener.php
+++ b/src/EventListener/RelationsChecksumListener.php
@@ -18,7 +18,9 @@
use App\Entity\Tenant\ScreenGroup;
use App\Entity\Tenant\ScreenGroupCampaign;
use App\Entity\Tenant\Slide;
+use App\Service\RelationsChecksumCalculator;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
+use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Event\PreRemoveEventArgs;
@@ -53,14 +55,12 @@
#[AsDoctrineListener(event: Events::prePersist, priority: 100)]
#[AsDoctrineListener(event: Events::preUpdate)]
#[AsDoctrineListener(event: Events::preRemove)]
+#[AsDoctrineListener(event: Events::onFlush)]
#[AsDoctrineListener(event: Events::postFlush)]
class RelationsChecksumListener
{
- private const array CHECKSUM_TABLES = ['feed_source', 'feed', 'slide', 'media', 'theme', 'template', 'playlist_slide',
- 'playlist', 'screen_campaign', 'screen', 'screen_group_campaign', 'screen_group',
- 'playlist_screen_region', 'screen_layout_regions', 'screen_layout'];
-
public function __construct(
+ private readonly RelationsChecksumCalculator $calculator,
private readonly bool $enabled = false,
) {}
@@ -73,6 +73,8 @@ public function __construct(
* function to work. If the field is left as null it will be serialized to the
* database as `[]` preventing JSON_SET from updating the field.
*
+ * @param PrePersistEventArgs $args
+ *
* @return void
*/
final public function prePersist(PrePersistEventArgs $args): void
@@ -166,6 +168,8 @@ final public function prePersist(PrePersistEventArgs $args): void
*
* On update set "changed" to "true" to ensure checksum changes propagate up the tree.
*
+ * @param PreUpdateEventArgs $args
+ *
* @return void
*/
final public function preUpdate(PreUpdateEventArgs $args): void
@@ -184,10 +188,12 @@ final public function preUpdate(PreUpdateEventArgs $args): void
/**
* PreRemove listener.
*
- * For "toMany" relations the "preUpdate" listener will not be called for the parent if a child relations
- * is deleted by calling remove() on the entity manager. We need to manually set "changed" on the parent
+ * For "toMany" relations the "preUpdate" listener will not be called for the parent if child relations
+ * are deleted by calling remove() on the entity manager. We need to manually set "changed" on the parent
* to "true" to ensure checksum changes propagate up the tree.
*
+ * @param PreRemoveEventArgs $args
+ *
* @return void
*/
final public function preRemove(PreRemoveEventArgs $args): void
@@ -231,317 +237,66 @@ final public function preRemove(PreRemoveEventArgs $args): void
}
/**
- * PostFlush listener.
+ * OnFlush listener.
*
- * Executes update SQL queries to set changed and relations_checksum fields in the database.
+ * This listener is used to set the "changed" flag on entities that have been modified since the last flush.
+ * This is required because Doctrine does not call the "preUpdate" listener for entities that only have
+ * collection changes.
*
- * @param PostFlushEventArgs $args the PostFlushEventArgs object containing information about the event
+ * @param OnFlushEventArgs $args
*
* @return void
- *
- * @throws \Doctrine\DBAL\Exception
*/
- final public function postFlush(PostFlushEventArgs $args): void
+ final public function onFlush(OnFlushEventArgs $args): void
{
if (!$this->enabled) {
return;
}
- $connection = $args->getObjectManager()->getConnection();
-
- $sqlQueries = self::getUpdateRelationsAtQueries(withWhereClause: true);
-
- $connection->beginTransaction();
-
- try {
- foreach ($sqlQueries as $sqlQuery) {
- $stm = $connection->prepare($sqlQuery);
- $stm->executeStatement();
+ $em = $args->getObjectManager();
+ $uow = $em->getUnitOfWork();
+
+ // Catch ManyToMany collection adds/removes
+ foreach ($uow->getScheduledCollectionUpdates() as $collection) {
+ $owner = $collection->getOwner();
+ if ($owner instanceof RelationsChecksumInterface) {
+ $owner->setChanged(true);
+ $uow->recomputeSingleEntityChangeSet(
+ $em->getClassMetadata($owner::class),
+ $owner
+ );
}
-
- $connection->commit();
- } catch (\Exception $e) {
- $connection->rollBack();
- throw $e;
- }
- }
-
- /**
- * Get an array of SQL update statements to update the changed and relationsModified fields.
- *
- * @param bool $withWhereClause
- * Should the statements include a where clause to limit the statement
- *
- * @return string[]
- * Array of SQL statements
- */
- public static function getUpdateRelationsAtQueries(bool $withWhereClause = true): array
- {
- // Set SQL update queries for the "relations checksum" fields on the parent (p), child (c) relationships up through the entity tree
- $sqlQueries = [];
-
- // Feed
- $sqlQueries[] = self::getToOneQuery(jsonKey: 'feedSource', parentTable: 'feed', childTable: 'feed_source', withWhereClause: $withWhereClause);
- $sqlQueries[] = self::getToOneQuery(jsonKey: 'slide', parentTable: 'feed', childTable: 'slide', parentTableId: 'id', childTableId: 'feed_id', withWhereClause: $withWhereClause);
-
- // Slide
- $sqlQueries[] = self::getManyToManyQuery(jsonKey: 'media', parentTable: 'slide', pivotTable: 'slide_media', childTable: 'media', withWhereClause: $withWhereClause);
- $sqlQueries[] = self::getToOneQuery(jsonKey: 'theme', parentTable: 'slide', childTable: 'theme', withWhereClause: $withWhereClause);
- $sqlQueries[] = self::getToOneQuery(jsonKey: 'templateInfo', parentTable: 'slide', childTable: 'template', withWhereClause: $withWhereClause);
- $sqlQueries[] = self::getToOneQuery(jsonKey: 'feed', parentTable: 'slide', childTable: 'feed', withWhereClause: $withWhereClause);
-
- // PlaylistSlide
- $sqlQueries[] = self::getToOneQuery(jsonKey: 'slide', parentTable: 'playlist_slide', childTable: 'slide', withWhereClause: $withWhereClause);
-
- // Playlist
- $sqlQueries[] = self::getOneToManyQuery(jsonKey: 'slides', parentTable: 'playlist', childTable: 'playlist_slide', withWhereClause: $withWhereClause);
-
- // ScreenCampaign
- $sqlQueries[] = self::getToOneQuery(jsonKey: 'campaign', parentTable: 'screen_campaign', childTable: 'playlist', parentTableId: 'campaign_id', withWhereClause: $withWhereClause);
- $sqlQueries[] = self::getToOneQuery(jsonKey: 'screen', parentTable: 'screen_campaign', childTable: 'screen', withWhereClause: $withWhereClause);
-
- // ScreenGroupCampaign - campaign
- $sqlQueries[] = self::getToOneQuery(jsonKey: 'campaign', parentTable: 'screen_group_campaign', childTable: 'playlist', parentTableId: 'campaign_id', withWhereClause: $withWhereClause);
-
- // ScreenGroup
- $sqlQueries[] = self::getManyToManyQuery(jsonKey: 'screens', parentTable: 'screen_group', pivotTable: 'screen_group_screen', childTable: 'screen', withWhereClause: $withWhereClause);
- $sqlQueries[] = self::getOneToManyQuery(jsonKey: 'screenGroupCampaigns', parentTable: 'screen_group', childTable: 'screen_group_campaign', withWhereClause: $withWhereClause);
-
- // ScreenGroupCampaign - screenGroup
- $sqlQueries[] = self::getToOneQuery(jsonKey: 'screenGroup', parentTable: 'screen_group_campaign', childTable: 'screen_group', withWhereClause: $withWhereClause);
-
- // PlaylistScreenRegion
- $sqlQueries[] = self::getToOneQuery(jsonKey: 'playlist', parentTable: 'playlist_screen_region', childTable: 'playlist', withWhereClause: $withWhereClause);
-
- // ScreenLayoutRegions
- $sqlQueries[] = self::getToOneQuery(jsonKey: 'regions', parentTable: 'screen_layout_regions', childTable: 'playlist_screen_region', parentTableId: 'id', childTableId: 'region_id', withWhereClause: $withWhereClause);
-
- // ScreenLayout
- $sqlQueries[] = self::getOneToManyQuery(jsonKey: 'regions', parentTable: 'screen_layout', childTable: 'screen_layout_regions', withWhereClause: $withWhereClause);
-
- // Screen
- $sqlQueries[] = self::getOneToManyQuery(jsonKey: 'campaigns', parentTable: 'screen', childTable: 'screen_campaign', withWhereClause: $withWhereClause);
- $sqlQueries[] = self::getToOneQuery(jsonKey: 'layout', parentTable: 'screen', childTable: 'screen_layout', withWhereClause: $withWhereClause);
- $sqlQueries[] = self::getOneToManyQuery(jsonKey: 'regions', parentTable: 'screen', childTable: 'playlist_screen_region', withWhereClause: $withWhereClause);
- $sqlQueries[] = self::getManyToManyQuery(jsonKey: 'inScreenGroups', parentTable: 'screen', pivotTable: 'screen_group_screen', childTable: 'screen_group', withWhereClause: $withWhereClause);
-
- // Add reset 'changed' fields queries
- $sqlQueries = array_merge($sqlQueries, self::getResetChangedQueries());
-
- return $sqlQueries;
- }
-
- /**
- * Get "One/ManyToOne" query.
- *
- * For a table (parent) that has a relation to another table (child) where we need to update the "relations_checksum"
- * field on the parent with a checksum of values from the child we need to join the tables and set the values.
- *
- * Basically we do: "Update parent, join child, set parent value = SHA(child values)"
- *
- * Example:
- * UPDATE slide p
- * INNER JOIN theme c ON p.theme_id = c.id
- * SET p.changed = 1,
- * p.relations_checksum = JSON_SET(p.relations_checksum, "$.theme", SHA1(CONCAT(c.id, c.version, c.relations_checksum)))
- * WHERE
- * p.changed = 1
- * OR c.changed = 1
- *
- * Explanation:
- * UPDATE parent table p, INNER JOIN child table c
- * - use INNER JOIN because the query only makes sense for result where both parent and child tables have rows
- * SET changed to 1 (true) to enable propagation up the tree.
- * SET the value for the relevant json key on the json object in p.relations_checksum to the checksum of the child id, version and relations checksum
- * WHERE either p.changed or c.changed is true
- * - Because we can't easily get a list of ID's of affected rows as we work up the tree we use the bool "changed" as clause in WHERE to limit to only update the rows just modified.
- *
- * @param string|null $parentTableId
- *
- * @return string
- */
- private static function getToOneQuery(string $jsonKey, string $parentTable, string $childTable, ?string $parentTableId = null, string $childTableId = 'id', bool $withWhereClause = true): string
- {
- // Set the column name to use for "ON" in the Join clause. By default, the child table name with "_id" appended.
- // E.g. "UPDATE feed p INNER JOIN feed_source c ON p.feed_source_id = c.id"
- $parentTableId ??= $childTable.'_id';
-
- // The base UPDATE query.
- // - Use INNER JON to only select rows that have a match in both parent and child tables
- // - Use JSON_SET to only INSERT/UPDATE the relevant key in the json object, not the whole field.
- $queryFormat = '
- UPDATE %s p
- INNER JOIN %s c ON p.%s = c.%s
- SET p.changed = 1,
- p.relations_checksum = JSON_SET(p.relations_checksum, "$.%s", SHA1(CONCAT(c.id, c.version, c.relations_checksum)))
- ';
-
- $query = sprintf($queryFormat, $parentTable, $childTable, $parentTableId, $childTableId, $jsonKey);
-
- // Add WHERE clause to only update rows that have been modified since ":modified_at"
- if ($withWhereClause) {
- $query .= ' WHERE p.changed = 1 OR c.changed = 1';
}
- return $query;
- }
-
- /**
- * Get "OnetoMany" query.
- *
- * For a table (parent) that has a toMany relationship to another table (child) where we need to update the "relations_checksum"
- * field on the parent with a checksum of values from the child we need to join the tables and set the values.
- *
- * Example:
- * UPDATE
- * playlist p
- * INNER JOIN (
- * SELECT
- * c.playlist_id,
- * CAST(GROUP_CONCAT(DISTINCT c.changed SEPARATOR "") > 0 AS UNSIGNED) changed,
- * SHA1(GROUP_CONCAT(c.id, c.version, c.relations_checksum)) checksum
- * FROM
- * playlist_slide c
- * GROUP BY
- * c.playlist_id
- * ) temp ON p.id = temp.playlist_id
- * SET p.changed = 1,
- * p.relations_checksum = JSON_SET(p.relations_checksum, "$.slides", temp.checksum)
- * WHERE p.changed = 1 OR temp.changed = 1
- *
- * Explanation:
- * Because this is a "to many" relation we need to GROUP_CONCAT values from the child relations. This is done in a temporary table
- * with GROUP BY parent id in the child table. This gives us just one child row for each parent row with a checksum from the relevant
- * fields across all child rows. We use a DISTINCT clause in GROUP_CONCAT to limit the length of the resulting value and avoid
- * illegal integer values.
- *
- * This temp table is then joined to the parent table to allow us to SET the p.changed and p.relations_checksum values on the parent.
- * - Because GROUP_CONCAT will give us all child rows "changed" as one, e.g. "00010001" we need "> 0" to evaluate to true/false
- * and then CAST that to "unsigned" to get a TINYINT (bool)
- * WHERE either p.changed or c.changed is true
- * - Because we can't easily get a list of ID's of affected rows as we work up the tree we use the bool "changed" as clause in
- * WHERE to limit to only update the rows just modified.
- *
- * @return string
- */
- private static function getOneToManyQuery(string $jsonKey, string $parentTable, string $childTable, bool $withWhereClause = true): string
- {
- $parentTableId = $parentTable.'_id';
-
- $queryFormat = '
- UPDATE
- %s p
- INNER JOIN (
- SELECT
- c.%s,
- CAST(GROUP_CONCAT(DISTINCT c.changed SEPARATOR "") > 0 AS UNSIGNED) changed,
- SHA1(GROUP_CONCAT(c.id, c.version, c.relations_checksum)) checksum
- FROM
- %s c
- GROUP BY
- c.%s
- ) temp ON p.id = temp.%s
- SET p.changed = 1,
- p.relations_checksum = JSON_SET(p.relations_checksum, "$.%s", temp.checksum)
- ';
-
- $query = sprintf($queryFormat, $parentTable, $parentTableId, $childTable, $parentTableId, $parentTableId, $jsonKey);
-
- if ($withWhereClause) {
- $query .= ' WHERE p.changed = 1 OR temp.changed = 1';
+ foreach ($uow->getScheduledCollectionDeletions() as $collection) {
+ $owner = $collection->getOwner();
+ if ($owner instanceof RelationsChecksumInterface) {
+ $owner->setChanged(true);
+ $uow->recomputeSingleEntityChangeSet(
+ $em->getClassMetadata($owner::class),
+ $owner
+ );
+ }
}
-
- return $query;
}
/**
- * Get "many to many" query.
- *
- * For a table (parent) that has a relation to another table (child) through a pivot table where we need to update the "changed"
- * and "relations_checksum" fields on the parent with values from the child we need to join the tables and set the values.
- *
- * Basically we do:
- * "Update parent, join temp (SELECT checksum of c.id, c.version, c.relations_checksum from the child rows with GROUP_CONCAT), set parent values = child values"
- *
- * Example:
- * UPDATE
- * slide p
- * INNER JOIN (
- * SELECT
- * pivot.slide_id,
- * CAST(GROUP_CONCAT(DISTINCT c.changed SEPARATOR "") > 0 AS UNSIGNED) changed,
- * SHA1(GROUP_CONCAT(c.id, c.version, c.relations_checksum)) checksum
- * FROM
- * slide_media pivot
- * INNER JOIN media c ON pivot.media_id = c.id
- * GROUP BY
- * pivot.slide_id
- * ) temp ON p.id = temp.slide_id
- * SET p.changed = 1,
- * p.relations_checksum = JSON_SET(p.relations_checksum, "$.media", temp.checksum)
- * WHERE p.changed = 1 OR temp.changed = 1
- *
- * Explanation:
- * Because this is a "to many" relation we need to GROUP_CONCAT values from the child relations. This is done in a temporary table
- * with GROUP BY parent id in the child table. This gives us just one child row for each parent row with a checksum from the relevant
- * fields across all child rows. We use a DISTINCT clause in GROUP_CONCAT to limit the length of the resulting value and avoid
- * illegal integer values.
+ * PostFlush listener.
*
- * This temp table is then joined to the parent table to allow us to SET the p.changed and p.relations_checksum values on the parent.
- * - Because GROUP_CONCAT will give us all child rows "changed" as one, e.g. "00010001" we need "> 0" to evaluate to true/false
- * and then CAST that to "unsigned" to get a TINYINT (bool)
- * WHERE either p.changed or c.changed is true
- * - Because we can't easily get a list of ID's of affected rows as we work up the tree we use the bool "changed" as clause in
- * WHERE to limit to only update the rows just modified.
+ * Executes update SQL queries to set changed and relations_checksum fields in the database.
*
- * @return string
- */
- private static function getManyToManyQuery(string $jsonKey, string $parentTable, string $pivotTable, string $childTable, bool $withWhereClause = true): string
- {
- $parentTableId = $parentTable.'_id';
- $childTableId = $childTable.'_id';
-
- $queryFormat = '
- UPDATE
- %s p
- INNER JOIN (
- SELECT
- pivot.%s,
- CAST(GROUP_CONCAT(DISTINCT c.changed SEPARATOR "") > 0 AS UNSIGNED) changed,
- SHA1(GROUP_CONCAT(c.id, c.version, c.relations_checksum)) checksum
- FROM
- %s pivot
- INNER JOIN %s c ON pivot.%s = c.id
- GROUP BY
- pivot.%s
- ) temp ON p.id = temp.%s
- SET p.changed = 1,
- p.relations_checksum = JSON_SET(p.relations_checksum, "$.%s", temp.checksum)
- ';
-
- $query = sprintf($queryFormat, $parentTable, $parentTableId, $pivotTable, $childTable, $childTableId, $parentTableId, $parentTableId, $jsonKey);
- if ($withWhereClause) {
- $query .= ' WHERE p.changed = 1 OR temp.changed = 1';
- }
-
- return $query;
- }
-
- /**
- * Get an array of queries to reset all "changed" fields to 0.
+ * @param PostFlushEventArgs $args the PostFlushEventArgs object containing information about the event
*
- * Example:
- * UPDATE screen SET screen.changed = 0 WHERE screen.changed = 1;
+ * @return void
*
- * @return array
+ * @throws \Doctrine\DBAL\Exception
*/
- private static function getResetChangedQueries(): array
+ final public function postFlush(PostFlushEventArgs $args): void
{
- $queries = [];
- foreach (self::CHECKSUM_TABLES as $table) {
- $queries[] = sprintf('UPDATE %s SET changed = 0 WHERE changed = 1', $table);
+ if (!$this->enabled) {
+ return;
}
- return $queries;
+ $this->calculator->execute(withWhereClause: true);
}
}
diff --git a/src/Feed/CalendarApiFeedType.php b/src/Feed/CalendarApiFeedType.php
index 3b0a7ed0b..8f5393ffe 100644
--- a/src/Feed/CalendarApiFeedType.php
+++ b/src/Feed/CalendarApiFeedType.php
@@ -461,9 +461,9 @@ private function parseBool(string|bool $value): bool
{
if (is_bool($value)) {
return $value;
- } else {
- return 'true' == strtolower($value);
}
+
+ return 'true' == strtolower($value);
}
private function getMapping(string $key): string
diff --git a/src/Feed/EventDatabaseApiV2FeedType.php b/src/Feed/EventDatabaseApiV2FeedType.php
index e24e002f7..3f552f571 100644
--- a/src/Feed/EventDatabaseApiV2FeedType.php
+++ b/src/Feed/EventDatabaseApiV2FeedType.php
@@ -165,9 +165,9 @@ public function getData(Feed $feed): array
// Fallback option is to return the cached data.
if ($cacheItem->isHit()) {
return $cacheItem->get();
- } else {
- return [];
}
+
+ return [];
}
/**
@@ -281,9 +281,9 @@ public function getConfigOptions(Request $request, FeedSource $feedSource, strin
} catch (\Exception) {
if ($cacheItem->isHit()) {
return $cacheItem->get();
- } else {
- return [];
}
+
+ return [];
}
} elseif ('subscription' === $name) {
$query = $request->query->all();
diff --git a/src/Feed/KobaFeedType.php b/src/Feed/KobaFeedType.php
index aab326081..db4bd10ec 100644
--- a/src/Feed/KobaFeedType.php
+++ b/src/Feed/KobaFeedType.php
@@ -88,9 +88,8 @@ public function getData(Feed $feed): array
if (true === $filterList) {
if (!str_contains($title, '(liste)')) {
continue;
- } else {
- $title = str_replace('(liste)', '', $title);
}
+ $title = str_replace('(liste)', '', $title);
}
// Apply booked title override. If enabled it changes the title to Optaget if it contains (optaget).
diff --git a/src/Repository/PlaylistSlideRepository.php b/src/Repository/PlaylistSlideRepository.php
index 8987f6797..a084f6a50 100644
--- a/src/Repository/PlaylistSlideRepository.php
+++ b/src/Repository/PlaylistSlideRepository.php
@@ -216,8 +216,8 @@ private function getWeight(Ulid $ulid): int
$playlistSlide = $queryBuilder->getQuery()->setMaxResults(1)->execute();
if (0 === count($playlistSlide)) {
return 0;
- } else {
- return $playlistSlide[0]->getWeight() + 1;
}
+
+ return $playlistSlide[0]->getWeight() + 1;
}
}
diff --git a/src/Service/RelationsChecksumCalculator.php b/src/Service/RelationsChecksumCalculator.php
new file mode 100644
index 000000000..2e76cf999
--- /dev/null
+++ b/src/Service/RelationsChecksumCalculator.php
@@ -0,0 +1,287 @@
+ slide ->
+ * playlist_slide -> playlist -> screen).
+ *
+ * Extracted from RelationsChecksumListener to allow reuse in console commands
+ * and other contexts outside Doctrine lifecycle events.
+ */
+class RelationsChecksumCalculator
+{
+ public const array CHECKSUM_TABLES = ['feed_source', 'feed', 'slide', 'media', 'theme', 'template', 'playlist_slide',
+ 'playlist', 'screen_campaign', 'screen', 'screen_group_campaign', 'screen_group',
+ 'playlist_screen_region', 'screen_layout_regions', 'screen_layout'];
+
+ public function __construct(
+ private readonly Connection $connection,
+ ) {}
+
+ /**
+ * Execute all checksum propagation queries in a transaction.
+ *
+ * @param bool $withWhereClause limit updates to rows where changed = 1
+ *
+ * @throws \Doctrine\DBAL\Exception
+ */
+ public function execute(bool $withWhereClause = true): void
+ {
+ $sqlQueries = $this->getUpdateRelationsAtQueries(withWhereClause: $withWhereClause);
+
+ $this->connection->beginTransaction();
+
+ try {
+ foreach ($sqlQueries as $sqlQuery) {
+ $stm = $this->connection->prepare($sqlQuery);
+ $stm->executeStatement();
+ }
+
+ $this->connection->commit();
+ } catch (\Exception $e) {
+ $this->connection->rollBack();
+ throw $e;
+ }
+ }
+
+ /**
+ * Get an array of SQL update statements to update the changed and relationsModified fields.
+ *
+ * @param bool $withWhereClause
+ * Should the statements include a where clause to limit the statement
+ *
+ * @return string[]
+ * Array of SQL statements
+ */
+ public function getUpdateRelationsAtQueries(bool $withWhereClause = true): array
+ {
+ // Set SQL update queries for the "relations checksum" fields on the parent (p), child (c) relationships up through the entity tree
+ $sqlQueries = [];
+
+ // Feed
+ $sqlQueries[] = $this->getToOneQuery(jsonKey: 'feedSource', parentTable: 'feed', childTable: 'feed_source', withWhereClause: $withWhereClause);
+ $sqlQueries[] = $this->getToOneQuery(jsonKey: 'slide', parentTable: 'feed', childTable: 'slide', parentTableId: 'id', childTableId: 'feed_id', withWhereClause: $withWhereClause);
+
+ // Slide
+ $sqlQueries[] = $this->getManyToManyQuery(jsonKey: 'media', parentTable: 'slide', pivotTable: 'slide_media', childTable: 'media', withWhereClause: $withWhereClause);
+ $sqlQueries[] = $this->getToOneQuery(jsonKey: 'theme', parentTable: 'slide', childTable: 'theme', withWhereClause: $withWhereClause);
+ $sqlQueries[] = $this->getToOneQuery(jsonKey: 'templateInfo', parentTable: 'slide', childTable: 'template', withWhereClause: $withWhereClause);
+ $sqlQueries[] = $this->getToOneQuery(jsonKey: 'feed', parentTable: 'slide', childTable: 'feed', withWhereClause: $withWhereClause);
+
+ // PlaylistSlide
+ $sqlQueries[] = $this->getToOneQuery(jsonKey: 'slide', parentTable: 'playlist_slide', childTable: 'slide', withWhereClause: $withWhereClause);
+
+ // Playlist
+ $sqlQueries[] = $this->getOneToManyQuery(jsonKey: 'slides', parentTable: 'playlist', childTable: 'playlist_slide', withWhereClause: $withWhereClause);
+
+ // ScreenCampaign
+ $sqlQueries[] = $this->getToOneQuery(jsonKey: 'campaign', parentTable: 'screen_campaign', childTable: 'playlist', parentTableId: 'campaign_id', withWhereClause: $withWhereClause);
+ $sqlQueries[] = $this->getToOneQuery(jsonKey: 'screen', parentTable: 'screen_campaign', childTable: 'screen', withWhereClause: $withWhereClause);
+
+ // ScreenGroupCampaign - campaign
+ $sqlQueries[] = $this->getToOneQuery(jsonKey: 'campaign', parentTable: 'screen_group_campaign', childTable: 'playlist', parentTableId: 'campaign_id', withWhereClause: $withWhereClause);
+
+ // ScreenGroup
+ $sqlQueries[] = $this->getManyToManyQuery(jsonKey: 'screens', parentTable: 'screen_group', pivotTable: 'screen_group_screen', childTable: 'screen', withWhereClause: $withWhereClause);
+ $sqlQueries[] = $this->getOneToManyQuery(jsonKey: 'screenGroupCampaigns', parentTable: 'screen_group', childTable: 'screen_group_campaign', withWhereClause: $withWhereClause);
+
+ // ScreenGroupCampaign - screenGroup
+ $sqlQueries[] = $this->getToOneQuery(jsonKey: 'screenGroup', parentTable: 'screen_group_campaign', childTable: 'screen_group', withWhereClause: $withWhereClause);
+
+ // PlaylistScreenRegion
+ $sqlQueries[] = $this->getToOneQuery(jsonKey: 'playlist', parentTable: 'playlist_screen_region', childTable: 'playlist', withWhereClause: $withWhereClause);
+
+ // ScreenLayoutRegions
+ $sqlQueries[] = $this->getToOneQuery(jsonKey: 'regions', parentTable: 'screen_layout_regions', childTable: 'playlist_screen_region', parentTableId: 'id', childTableId: 'region_id', withWhereClause: $withWhereClause);
+
+ // ScreenLayout
+ $sqlQueries[] = $this->getOneToManyQuery(jsonKey: 'regions', parentTable: 'screen_layout', childTable: 'screen_layout_regions', withWhereClause: $withWhereClause);
+
+ // Screen
+ $sqlQueries[] = $this->getOneToManyQuery(jsonKey: 'campaigns', parentTable: 'screen', childTable: 'screen_campaign', withWhereClause: $withWhereClause);
+ $sqlQueries[] = $this->getToOneQuery(jsonKey: 'layout', parentTable: 'screen', childTable: 'screen_layout', withWhereClause: $withWhereClause);
+ $sqlQueries[] = $this->getOneToManyQuery(jsonKey: 'regions', parentTable: 'screen', childTable: 'playlist_screen_region', withWhereClause: $withWhereClause);
+ $sqlQueries[] = $this->getManyToManyQuery(jsonKey: 'inScreenGroups', parentTable: 'screen', pivotTable: 'screen_group_screen', childTable: 'screen_group', withWhereClause: $withWhereClause);
+
+ // Add reset 'changed' fields queries
+ $sqlQueries = array_merge($sqlQueries, $this->getResetChangedQueries());
+
+ return $sqlQueries;
+ }
+
+ /**
+ * Get "One/ManyToOne" query.
+ *
+ * For a table (parent) that has a relation to another table (child) where we need to update the "relations_checksum"
+ * field on the parent with a checksum of values from the child we need to join the tables and set the values.
+ *
+ * Basically we do: "Update parent, join child, set parent value = SHA(child values)"
+ *
+ * Example:
+ * UPDATE slide p
+ * INNER JOIN theme c ON p.theme_id = c.id
+ * SET p.changed = 1,
+ * p.relations_checksum = JSON_SET(p.relations_checksum, "$.theme", SHA1(CONCAT(c.id, c.version, c.relations_checksum)))
+ * WHERE
+ * p.changed = 1
+ * OR c.changed = 1
+ *
+ * @param string|null $parentTableId
+ */
+ private function getToOneQuery(string $jsonKey, string $parentTable, string $childTable, ?string $parentTableId = null, string $childTableId = 'id', bool $withWhereClause = true): string
+ {
+ // Set the column name to use for "ON" in the Join clause. By default, the child table name with "_id" appended.
+ // E.g. "UPDATE feed p INNER JOIN feed_source c ON p.feed_source_id = c.id"
+ $parentTableId ??= $childTable.'_id';
+
+ // The base UPDATE query.
+ // - Use INNER JON to only select rows that have a match in both parent and child tables
+ // - Use JSON_SET to only INSERT/UPDATE the relevant key in the json object, not the whole field.
+ $queryFormat = '
+ UPDATE %s p
+ INNER JOIN %s c ON p.%s = c.%s
+ SET p.changed = 1,
+ p.relations_checksum = JSON_SET(p.relations_checksum, "$.%s", SHA1(CONCAT(c.id, c.version, c.relations_checksum)))
+ ';
+
+ $query = sprintf($queryFormat, $parentTable, $childTable, $parentTableId, $childTableId, $jsonKey);
+
+ // Add WHERE clause to only update rows that have been modified since ":modified_at"
+ if ($withWhereClause) {
+ $query .= ' WHERE p.changed = 1 OR c.changed = 1';
+ }
+
+ return $query;
+ }
+
+ /**
+ * Get "OnetoMany" query.
+ *
+ * For a table (parent) that has a toMany relationship to another table (child) where we need to update the "relations_checksum"
+ * field on the parent with a checksum of values from the child we need to join the tables and set the values.
+ *
+ * Example:
+ * UPDATE
+ * playlist p
+ * INNER JOIN (
+ * SELECT
+ * c.playlist_id,
+ * CAST(GROUP_CONCAT(DISTINCT c.changed SEPARATOR "") > 0 AS UNSIGNED) changed,
+ * SHA1(GROUP_CONCAT(c.id, c.version, c.relations_checksum)) checksum
+ * FROM
+ * playlist_slide c
+ * GROUP BY
+ * c.playlist_id
+ * ) temp ON p.id = temp.playlist_id
+ * SET p.changed = 1,
+ * p.relations_checksum = JSON_SET(p.relations_checksum, "$.slides", temp.checksum)
+ * WHERE p.changed = 1 OR temp.changed = 1
+ */
+ private function getOneToManyQuery(string $jsonKey, string $parentTable, string $childTable, bool $withWhereClause = true): string
+ {
+ $parentTableId = $parentTable.'_id';
+
+ $queryFormat = '
+ UPDATE
+ %s p
+ INNER JOIN (
+ SELECT
+ c.%s,
+ CAST(GROUP_CONCAT(DISTINCT c.changed SEPARATOR "") > 0 AS UNSIGNED) changed,
+ SHA1(GROUP_CONCAT(c.id, c.version, c.relations_checksum)) checksum
+ FROM
+ %s c
+ GROUP BY
+ c.%s
+ ) temp ON p.id = temp.%s
+ SET p.changed = 1,
+ p.relations_checksum = JSON_SET(p.relations_checksum, "$.%s", temp.checksum)
+ ';
+
+ $query = sprintf($queryFormat, $parentTable, $parentTableId, $childTable, $parentTableId, $parentTableId, $jsonKey);
+
+ if ($withWhereClause) {
+ $query .= ' WHERE p.changed = 1 OR temp.changed = 1';
+ }
+
+ return $query;
+ }
+
+ /**
+ * Get "many to many" query.
+ *
+ * For a table (parent) that has a relation to another table (child) through a pivot table where we need to update the "changed"
+ * and "relations_checksum" fields on the parent with values from the child we need to join the tables and set the values.
+ *
+ * Example:
+ * UPDATE
+ * slide p
+ * INNER JOIN (
+ * SELECT
+ * pivot.slide_id,
+ * CAST(GROUP_CONCAT(DISTINCT c.changed SEPARATOR "") > 0 AS UNSIGNED) changed,
+ * SHA1(GROUP_CONCAT(c.id, c.version, c.relations_checksum)) checksum
+ * FROM
+ * slide_media pivot
+ * INNER JOIN media c ON pivot.media_id = c.id
+ * GROUP BY
+ * pivot.slide_id
+ * ) temp ON p.id = temp.slide_id
+ * SET p.changed = 1,
+ * p.relations_checksum = JSON_SET(p.relations_checksum, "$.media", temp.checksum)
+ * WHERE p.changed = 1 OR temp.changed = 1
+ */
+ private function getManyToManyQuery(string $jsonKey, string $parentTable, string $pivotTable, string $childTable, bool $withWhereClause = true): string
+ {
+ $parentTableId = $parentTable.'_id';
+ $childTableId = $childTable.'_id';
+
+ $queryFormat = '
+ UPDATE
+ %s p
+ INNER JOIN (
+ SELECT
+ pivot.%s,
+ CAST(GROUP_CONCAT(DISTINCT c.changed SEPARATOR "") > 0 AS UNSIGNED) changed,
+ SHA1(GROUP_CONCAT(c.id, c.version, c.relations_checksum)) checksum
+ FROM
+ %s pivot
+ INNER JOIN %s c ON pivot.%s = c.id
+ GROUP BY
+ pivot.%s
+ ) temp ON p.id = temp.%s
+ SET p.changed = 1,
+ p.relations_checksum = JSON_SET(p.relations_checksum, "$.%s", temp.checksum)
+ ';
+
+ $query = sprintf($queryFormat, $parentTable, $parentTableId, $pivotTable, $childTable, $childTableId, $parentTableId, $parentTableId, $jsonKey);
+ if ($withWhereClause) {
+ $query .= ' WHERE p.changed = 1 OR temp.changed = 1';
+ }
+
+ return $query;
+ }
+
+ /**
+ * Get an array of queries to reset all "changed" fields to 0.
+ *
+ * @return string[]
+ */
+ private function getResetChangedQueries(): array
+ {
+ $queries = [];
+ foreach (self::CHECKSUM_TABLES as $table) {
+ $queries[] = sprintf('UPDATE %s SET changed = 0 WHERE changed = 1', $table);
+ }
+
+ return $queries;
+ }
+}
diff --git a/tests/EventListener/RelationsChecksumListenerTest.php b/tests/EventListener/RelationsChecksumListenerTest.php
index 56da30fe7..ab294d678 100644
--- a/tests/EventListener/RelationsChecksumListenerTest.php
+++ b/tests/EventListener/RelationsChecksumListenerTest.php
@@ -478,6 +478,137 @@ public function testPersistScreen(): void
$this->assertFalse($playlistScreenRegion->isChanged());
}
+ public function testAddScreenToScreenGroupUpdatesChecksum(): void
+ {
+ $tenant = $this->em->getRepository(Tenant::class)->findOneBy(['tenantKey' => 'ABC']);
+ /** @var Tenant\ScreenGroup $screenGroup */
+ $screenGroup = $this->em->getRepository(Tenant\ScreenGroup::class)->findOneBy(['tenant' => $tenant]);
+ $beforeChecksum = $screenGroup->getRelationsChecksum()['screens'];
+
+ // Create a new screen to add
+ $screenLayout = $this->em->getRepository(ScreenLayout::class)->findOneBy(['title' => 'Full screen']);
+ $screen = new Tenant\Screen();
+ $screen->setTenant($tenant);
+ $screen->setScreenLayout($screenLayout);
+ $screen->setCreatedBy(self::class.'::testAddScreenToScreenGroupUpdatesChecksum()');
+
+ $this->em->persist($screen);
+ $this->em->flush();
+
+ // Collection-only change on screen group — no scalar property change
+ $screenGroup->addScreen($screen);
+ $this->em->flush();
+
+ $this->em->refresh($screenGroup);
+ $this->assertNotEquals($beforeChecksum, $screenGroup->getRelationsChecksum()['screens']);
+ $this->assertFalse($screenGroup->isChanged());
+ }
+
+ public function testRemoveScreenFromScreenGroupUpdatesChecksum(): void
+ {
+ $tenant = $this->em->getRepository(Tenant::class)->findOneBy(['tenantKey' => 'ABC']);
+ /** @var Tenant\ScreenGroup $screenGroup */
+ $screenGroup = $this->em->getRepository(Tenant\ScreenGroup::class)->findOneBy(['tenant' => $tenant]);
+ $this->assertGreaterThan(0, $screenGroup->getScreens()->count());
+
+ $beforeChecksum = $screenGroup->getRelationsChecksum()['screens'];
+
+ // Collection-only change — remove a screen without changing scalar properties
+ $screen = $screenGroup->getScreens()->first();
+ $screenGroup->removeScreen($screen);
+ $this->em->flush();
+
+ $this->em->refresh($screenGroup);
+ $this->assertNotEquals($beforeChecksum, $screenGroup->getRelationsChecksum()['screens']);
+ $this->assertFalse($screenGroup->isChanged());
+ }
+
+ public function testAddMediaToSlideUpdatesChecksum(): void
+ {
+ $tenant = $this->em->getRepository(Tenant::class)->findOneBy(['tenantKey' => 'ABC']);
+ /** @var Tenant\Slide $slide */
+ $slide = $this->em->getRepository(Tenant\Slide::class)->findOneBy(['tenant' => $tenant]);
+ $beforeChecksum = $slide->getRelationsChecksum()['media'];
+
+ // Find a media not already on this slide
+ $allMedia = $this->em->getRepository(Tenant\Media::class)->findBy(['tenant' => $tenant]);
+ $existingMediaIds = $slide->getMedia()->map(fn ($m) => $m->getId())->toArray();
+ $newMedia = null;
+ foreach ($allMedia as $candidate) {
+ if (!in_array($candidate->getId(), $existingMediaIds, true)) {
+ $newMedia = $candidate;
+ break;
+ }
+ }
+ $this->assertNotNull($newMedia, 'No available media found for test');
+
+ // Collection-only change on slide — no scalar property change
+ $slide->addMedium($newMedia);
+ $this->em->flush();
+
+ $this->em->refresh($slide);
+ $this->assertNotEquals($beforeChecksum, $slide->getRelationsChecksum()['media']);
+ $this->assertFalse($slide->isChanged());
+ }
+
+ public function testRemoveMediaFromSlideUpdatesChecksum(): void
+ {
+ $tenant = $this->em->getRepository(Tenant::class)->findOneBy(['tenantKey' => 'ABC']);
+ /** @var Tenant\Slide $slide */
+ $slide = $this->em->getRepository(Tenant\Slide::class)->findOneBy(['tenant' => $tenant]);
+ $this->assertGreaterThan(0, $slide->getMedia()->count());
+
+ $beforeChecksum = $slide->getRelationsChecksum()['media'];
+
+ // Collection-only change — remove media without changing scalar properties
+ $media = $slide->getMedia()->first();
+ $slide->removeMedium($media);
+ $this->em->flush();
+
+ $this->em->refresh($slide);
+ $this->assertNotEquals($beforeChecksum, $slide->getRelationsChecksum()['media']);
+ $this->assertFalse($slide->isChanged());
+ }
+
+ public function testManyToManyChangePropagatesUpTree(): void
+ {
+ $tenant = $this->em->getRepository(Tenant::class)->findOneBy(['tenantKey' => 'ABC']);
+
+ /** @var Tenant\ScreenGroup $screenGroup */
+ $screenGroup = $this->em->getRepository(Tenant\ScreenGroup::class)->findOneBy(['tenant' => $tenant]);
+ $screenGroupBefore = $screenGroup->getRelationsChecksum()['screens'];
+
+ // Get a screen that belongs to this group — its inScreenGroups checksum should also change
+ /** @var Tenant\Screen $existingScreen */
+ $existingScreen = $screenGroup->getScreens()->first();
+ $this->assertNotNull($existingScreen);
+ $screenBefore = $existingScreen->getRelationsChecksum()['inScreenGroups'];
+
+ // Create a new screen to add to the group
+ $screenLayout = $this->em->getRepository(ScreenLayout::class)->findOneBy(['title' => 'Full screen']);
+ $newScreen = new Tenant\Screen();
+ $newScreen->setTenant($tenant);
+ $newScreen->setScreenLayout($screenLayout);
+ $newScreen->setCreatedBy(self::class.'::testManyToManyChangePropagatesUpTree()');
+
+ $this->em->persist($newScreen);
+ $this->em->flush();
+
+ // Collection-only change on screen group — triggers onFlush
+ $screenGroup->addScreen($newScreen);
+ $this->em->flush();
+
+ // Screen group checksum should update
+ $this->em->refresh($screenGroup);
+ $this->assertNotEquals($screenGroupBefore, $screenGroup->getRelationsChecksum()['screens']);
+ $this->assertFalse($screenGroup->isChanged());
+
+ // Existing screen's inScreenGroups checksum should propagate
+ $this->em->refresh($existingScreen);
+ $this->assertNotEquals($screenBefore, $existingScreen->getRelationsChecksum()['inScreenGroups']);
+ $this->assertFalse($existingScreen->isChanged());
+ }
+
public function testPlaylistSlideRelation(): void
{
$tenant = $this->em->getRepository(Tenant::class)->findOneBy(['tenantKey' => 'ABC']);