TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:24 +02:00
commit aad9daaefd
1506 changed files with 94005 additions and 0 deletions
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Form\Storage;
/**
* Workaround for form_definition.configuration being persisted through a
* native SQL JSON column (TCA type=json).
*
* Multi-value form elements (SingleSelect, MultiSelect, RadioButton,
* MultiCheckbox, ...) store their "options" as an associative map
* (value => label) keyed by option value. json_encode() of such a map
* produces a JSON *object*, and MySQL's native JSON column type does not
* guarantee that a JSON object's member order survives a write/read round
* trip. This is documented, spec-compliant behavior (RFC 8259: object
* member order "has no significance"). JSON *array* element order, by
* contrast, is reliably preserved by MySQL.
*
* MariaDB's JSON type is a plain LONGTEXT alias with a
* CHECK(JSON_VALID(...)) constraint, so it happens to preserve the exact
* text (and therefore object member order) byte-for-byte, which is why
* this only reproduces on real MySQL.
*
* Practical effect without this workaround: reordering a select element's
* options in the form editor visibly "works" right after saving, but
* reverts to the previous order on the next reload, once MySQL has
* renormalized the JSON object.
*
* protect() wraps every "options" map in an array-based structure before
* persisting, so the intended order survives via a JSON array instead of
* relying on object member order. restore() reverses this again after
* json_decode() on read, using the explicit order list rather than the
* member order MySQL happened to return the object in.
*
* Note: "options" is matched by key name rather than by resolving each
* renderable's prototype configuration for declared multi-value
* properties (as FormEditorController does for the editor UI). This is a
* deliberate simplification. It also protects option order inside
* variant overrides for free, without needing prototype/DI wiring in the
* persistence layer and is safe even where it over-applies, since
* protect()/restore() are lossless no-ops on any "options" map that
* doesn't need reordering.
*
* @internal
*/
final readonly class JsonObjectKeyOrderPreserver
{
private const string MARKER = '__jsonKeyOrderProtected';
public function protect(array $formDefinition): array
{
$output = $formDefinition;
foreach ($formDefinition as $key => $value) {
if (!is_array($value)) {
continue;
}
if ($key === 'options' && !array_is_list($value)) {
$output[$key] = [
self::MARKER => true,
'order' => array_keys($value),
'values' => $value,
];
continue;
}
$output[$key] = $this->protect($value);
}
return $output;
}
public function restore(array $formDefinition): array
{
$output = $formDefinition;
foreach ($formDefinition as $key => $value) {
if (!is_array($value)) {
continue;
}
if ($key === 'options' && ($value[self::MARKER] ?? false) === true) {
$order = $value['order'] ?? [];
$values = $value['values'] ?? [];
$restored = [];
foreach ($order as $optionKey) {
if (array_key_exists($optionKey, $values)) {
$restored[$optionKey] = $values[$optionKey];
unset($values[$optionKey]);
}
}
// Defensive fallback for entries the order list doesn't cover
// (should not normally happen, keeps old/foreign data intact).
$output[$key] = $restored + $values;
continue;
}
$output[$key] = $this->restore($value);
}
return $output;
}
}