Repository: symfony/ux
Pull request #1322 (2023-12-03 11:51:43)

[Autocomplete] Allow passing extra options to the autocomplete fields
| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| Issues        | n/a
| License       | MIT

This PR still needs some tweaks and tests, but I want to validate my idea. Currently, if we pass anything as a third argument in the `->add()` method, it is only used on the form render, and is fully ignored during the AJAX call.
![CleanShot 2023-12-03 at 12 36 59](https://github.com/symfony/ux/assets/80641364/28bc4b75-215c-4588-80b2-0db9eb82dcc6)

While implementing UX's Autocomplete in Sylius, I wanted to complete the following user story
```
Given I want to edit a taxon
When I want to assign a parent taxon to it
And I check a list of available taxa
Then I should see all taxa except the edited one
```

So basically, I have to pass a **current** taxon's ID to the autocomplete's query. As we know, currently it's not possible. So after contacting @weaverryan, I decided to implement a mechanism similar to the one from Live Components.

When you pass an array of options as a `3rd` argument, you can use a special `extra_options` key, which is an array consisting `scalars`/`arrays`/`nulls`.

Next, when the form is rendered, I get these values, calculate a checksum for them, then pass them through `json_encode` and `base64_encode` functions. In the end, I glue them to the `url` values in the `\Symfony\UX\Autocomplete\Form\AutocompleteChoiceTypeExtension::finishView` method.

So, basically with the following configuration:
![CleanShot 2023-12-03 at 12 48 51](https://github.com/symfony/ux/assets/80641364/e7b2585c-f052-4803-9473-a83930a4d1c8)

we end up with the following HTML code:
![CleanShot 2023-12-03 at 12 49 36](https://github.com/symfony/ux/assets/80641364/a173220f-19ed-4104-951c-0fee3469768e)

I decided to "glue" the `extra_options` to the URL, as I didn't have to deal with JS. Of course, I do not exclude a chance to refactor it, as a whole method should be refactored anyway.

Finally, the controller decodes the data, checks the checksum and passes the values to the autocomplete's form.

alexander-schranz: As I'm currently facing a similar issue in recreating some more complex Selection Type which is doing similar things like autocomplete. I'm going to share it also here. https://gist.github.com/alexander-schranz/b1cb8843bc54dc368001375fe6998e61#file-mycontactchoicetype-php-L39 In my case I did also go with providing a list of options, so the loading part will have possibility todo such things. But instead of duplicating the options I just provide a list of which options should be forwarded (string[], instead of array<string, mixed>`. So I don't have to duplicate the value of the option inside the extra_options, item_loader_options in my case.
alexander-schranz: I try currently some magic to find out automatically my `extra_options` without have the need to define them in my custom Selection Type:

```php
    public function buildView(FormView $view, FormInterface $form, array $options): void
    {
        $extraOptions = [];
        $formType = $form->getConfig()->getType();

        $innerType = $formType->getInnerType();
        $defaultOptionsResolver = new OptionsResolver();
        $innerType->configureOptions($defaultOptionsResolver);
        $definedOptions = $defaultOptionsResolver->getDefinedOptions();
        foreach ($defaultOptionsResolver->getMissingOptions() as $missingOption) {
            $defaultOptionsResolver->remove($missingOption);
        }
        // FromType, ChoiceType options which we keep always in mind and are important for form widget rendering e.g. required, disabled, attr, ...
        $definedOptions = \array_merge($definedOptions, $options['extra_options']);

        $defaultOptions = $defaultOptionsResolver->resolve([]);
        foreach ($options as $key => $value) {
            if (!\is_scalar($value)) {
                // ignore all options which are non-scalar values
                continue;
            }

            if (!\in_array($key, $definedOptions)) {
                // we ignore all options which were not defined specifically for the inner type
                continue;
            }

            if (isset($defaultOptions[$key]) && $defaultOptions[$key] === $value) {
                // we ignore all options which have the same value as the default value
                continue;
            }

            $extraOptions[$key] = $value;
        }

        $view->vars['extra_options'] = $extraOptions;
    }
```
smnandre: Do you want some help/feedback/other on this ?
jakubtobiasz: Hi @smnandre,
I've refactored one of two places I marked as "needs to be refactored". I have to write some functional tests for it, and... basically it will be ready to be fully reviewed.

I'll let you know once ready to be reviewed :D. 
smnandre: Great! Good luck then :) 
jakubtobiasz: I guess I haven't missed anything, and it's ready to be reviewed 👍🏻. AFAIR LiveComponent build failures are not related to my PR.
smnandre: Somewhere in the future documentation, should we add a small warning about the visibility of extra_options ? Something like "the given extra options can be retrieved and therefore should not contain sensible or personal information"
smnandre: Do you have a demo in mind (once its merged) ? 

I was thinking about it, and i think a "basic" demo would be great (with the kind of scenario you describe in the PR introduction).

But even greater, and showing how all the UX component can together create some smart interface, i'd suggest something that combine LiveComponent and Autocomplete+option.

Something like: 

![Frame 8](https://github.com/symfony/ux/assets/1359581/306a1988-2664-4a6f-a5aa-94274e7b5fcf)

What do you think ? Have you any other idea ? 
jakubtobiasz: FYI my solution seems to work. I set up a new Sylius branch locally with our new admin panel, and here're results :).

```php
final class TaxonTypeExtension extends AbstractTypeExtension
{
    /**
     * @param array<string, mixed> $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $taxonId = $builder->getData()->getId();

        $builder
            ->add('parent', TaxonAutocompleteChoiceType::class, [
                'label' => 'sylius.form.taxon.parent',
                'extra_options' => [
                    'excluded_taxons' => null !== $taxonId ? [$taxonId] : [],
                ],
            ])
        ;
    }

    public static function getExtendedTypes(): iterable
    {
        return [TaxonType::class];
    }
}
```
// ...
public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'class' => $this->entityClass,
            'searchable_fields' => ['translations.name'],
            'choice_label' => 'fullname',
            'query_builder' => function (Options $options) {
                return function (EntityRepository $er) use ($options) {
                    $qb = $er->createQueryBuilder('o');
                    $qb->andWhere('o.enabled = true');

                    $taxonsToBeExcluded = $options['extra_options']['excluded_taxons'] ?? [];
                    if ([] !== $taxonsToBeExcluded) {
                        $qb->andWhere($qb->expr()->notIn('o.id', $taxonsToBeExcluded));
                    }

                    return $qb;
                };
            }
        ]);
    }
// ...
```

![CleanShot 2023-12-27 at 15 30 58@2x](https://github.com/symfony/ux/assets/80641364/9218552b-2356-4138-a935-f600e4381909)


@smnandre about the demo... I wasn't thinking about it yet :D.
smnandre: > @smnandre about the demo... I wasn't thinking about it yet :D.

Oh it's not something required or asked in any way haha :)  Just to know if you had something in mind :)) 


weaverryan: Friendly ping :) - I'd love to get this across the finish line. Thanks!
jakubtobiasz: Hi @weaverryan,
I'll try to get back to it this weekend 😅.

Patch:
diff --git a/src/Autocomplete/CHANGELOG.md b/src/Autocomplete/CHANGELOG.md
index 905b13d4..39032dcc 100644
--- a/src/Autocomplete/CHANGELOG.md
+++ b/src/Autocomplete/CHANGELOG.md
@@ -11,6 +11,8 @@
 -   Added `tom-select/dist/css/tom-select.bootstrap4.css` to `autoimport` - this
     will cause this to appear in your `controllers.json` file by default, but disabled
     see.
+-   Allow passing `extra_options` key in an array passed as a `3rd` argument of the `->add()` method.
+    It will be used during the Ajax call to fetch results.
 
 ## 2.13.2
 
diff --git a/src/Autocomplete/src/Checksum/ChecksumCalculator.php b/src/Autocomplete/src/Checksum/ChecksumCalculator.php
new file mode 100644
index 00000000..d86ff570
--- /dev/null
+++ b/src/Autocomplete/src/Checksum/ChecksumCalculator.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\UX\Autocomplete\Checksum;
+
+/** @internal */
+class ChecksumCalculator
+{
+    public function __construct(private readonly string $secret)
+    {
+    }
+
+    public function calculateForArray(array $data): string
+    {
+        $this->sortKeysRecursively($data);
+
+        return base64_encode(hash_hmac('sha256', json_encode($data), $this->secret, true));
+    }
+
+    private function sortKeysRecursively(array &$data): void
+    {
+        foreach ($data as &$value) {
+            if (\is_array($value)) {
+                $this->sortKeysRecursively($value);
+            }
+        }
+        ksort($data);
+    }
+}
diff --git a/src/Autocomplete/src/Controller/EntityAutocompleteController.php b/src/Autocomplete/src/Controller/EntityAutocompleteController.php
index e65cb6da..6188ca21 100644
--- a/src/Autocomplete/src/Controller/EntityAutocompleteController.php
+++ b/src/Autocomplete/src/Controller/EntityAutocompleteController.php
@@ -14,20 +14,27 @@ namespace Symfony\UX\Autocomplete\Controller;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 use Symfony\UX\Autocomplete\AutocompleteResultsExecutor;
 use Symfony\UX\Autocomplete\AutocompleterRegistry;
+use Symfony\UX\Autocomplete\Checksum\ChecksumCalculator;
+use Symfony\UX\Autocomplete\Form\AutocompleteChoiceTypeExtension;
+use Symfony\UX\Autocomplete\OptionsAwareEntityAutocompleterInterface;
 
 /**
  * @author Ryan Weaver <ryan@symfonycasts.com>
  */
 final class EntityAutocompleteController
 {
+    public const EXTRA_OPTIONS = 'extra_options';
+
     public function __construct(
         private AutocompleterRegistry $autocompleteFieldRegistry,
         private AutocompleteResultsExecutor $autocompleteResultsExecutor,
         private UrlGeneratorInterface $urlGenerator,
+        private ChecksumCalculator $checksumCalculator,
     ) {
     }
 
@@ -38,6 +45,11 @@ final class EntityAutocompleteController
             throw new NotFoundHttpException(sprintf('No autocompleter found for "%s". Available autocompleters are: (%s)', $alias, implode(', ', $this->autocompleteFieldRegistry->getAutocompleterNames())));
         }
 
+        if ($autocompleter instanceof OptionsAwareEntityAutocompleterInterface) {
+            $extraOptions = $this->getExtraOptions($request);
+            $autocompleter->setOptions([self::EXTRA_OPTIONS => $extraOptions]);
+        }
+
         $page = $request->query->getInt('page', 1);
         $nextPage = null;
 
@@ -54,4 +66,48 @@ final class EntityAutocompleteController
             'next_page' => $nextPage,
         ]);
     }
+
+    /**
+     * @return array<string, scalar|array|null>
+     */
+    private function getExtraOptions(Request $request): array
+    {
+        if (!$request->query->has(self::EXTRA_OPTIONS)) {
+            return [];
+        }
+
+        $extraOptions = $this->getDecodedExtraOptions($request->query->getString(self::EXTRA_OPTIONS));
+
+        if (!\array_key_exists(AutocompleteChoiceTypeExtension::CHECKSUM_KEY, $extraOptions)) {
+            throw new BadRequestHttpException('The extra options are missing the checksum.');
+        }
+
+        $this->validateChecksum($extraOptions[AutocompleteChoiceTypeExtension::CHECKSUM_KEY], $extraOptions);
+
+        return $extraOptions;
+    }
+
+    /**
+     * @return array<string, scalar>
+     */
+    private function getDecodedExtraOptions(string $extraOptions): array
+    {
+        return json_decode(base64_decode($extraOptions), true, flags: \JSON_THROW_ON_ERROR);
+    }
+
+    /**
+     * @param array<string, scalar> $extraOptions
+     */
+    private function validateChecksum(string $checksum, array $extraOptions): void
+    {
+        $extraOptionsWithoutChecksum = array_filter(
+            $extraOptions,
+            fn (string $key) => AutocompleteChoiceTypeExtension::CHECKSUM_KEY !== $key,
+            \ARRAY_FILTER_USE_KEY,
+        );
+
+        if ($checksum !== $this->checksumCalculator->calculateForArray($extraOptionsWithoutChecksum)) {
+            throw new BadRequestHttpException('The extra options have been tampered with.');
+        }
+    }
 }
diff --git a/src/Autocomplete/src/DependencyInjection/AutocompleteExtension.php b/src/Autocomplete/src/DependencyInjection/AutocompleteExtension.php
index 859f1c4e..c44103c8 100644
--- a/src/Autocomplete/src/DependencyInjection/AutocompleteExtension.php
+++ b/src/Autocomplete/src/DependencyInjection/AutocompleteExtension.php
@@ -21,6 +21,7 @@ use Symfony\Component\Form\Form;
 use Symfony\Component\HttpKernel\DependencyInjection\Extension;
 use Symfony\UX\Autocomplete\AutocompleteResultsExecutor;
 use Symfony\UX\Autocomplete\AutocompleterRegistry;
+use Symfony\UX\Autocomplete\Checksum\ChecksumCalculator;
 use Symfony\UX\Autocomplete\Controller\EntityAutocompleteController;
 use Symfony\UX\Autocomplete\Doctrine\DoctrineRegistryWrapper;
 use Symfony\UX\Autocomplete\Doctrine\EntityMetadataFactory;
@@ -116,6 +117,7 @@ final class AutocompleteExtension extends Extension implements PrependExtensionI
                 new Reference('ux.autocomplete.autocompleter_registry'),
                 new Reference('ux.autocomplete.results_executor'),
                 new Reference('router'),
+                new Reference('ux.autocomplete.checksum_calculator'),
             ])
             ->addTag('controller.service_arguments')
         ;
@@ -127,6 +129,13 @@ final class AutocompleteExtension extends Extension implements PrependExtensionI
             ])
             ->addTag('maker.command')
         ;
+
+        $container
+            ->register('ux.autocomplete.checksum_calculator', ChecksumCalculator::class)
+            ->setArguments([
+                '%kernel.secret%',
+            ])
+        ;
     }
 
     private function registerFormServices(ContainerBuilder $container): void
@@ -149,6 +158,7 @@ final class AutocompleteExtension extends Extension implements PrependExtensionI
         $container
             ->register('ux.autocomplete.choice_type_extension', AutocompleteChoiceTypeExtension::class)
             ->setArguments([
+                new Reference('ux.autocomplete.checksum_calculator'),
                 new Reference('translator', ContainerInterface::IGNORE_ON_INVALID_REFERENCE),
             ])
             ->addTag('form.type_extension');
diff --git a/src/Autocomplete/src/Doctrine/EntitySearchUtil.php b/src/Autocomplete/src/Doctrine/EntitySearchUtil.php
index 44986dad..6f9c1e61 100644
--- a/src/Autocomplete/src/Doctrine/EntitySearchUtil.php
+++ b/src/Autocomplete/src/Doctrine/EntitySearchUtil.php
@@ -27,7 +27,7 @@ class EntitySearchUtil
     /**
      * Adapted from easycorp/easyadmin EntityRepository.
      */
-    public function addSearchClause(QueryBuilder $queryBuilder, string $query, string $entityClass, ?array $searchableProperties = null): void
+    public function addSearchClause(QueryBuilder $queryBuilder, string $query, string $entityClass, array $searchableProperties = null): void
     {
         $entityMetadata = $this->metadataFactory->create($entityClass);
 
diff --git a/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php b/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php
index d22dd043..9c40fd39 100644
--- a/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php
+++ b/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php
@@ -19,6 +19,7 @@ use Symfony\Component\Form\FormView;
 use Symfony\Component\OptionsResolver\Options;
 use Symfony\Component\OptionsResolver\OptionsResolver;
 use Symfony\Contracts\Translation\TranslatorInterface;
+use Symfony\UX\Autocomplete\Checksum\ChecksumCalculator;
 
 /**
  * Initializes the autocomplete Stimulus controller for any fields with the "autocomplete" option.
@@ -27,8 +28,12 @@ use Symfony\Contracts\Translation\TranslatorInterface;
  */
 final class AutocompleteChoiceTypeExtension extends AbstractTypeExtension
 {
-    public function __construct(private ?TranslatorInterface $translator = null)
-    {
+    public const CHECKSUM_KEY = '@checksum';
+
+    public function __construct(
+        private readonly ChecksumCalculator $checksumCalculator,
+        private readonly ?TranslatorInterface $translator = null,
+    ) {
     }
 
     public static function getExtendedTypes(): iterable
@@ -79,6 +84,10 @@ final class AutocompleteChoiceTypeExtension extends AbstractTypeExtension
             $values['min-characters'] = $options['min_characters'];
         }
 
+        if ($options['extra_options']) {
+            $values['url'] = $this->getUrlWithExtraOptions($values['url'], $options['extra_options']);
+        }
+
         $values['loading-more-text'] = $this->trans($options['loading_more_text']);
         $values['no-results-found-text'] = $this->trans($options['no_results_found_text']);
         $values['no-more-results-text'] = $this->trans($options['no_more_results_text']);
@@ -92,6 +101,41 @@ final class AutocompleteChoiceTypeExtension extends AbstractTypeExtension
         $view->vars['attr'] = $attr;
     }
 
+    private function getUrlWithExtraOptions(string $url, array $extraOptions): string
+    {
+        $this->validateExtraOptions($extraOptions);
+
+        $extraOptions[self::CHECKSUM_KEY] = $this->checksumCalculator->calculateForArray($extraOptions);
+        $extraOptions = base64_encode(json_encode($extraOptions));
+
+        return sprintf(
+            '%s%s%s',
+            $url,
+            $this->hasUrlParameters($url) ? '&' : '?',
+            http_build_query(['extra_options' => $extraOptions]),
+        );
+    }
+
+    private function hasUrlParameters(string $url): bool
+    {
+        $parsedUrl = parse_url($url);
+
+        return isset($parsedUrl['query']);
+    }
+
+    private function validateExtraOptions(array $extraOptions): void
+    {
+        foreach ($extraOptions as $optionKey => $option) {
+            if (!\is_scalar($option) && !\is_array($option) && null !== $option) {
+                throw new \InvalidArgumentException(sprintf('Extra option with key "%s" must be a scalar value, an array or null. Got "%s".', $optionKey, get_debug_type($option)));
+            }
+
+            if (\is_array($option)) {
+                $this->validateExtraOptions($option);
+            }
+        }
+    }
+
     public function configureOptions(OptionsResolver $resolver): void
     {
         $resolver->setDefaults([
@@ -106,6 +150,7 @@ final class AutocompleteChoiceTypeExtension extends AbstractTypeExtension
             'min_characters' => null,
             'max_results' => 10,
             'preload' => 'focus',
+            'extra_options' => [],
         ]);
 
         // if autocomplete_url is passed, then HTML options are already supported
diff --git a/src/Autocomplete/src/Form/ChoiceList/Loader/ExtraLazyChoiceLoader.php b/src/Autocomplete/src/Form/ChoiceList/Loader/ExtraLazyChoiceLoader.php
index dfa3b0b4..b0036bd6 100644
--- a/src/Autocomplete/src/Form/ChoiceList/Loader/ExtraLazyChoiceLoader.php
+++ b/src/Autocomplete/src/Form/ChoiceList/Loader/ExtraLazyChoiceLoader.php
@@ -27,12 +27,12 @@ class ExtraLazyChoiceLoader implements ChoiceLoaderInterface
     ) {
     }
 
-    public function loadChoiceList(?callable $value = null): ChoiceListInterface
+    public function loadChoiceList(callable $value = null): ChoiceListInterface
     {
         return $this->choiceList ??= new ArrayChoiceList([], $value);
     }
 
-    public function loadChoicesForValues(array $values, ?callable $value = null): array
+    public function loadChoicesForValues(array $values, callable $value = null): array
     {
         $choices = $this->decorated->loadChoicesForValues($values, $value);
         $this->choiceList = new ArrayChoiceList($choices, $value);
@@ -40,7 +40,7 @@ class ExtraLazyChoiceLoader implements ChoiceLoaderInterface
         return $choices;
     }
 
-    public function loadValuesForChoices(array $choices, ?callable $value = null): array
+    public function loadValuesForChoices(array $choices, callable $value = null): array
     {
         $values = $this->decorated->loadValuesForChoices($choices, $value);
         $this->loadChoicesForValues($values, $value);
diff --git a/src/Autocomplete/src/Form/WrappedEntityTypeAutocompleter.php b/src/Autocomplete/src/Form/WrappedEntityTypeAutocompleter.php
index 61bb2bd8..66615bda 100644
--- a/src/Autocomplete/src/Form/WrappedEntityTypeAutocompleter.php
+++ b/src/Autocomplete/src/Form/WrappedEntityTypeAutocompleter.php
@@ -22,17 +22,18 @@ use Symfony\Component\PropertyAccess\PropertyPathInterface;
 use Symfony\UX\Autocomplete\Doctrine\EntityMetadata;
 use Symfony\UX\Autocomplete\Doctrine\EntityMetadataFactory;
 use Symfony\UX\Autocomplete\Doctrine\EntitySearchUtil;
-use Symfony\UX\Autocomplete\EntityAutocompleterInterface;
+use Symfony\UX\Autocomplete\OptionsAwareEntityAutocompleterInterface;
 
 /**
  * An entity auto-completer that wraps a form type to get its information.
  *
  * @internal
  */
-final class WrappedEntityTypeAutocompleter implements EntityAutocompleterInterface
+final class WrappedEntityTypeAutocompleter implements OptionsAwareEntityAutocompleterInterface
 {
     private ?FormInterface $form = null;
     private ?EntityMetadata $entityMetadata = null;
+    private array $options = [];
 
     public function __construct(
         private string $formType,
@@ -139,7 +140,7 @@ final class WrappedEntityTypeAutocompleter implements EntityAutocompleterInterfa
     private function getForm(): FormInterface
     {
         if (null === $this->form) {
-            $this->form = $this->formFactory->create($this->formType);
+            $this->form = $this->formFactory->create($this->formType, options: $this->options);
         }
 
         return $this->form;
@@ -168,4 +169,13 @@ final class WrappedEntityTypeAutocompleter implements EntityAutocompleterInterfa
 
         return $this->entityMetadata;
     }
+
+    public function setOptions(array $options): void
+    {
+        if (null !== $this->form) {
+            throw new \LogicException('The options can only be set before the form is created.');
+        }
+
+        $this->options = $options;
+    }
 }
diff --git a/src/Autocomplete/src/OptionsAwareEntityAutocompleterInterface.php b/src/Autocomplete/src/OptionsAwareEntityAutocompleterInterface.php
new file mode 100644
index 00000000..0a0b7875
--- /dev/null
+++ b/src/Autocomplete/src/OptionsAwareEntityAutocompleterInterface.php
@@ -0,0 +1,20 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\UX\Autocomplete;
+
+/**
+ * Interface for classes that will have an "autocomplete" endpoint exposed with a possibility to pass additional form options.
+ */
+interface OptionsAwareEntityAutocompleterInterface extends EntityAutocompleterInterface
+{
+    public function setOptions(array $options): void;
+}
diff --git a/src/LazyImage/src/BlurHash/BlurHash.php b/src/LazyImage/src/BlurHash/BlurHash.php
index 26092225..1a1c965b 100644
--- a/src/LazyImage/src/BlurHash/BlurHash.php
+++ b/src/LazyImage/src/BlurHash/BlurHash.php
@@ -23,7 +23,7 @@ class BlurHash implements BlurHashInterface
 {
     private $imageManager;
 
-    public function __construct(?ImageManager $imageManager = null)
+    public function __construct(ImageManager $imageManager = null)
     {
         $this->imageManager = $imageManager;
     }
diff --git a/src/LiveComponent/src/Attribute/AsLiveComponent.php b/src/LiveComponent/src/Attribute/AsLiveComponent.php
index 7a3acde0..991b6045 100644
--- a/src/LiveComponent/src/Attribute/AsLiveComponent.php
+++ b/src/LiveComponent/src/Attribute/AsLiveComponent.php
@@ -37,8 +37,8 @@ final class AsLiveComponent extends AsTwigComponent
      * @param int         $urlReferenceType  Which type of URL should be generated for the given route. Use the constants from UrlGeneratorInterface (default: absolute path, e.g. "/dir/file").
      */
     public function __construct(
-        ?string $name = null,
-        ?string $template = null,
+        string $name = null,
+        string $template = null,
         private ?string $defaultAction = null,
         bool $exposePublicProps = true,
         string $attributesVar = 'attributes',
diff --git a/src/LiveComponent/src/Attribute/LiveProp.php b/src/LiveComponent/src/Attribute/LiveProp.php
index aaf081a6..90afc678 100644
--- a/src/LiveComponent/src/Attribute/LiveProp.php
+++ b/src/LiveComponent/src/Attribute/LiveProp.php
@@ -96,7 +96,7 @@ final class LiveProp
          *
          * @var string|string[]|null
          */
-        private string|array|null $onUpdated = null,
+        private null|string|array $onUpdated = null,
 
         /**
          * If true, this property will be synchronized with a query parameter
@@ -193,7 +193,7 @@ final class LiveProp
         return $this->updateFromParent;
     }
 
-    public function onUpdated(): string|array|null
+    public function onUpdated(): null|string|array
     {
         return $this->onUpdated;
     }
diff --git a/src/LiveComponent/src/ComponentToolsTrait.php b/src/LiveComponent/src/ComponentToolsTrait.php
index 808084a6..30ff8ce4 100644
--- a/src/LiveComponent/src/ComponentToolsTrait.php
+++ b/src/LiveComponent/src/ComponentToolsTrait.php
@@ -33,12 +33,12 @@ trait ComponentToolsTrait
         $this->liveResponder = $liveResponder;
     }
 
-    public function emit(string $eventName, array $data = [], ?string $componentName = null): void
+    public function emit(string $eventName, array $data = [], string $componentName = null): void
     {
         $this->liveResponder->emit($eventName, $data, $componentName);
     }
 
-    public function emitUp(string $eventName, array $data = [], ?string $componentName = null): void
+    public function emitUp(string $eventName, array $data = [], string $componentName = null): void
     {
         $this->liveResponder->emitUp($eventName, $data, $componentName);
     }
diff --git a/src/LiveComponent/src/LiveResponder.php b/src/LiveComponent/src/LiveResponder.php
index ad97fa79..4a7caca5 100644
--- a/src/LiveComponent/src/LiveResponder.php
+++ b/src/LiveComponent/src/LiveResponder.php
@@ -28,7 +28,7 @@ final class LiveResponder
      */
     private array $browserEventsToDispatch = [];
 
-    public function emit(string $eventName, array $data = [], ?string $componentName = null): void
+    public function emit(string $eventName, array $data = [], string $componentName = null): void
     {
         $this->eventsToEmit[] = [
             'event' => $eventName,
@@ -38,7 +38,7 @@ final class LiveResponder
         ];
     }
 
-    public function emitUp(string $eventName, array $data = [], ?string $componentName = null): void
+    public function emitUp(string $eventName, array $data = [], string $componentName = null): void
     {
         $this->eventsToEmit[] = [
             'event' => $eventName,
diff --git a/src/LiveComponent/src/Metadata/LivePropMetadata.php b/src/LiveComponent/src/Metadata/LivePropMetadata.php
index 1c6ba98a..896edd3a 100644
--- a/src/LiveComponent/src/Metadata/LivePropMetadata.php
+++ b/src/LiveComponent/src/Metadata/LivePropMetadata.php
@@ -112,7 +112,7 @@ final class LivePropMetadata
         return $this->liveProp->format();
     }
 
-    public function onUpdated(): string|array|null
+    public function onUpdated(): null|string|array
     {
         return $this->liveProp->onUpdated();
     }
diff --git a/src/LiveComponent/src/Twig/DeterministicTwigIdCalculator.php b/src/LiveComponent/src/Twig/DeterministicTwigIdCalculator.php
index 2952e3b4..233e13b4 100644
--- a/src/LiveComponent/src/Twig/DeterministicTwigIdCalculator.php
+++ b/src/LiveComponent/src/Twig/DeterministicTwigIdCalculator.php
@@ -41,7 +41,7 @@ class DeterministicTwigIdCalculator
      * @param bool        $increment Whether to increment the counter for this file+line
      * @param string|null $key       An optional key to use instead of the incremented counter
      */
-    public function calculateDeterministicId(bool $increment = true, ?string $key = null): string
+    public function calculateDeterministicId(bool $increment = true, string $key = null): string
     {
         $lineData = $this->guessTemplateInfo();
 
diff --git a/src/LiveComponent/src/Twig/TemplateCacheWarmer.php b/src/LiveComponent/src/Twig/TemplateCacheWarmer.php
index 18b33be2..4f150004 100644
--- a/src/LiveComponent/src/Twig/TemplateCacheWarmer.php
+++ b/src/LiveComponent/src/Twig/TemplateCacheWarmer.php
@@ -26,7 +26,7 @@ final class TemplateCacheWarmer implements CacheWarmerInterface
     {
     }
 
-    public function warmUp(string $cacheDir, ?string $buildDir = null): array
+    public function warmUp(string $cacheDir, string $buildDir = null): array
     {
         $map = [];
         foreach ($this->templateIterator as $item) {
diff --git a/src/LiveComponent/src/Util/LiveControllerAttributesCreator.php b/src/LiveComponent/src/Util/LiveControllerAttributesCreator.php
index 81a55a2f..29f25dd3 100644
--- a/src/LiveComponent/src/Util/LiveControllerAttributesCreator.php
+++ b/src/LiveComponent/src/Util/LiveControllerAttributesCreator.php
@@ -56,7 +56,7 @@ class LiveControllerAttributesCreator
      * Calculates the array of extra attributes that should be added to the root
      * component element to activate the live controller functionality.
      */
-    public function attributesForRendering(MountedComponent $mounted, ComponentMetadata $metadata, bool $isChildComponent, ?string $deterministicId = null): LiveAttributesCollection
+    public function attributesForRendering(MountedComponent $mounted, ComponentMetadata $metadata, bool $isChildComponent, string $deterministicId = null): LiveAttributesCollection
     {
         $attributesCollection = $this->attributeHelper->create();
         $attributesCollection->setLiveController($mounted->getName());
diff --git a/src/StimulusBundle/CHANGELOG.md b/src/StimulusBundle/CHANGELOG.md
index dc18f652..d3c9157b 100644
--- a/src/StimulusBundle/CHANGELOG.md
+++ b/src/StimulusBundle/CHANGELOG.md
@@ -1,9 +1,5 @@
 # CHANGELOG
 
-## 2.14.2
-
--   Fix bug with finding UX Packages with non-standard project structure
-
 ## 2.14.1
 
 -   Fixed bug with Stimulus controllers in subdirectories on Windows
diff --git a/src/StimulusBundle/src/Dto/StimulusAttributes.php b/src/StimulusBundle/src/Dto/StimulusAttributes.php
index f65ebb7d..71291b7d 100644
--- a/src/StimulusBundle/src/Dto/StimulusAttributes.php
+++ b/src/StimulusBundle/src/Dto/StimulusAttributes.php
@@ -70,7 +70,7 @@ class StimulusAttributes implements \Stringable, \IteratorAggregate
     /**
      * @param array $parameters Parameters to pass to the action. Optional.
      */
-    public function addAction(string $controllerName, string $actionName, ?string $eventName = null, array $parameters = []): void
+    public function addAction(string $controllerName, string $actionName, string $eventName = null, array $parameters = []): void
     {
         $controllerName = $this->normalizeControllerName($controllerName);
         $this->actions[] = [
@@ -90,7 +90,7 @@ class StimulusAttributes implements \Stringable, \IteratorAggregate
      * @param string      $controllerName the Stimulus controller name
      * @param string|null $targetNames    The space-separated list of target names if a string is passed to the 1st argument. Optional.
      */
-    public function addTarget(string $controllerName, ?string $targetNames = null): void
+    public function addTarget(string $controllerName, string $targetNames = null): void
     {
         if (null === $targetNames) {
             return;
diff --git a/src/StimulusBundle/src/Twig/StimulusTwigExtension.php b/src/StimulusBundle/src/Twig/StimulusTwigExtension.php
index e1a72a75..b2d90f2f 100644
--- a/src/StimulusBundle/src/Twig/StimulusTwigExtension.php
+++ b/src/StimulusBundle/src/Twig/StimulusTwigExtension.php
@@ -70,7 +70,7 @@ final class StimulusTwigExtension extends AbstractExtension
     /**
      * @param array $parameters Parameters to pass to the action. Optional.
      */
-    public function renderStimulusAction(string $controllerName, ?string $actionName = null, ?string $eventName = null, array $parameters = []): StimulusAttributes
+    public function renderStimulusAction(string $controllerName, string $actionName = null, string $eventName = null, array $parameters = []): StimulusAttributes
     {
         $stimulusAttributes = $this->stimulusHelper->createStimulusAttributes();
         $stimulusAttributes->addAction($controllerName, $actionName, $eventName, $parameters);
@@ -81,7 +81,7 @@ final class StimulusTwigExtension extends AbstractExtension
     /**
      * @param array $parameters Parameters to pass to the action. Optional.
      */
-    public function appendStimulusAction(StimulusAttributes $stimulusAttributes, string $controllerName, string $actionName, ?string $eventName = null, array $parameters = []): StimulusAttributes
+    public function appendStimulusAction(StimulusAttributes $stimulusAttributes, string $controllerName, string $actionName, string $eventName = null, array $parameters = []): StimulusAttributes
     {
         $stimulusAttributes->addAction($controllerName, $actionName, $eventName, $parameters);
 
@@ -92,7 +92,7 @@ final class StimulusTwigExtension extends AbstractExtension
      * @param string      $controllerName the Stimulus controller name
      * @param string|null $targetNames    The space-separated list of target names if a string is passed to the 1st argument. Optional.
      */
-    public function renderStimulusTarget(string $controllerName, ?string $targetNames = null): StimulusAttributes
+    public function renderStimulusTarget(string $controllerName, string $targetNames = null): StimulusAttributes
     {
         $stimulusAttributes = $this->stimulusHelper->createStimulusAttributes();
         $stimulusAttributes->addTarget($controllerName, $targetNames);
@@ -104,7 +104,7 @@ final class StimulusTwigExtension extends AbstractExtension
      * @param string      $controllerName the Stimulus controller name
      * @param string|null $targetNames    The space-separated list of target names if a string is passed to the 1st argument. Optional.
      */
-    public function appendStimulusTarget(StimulusAttributes $stimulusAttributes, string $controllerName, ?string $targetNames = null): StimulusAttributes
+    public function appendStimulusTarget(StimulusAttributes $stimulusAttributes, string $controllerName, string $targetNames = null): StimulusAttributes
     {
         $stimulusAttributes->addTarget($controllerName, $targetNames);
 
diff --git a/src/StimulusBundle/src/Ux/UxPackageReader.php b/src/StimulusBundle/src/Ux/UxPackageReader.php
index 03256936..ea31f69d 100644
--- a/src/StimulusBundle/src/Ux/UxPackageReader.php
+++ b/src/StimulusBundle/src/Ux/UxPackageReader.php
@@ -11,8 +11,6 @@
 
 namespace Symfony\UX\StimulusBundle\Ux;
 
-use Composer\InstalledVersions;
-
 /**
  * @internal
  *
@@ -30,12 +28,7 @@ class UxPackageReader
     {
         // remove the '@' from the name to get back to the PHP package name
         $phpPackageName = substr($packageName, 1);
-        if (class_exists(InstalledVersions::class) && InstalledVersions::isInstalled($phpPackageName)) {
-            $phpPackagePath = InstalledVersions::getInstallPath($phpPackageName);
-        } else {
-            $phpPackagePath = $this->projectDir.'/vendor/'.$phpPackageName;
-        }
-
+        $phpPackagePath = $this->projectDir.'/vendor/'.$phpPackageName;
         if (!is_dir($phpPackagePath)) {
             throw new \RuntimeException(sprintf('Could not find package "%s" referred to from controllers.json.', $phpPackageName));
         }
diff --git a/src/Translator/src/CacheWarmer/TranslationsCacheWarmer.php b/src/Translator/src/CacheWarmer/TranslationsCacheWarmer.php
index 24117723..f2e17b1c 100644
--- a/src/Translator/src/CacheWarmer/TranslationsCacheWarmer.php
+++ b/src/Translator/src/CacheWarmer/TranslationsCacheWarmer.php
@@ -33,7 +33,7 @@ class TranslationsCacheWarmer implements CacheWarmerInterface
         return true;
     }
 
-    public function warmUp(string $cacheDir, ?string $buildDir = null): array
+    public function warmUp(string $cacheDir, string $buildDir = null): array
     {
         $this->translationsDumper->dump(
             ...$this->translatorBag->getCatalogues()
diff --git a/src/Turbo/CHANGELOG.md b/src/Turbo/CHANGELOG.md
index 73e1fb0c..ff5a7151 100644
--- a/src/Turbo/CHANGELOG.md
+++ b/src/Turbo/CHANGELOG.md
@@ -1,9 +1,5 @@
 # CHANGELOG
 
-## 2.14.2
-
--   Fix using old `ClassUtils` class that's not used in newer versions of Doctrine
-
 ## 2.13.2
 
 -   Revert "Change JavaScript package to `type: module`"
diff --git a/src/Turbo/assets/package.json b/src/Turbo/assets/package.json
index 48404365..84570a7b 100644
--- a/src/Turbo/assets/package.json
+++ b/src/Turbo/assets/package.json
@@ -21,7 +21,7 @@
             }
         },
         "importmap": {
-            "@hotwired/turbo": "^7.1.0",
+            "@hotwired/turbo": "^7.0.1",
             "@hotwired/stimulus": "^3.0.0"
         }
     },
diff --git a/src/Turbo/src/Bridge/Mercure/Broadcaster.php b/src/Turbo/src/Bridge/Mercure/Broadcaster.php
index e727a127..9fa18117 100644
--- a/src/Turbo/src/Bridge/Mercure/Broadcaster.php
+++ b/src/Turbo/src/Bridge/Mercure/Broadcaster.php
@@ -11,11 +11,12 @@
 
 namespace Symfony\UX\Turbo\Bridge\Mercure;
 
+use Doctrine\Common\Util\ClassUtils;
 use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
 use Symfony\Component\Mercure\HubInterface;
 use Symfony\Component\Mercure\Update;
+use Symfony\Component\VarExporter\LazyObjectInterface;
 use Symfony\UX\Turbo\Broadcaster\BroadcasterInterface;
-use Symfony\UX\Turbo\Doctrine\ClassUtil;
 
 /**
  * Broadcasts updates rendered using Twig with Mercure.
@@ -62,7 +63,14 @@ final class Broadcaster implements BroadcasterInterface
             return;
         }
 
-        $entityClass = ClassUtil::getEntityClass($entity);
+        if ($entity instanceof LazyObjectInterface) {
+            $entityClass = get_parent_class($entity);
+            if (false === $entityClass) {
+                throw new \LogicException('Parent class missing');
+            }
+        } else {
+            $entityClass = ClassUtils::getClass($entity);
+        }
 
         if (!isset($options['rendered_action'])) {
             throw new \InvalidArgumentException(sprintf('Cannot broadcast entity of class "%s" as option "rendered_action" is missing.', $entityClass));
diff --git a/src/Turbo/src/Broadcaster/BroadcasterInterface.php b/src/Turbo/src/Broadcaster/BroadcasterInterface.php
index 7bff8a1e..8a692b79 100644
--- a/src/Turbo/src/Broadcaster/BroadcasterInterface.php
+++ b/src/Turbo/src/Broadcaster/BroadcasterInterface.php
@@ -19,7 +19,7 @@ namespace Symfony\UX\Turbo\Broadcaster;
 interface BroadcasterInterface
 {
     /**
-     * @param array{id?: string|string[], transports?: string|string[], topics?: string|string[], template?: string, rendered_action?: string, private?: bool, sse_id?: string, sse_type?: string, sse_retry?: int} $options
+     * @param array{id?: string|string[], transports?: string|string[], topics?: string|string[], template?: string, rendered_action?: string, private?: boolean, sse_id?: string, sse_type?: string, sse_retry?: int} $options
      */
     public function broadcast(object $entity, string $action, array $options): void;
 }
diff --git a/src/Turbo/src/Broadcaster/IdAccessor.php b/src/Turbo/src/Broadcaster/IdAccessor.php
index 64606947..8e959979 100644
--- a/src/Turbo/src/Broadcaster/IdAccessor.php
+++ b/src/Turbo/src/Broadcaster/IdAccessor.php
@@ -20,7 +20,7 @@ class IdAccessor
     private $propertyAccessor;
     private $doctrine;
 
-    public function __construct(?PropertyAccessorInterface $propertyAccessor = null, ?ManagerRegistry $doctrine = null)
+    public function __construct(PropertyAccessorInterface $propertyAccessor = null, ManagerRegistry $doctrine = null)
     {
         $this->propertyAccessor = $propertyAccessor ?? (class_exists(PropertyAccess::class) ? PropertyAccess::createPropertyAccessor() : null);
         $this->doctrine = $doctrine;
diff --git a/src/Turbo/src/Broadcaster/TwigBroadcaster.php b/src/Turbo/src/Broadcaster/TwigBroadcaster.php
index 05a428cd..1c3b85c2 100644
--- a/src/Turbo/src/Broadcaster/TwigBroadcaster.php
+++ b/src/Turbo/src/Broadcaster/TwigBroadcaster.php
@@ -11,7 +11,8 @@
 
 namespace Symfony\UX\Turbo\Broadcaster;
 
-use Symfony\UX\Turbo\Doctrine\ClassUtil;
+use Doctrine\Common\Util\ClassUtils;
+use Symfony\Component\VarExporter\LazyObjectInterface;
 use Twig\Environment;
 
 /**
@@ -29,7 +30,7 @@ final class TwigBroadcaster implements BroadcasterInterface
     /**
      * @param array<string, string> $templatePrefixes
      */
-    public function __construct(BroadcasterInterface $broadcaster, Environment $twig, array $templatePrefixes = [], ?IdAccessor $idAccessor = null)
+    public function __construct(BroadcasterInterface $broadcaster, Environment $twig, array $templatePrefixes = [], IdAccessor $idAccessor = null)
     {
         $this->broadcaster = $broadcaster;
         $this->twig = $twig;
@@ -43,7 +44,15 @@ final class TwigBroadcaster implements BroadcasterInterface
             $options['id'] = $id;
         }
 
-        $class = ClassUtil::getEntityClass($entity);
+        // handle proxies (both styles)
+        if ($entity instanceof LazyObjectInterface) {
+            $class = get_parent_class($entity);
+            if (false === $class) {
+                throw new \LogicException('Parent class missing');
+            }
+        } else {
+            $class = ClassUtils::getClass($entity);
+        }
 
         if (null === $template = $options['template'] ?? null) {
             $template = $class;
diff --git a/src/Turbo/src/Doctrine/BroadcastListener.php b/src/Turbo/src/Doctrine/BroadcastListener.php
index 9298b117..af99a144 100644
--- a/src/Turbo/src/Doctrine/BroadcastListener.php
+++ b/src/Turbo/src/Doctrine/BroadcastListener.php
@@ -13,9 +13,11 @@ namespace Symfony\UX\Turbo\Doctrine;
 
 use Doctrine\Common\Annotations\Reader;
 use Doctrine\Common\EventArgs;
+use Doctrine\Common\Util\ClassUtils;
 use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\Event\OnFlushEventArgs;
 use Doctrine\ORM\Event\PostFlushEventArgs;
+use Symfony\Component\VarExporter\LazyObjectInterface;
 use Symfony\Contracts\Service\ResetInterface;
 use Symfony\UX\Turbo\Attribute\Broadcast;
 use Symfony\UX\Turbo\Broadcaster\BroadcasterInterface;
@@ -48,7 +50,7 @@ final class BroadcastListener implements ResetInterface
      */
     private $removedEntities;
 
-    public function __construct(BroadcasterInterface $broadcaster, ?Reader $annotationReader = null)
+    public function __construct(BroadcasterInterface $broadcaster, Reader $annotationReader = null)
     {
         $this->reset();
 
@@ -126,13 +128,21 @@ final class BroadcastListener implements ResetInterface
 
     private function storeEntitiesToPublish(EntityManagerInterface $em, object $entity, string $property): void
     {
-        $class = ClassUtil::getEntityClass($entity);
+        // handle proxies (both styles)
+        if ($entity instanceof LazyObjectInterface) {
+            $class = get_parent_class($entity);
+            if (false === $class) {
+                throw new \LogicException('Parent class missing');
+            }
+        } else {
+            $class = ClassUtils::getClass($entity);
+        }
 
         if (!isset($this->broadcastedClasses[$class])) {
             $this->broadcastedClasses[$class] = [];
             $r = null;
 
-            if ($options = ($r = new \ReflectionClass($class))->getAttributes(Broadcast::class)) {
+            if (\PHP_VERSION_ID >= 80000 && $options = ($r = new \ReflectionClass($class))->getAttributes(Broadcast::class)) {
                 foreach ($options as $option) {
                     $this->broadcastedClasses[$class][] = $option->newInstance()->options;
                 }
diff --git a/src/Turbo/src/Doctrine/ClassUtil.php b/src/Turbo/src/Doctrine/ClassUtil.php
deleted file mode 100644
index cfd27836..00000000
--- a/src/Turbo/src/Doctrine/ClassUtil.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-
-/*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\UX\Turbo\Doctrine;
-
-use Doctrine\Common\Util\ClassUtils as LegacyClassUtils;
-use Symfony\Component\VarExporter\LazyObjectInterface;
-
-/**
- * @internal
- */
-final class ClassUtil
-{
-    public static function getEntityClass(object $entity): string
-    {
-        if ($entity instanceof LazyObjectInterface) {
-            $entityClass = get_parent_class($entity);
-            if (false === $entityClass) {
-                throw new \LogicException('Parent class missing');
-            }
-
-            return $entityClass;
-        }
-
-        // @legacy for old versions of Doctrine
-        if (class_exists(LegacyClassUtils::class)) {
-            return LegacyClassUtils::getClass($entity);
-        }
-
-        return $entity::class;
-    }
-}
diff --git a/src/Turbo/src/Twig/TwigExtension.php b/src/Turbo/src/Twig/TwigExtension.php
index e23024ed..27cc5db9 100644
--- a/src/Turbo/src/Twig/TwigExtension.php
+++ b/src/Turbo/src/Twig/TwigExtension.php
@@ -38,7 +38,7 @@ final class TwigExtension extends AbstractExtension
     /**
      * @param object|string $topic
      */
-    public function turboStreamListen(Environment $env, $topic, ?string $transport = null): string
+    public function turboStreamListen(Environment $env, $topic, string $transport = null): string
     {
         $transport = $transport ?? $this->default;
 
diff --git a/src/TwigComponent/CHANGELOG.md b/src/TwigComponent/CHANGELOG.md
index df794181..50f1e358 100644
--- a/src/TwigComponent/CHANGELOG.md
+++ b/src/TwigComponent/CHANGELOG.md
@@ -5,7 +5,6 @@
 -   Make `ComponentAttributes` traversable/countable
 -   Fixed lexing some `{# twig comments #}` with HTML Twig syntax
 -   Fix various usages of deprecated Twig code
--   Add attribute rendering system
 
 ## 2.13.0
 
diff --git a/src/TwigComponent/doc/index.rst b/src/TwigComponent/doc/index.rst
index 72e93491..61c61c33 100644
--- a/src/TwigComponent/doc/index.rst
+++ b/src/TwigComponent/doc/index.rst
@@ -745,45 +745,6 @@ When overriding the ``alert_message`` block, you have access to the ``message``
         </twig:block>
     </twig:SuccessAlert>
 
-.. versionadded:: 2.13
-
-    The ability to refer to the scope of higher components via the ``outerScope`` variable was added in 2.13.
-
-As mentioned before, variables from lower components are merged with those from upper components. When you need
-access to some properties or functions from higher components, that can be done via the ``outerScope...`` variable:
-
-.. code-block:: twig
-
-    {# templates/SuccessAlert.html.twig #}
-    {% set name = 'Fabien' %}
-    {% set message = 'Hello' %}
-    {% component Alert with { type: 'success', name: 'Bart' } %}
-        Hello {{ name }} {# Hello Bart #}
-
-        {{ message }} {{ outerScope.name }} {# Hello Fabien #}
-
-        {{ outerScope.this.someFunction }} {# this refers to SuccessAlert #}
-
-        {{ outerScope.this.someProp }} {# references a "someProp" prop from SuccessAlert #}
-    {% endcomponent %}
-
-You can keep referring to components higher up as well. Just add another ``outerScope``.
-Remember though that the ``outerScope`` reference only starts once you're INSIDE the (embedded) component.
-
-.. code-block:: twig
-
-    {# templates/FancyProfileCard.html.twig #}
-    {% component Card %}
-        {% block header %}
-            {% component Alert with { message: outerScope.this.someProp } %} {# not yet INSIDE the Alert template #}
-                {% block content %}
-                    {{ message }} {# same value as below, indirectly refers to FancyProfileCard::someProp #}
-                    {{ outerScope.outerScope.this.someProp }} {# directly refers to FancyProfileCard::someProp #}
-                {% endblock %}
-            {% endcomponent %}
-        {% endblock %}
-    {% endcomponent %}
-
 Inheritance & Forwarding "Outer Blocks"
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -945,79 +906,6 @@ the exception of *class*. For ``class``, the defaults are prepended:
     {# renders as: #}
     <button class="bar foo" type="submit">Save</button>
 
-Render
-~~~~~~
-
-.. versionadded:: 2.15
-
-    The ability to *render* attributes was added in TwigComponents 2.15.
-
-You can take full control over the attributes that are rendered by using the
-``render()`` method.
-
-.. code-block:: html+twig
-
-    {# templates/components/MyComponent.html.twig #}
-    <div
-      style="{{ attributes.render('style') }} display:block;"
-      {{ attributes }} {# be sure to always render the remaining attributes! #}
-    >
-      My Component!
-    </div>
-
-    {# render component #}
-    {{ component('MyComponent', { style: 'color:red;' }) }}
-
-    {# renders as: #}
-    <div style="color:red; display:block;">
-      My Component!
-    </div>
-
-.. caution::
-
-    There are a few important things to know about using ``render()``:
-
-    1. You need to be sure to call your ``render()`` methods before calling ``{{ attributes }}`` or some
-       attributes could be rendered twice. For instance:
-
-            .. code-block:: html+twig
-
-                {# templates/components/MyComponent.html.twig #}
-                <div
-                    {{ attributes }} {# called before style is rendered #}
-                    style="{{ attributes.render('style') }} display:block;"
-                >
-                    My Component!
-                </div>
-
-                {# render component #}
-                {{ component('MyComponent', { style: 'color:red;' }) }}
-
-                {# renders as: #}
-                <div style="color:red;" style="color:red; display:block;"> {# style is rendered twice! #}
-                    My Component!
-                </div>
-
-    2. If you add an attribute without calling ``render()``, it will be rendered twice. For instance:
-
-         .. code-block:: html+twig
-
-              {# templates/components/MyComponent.html.twig #}
-              <div
-                 style="display:block;" {# not calling attributes.render('style') #}
-                 {{ attributes }}
-              >
-                 My Component!
-              </div>
-
-              {# render component #}
-              {{ component('MyComponent', { style: 'color:red;' }) }}
-
-              {# renders as: #}
-              <div style="display:block;" style="color:red;"> {# style is rendered twice! #}
-                 My Component!
-              </div>
-
 Only
 ~~~~
 
@@ -1355,6 +1243,399 @@ controls how components are named and where their templates live:
 If a component class matches multiple namespaces, the first matched will
 be used.
 
+.. _embedded-components:
+
+Passing Blocks to Components
+----------------------------
+
+.. tip::
+
+    The `Component HTML Syntax`_ allows you to pass blocks to components in an
+    even more readable way.
+
+You can write your component's Twig template with blocks that can be overridden
+when rendering using the ``{% component %}`` syntax. These blocks can be thought of as
+*slots* which you may be familiar with from Vue. The ``component`` tag is very
+similar to Twig's native `embed tag`_.
+
+Consider a data table component. You pass it headers and rows but can expose
+blocks for the cells and an optional footer:
+
+.. code-block:: html+twig
+
+    {# templates/components/DataTable.html.twig #}
+    <div{{ attributes.defaults({class: 'data-table'}) }}>
+        <table>
+            <thead>
+                <tr>
+                    {% for header in this.headers %}
+                        <th class="{% block th_class %}data-table-header{% endblock %}">
+                            {{ header }}
+                        </th>
+                    {% endfor %}
+                </tr>
+            </thead>
+            <tbody>
+                {% for row in this.data %}
+                    <tr>
+                        {% for cell in row %}
+                            <td class="{% block td_class %}data-table-cell{% endblock %}">
+                                {{ cell }}
+                            </td>
+                        {% endfor %}
+                    </tr>
+                {% endfor %}
+            </tbody>
+        </table>
+        {% block footer %}{% endblock %}
+    </div>
+
+When rendering, you can override the ``th_class``, ``td_class``, and ``footer`` blocks.
+The ``with`` data is what's mounted on the component object.
+
+.. code-block:: html+twig
+
+    {# templates/some_page.html.twig #}
+    {% component DataTable with {headers: ['key', 'value'], data: [[1, 2], [3, 4]]} %}
+        {% block th_class %}{{ parent() }} text-bold{% endblock %}
+
+        {% block td_class %}{{ parent() }} text-italic{% endblock %}
+
+        {% block footer %}
+            <div class="data-table-footer">
+                My footer
+            </div>
+        {% endblock %}
+    {% endcomponent %}
+
+.. versionadded:: 2.11
+
+    The ``{% component %}`` syntax can also be used with LiveComponents since 2.11.
+    However, there are some caveats related to the context between parent and child
+    components during re-rending. Read `Passing Blocks to Live Components`_.
+
+Inheritance & Forwarding "Outer Blocks"
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 2.10
+
+    The ``outerBlocks`` variable was added in 2.10.
+
+The content inside a ``{% component ... %}`` tag should be viewed as living in
+its own, independent template, which extends the component's template. This means that
+any blocks that live in the "outer" template are not available inside the ``{% component %}`` tag.
+However, a special ``outerBlocks`` variable is added as a way to refer to those blocks:
+
+.. code-block:: html+twig
+
+  {% extends 'base.html.twig' %}
+
+  {% block call_to_action %}<strong>Attention! Free Puppies!</strong>{% endblock %}
+
+  {% block body %}
+    {% component Alert %}
+      {% block content %}{{ block(outerBlocks.call_to_action) }}{% endblock %}
+    {% endcomponent %}
+  {% endblock %}
+
+The ``outerBlocks`` variable becomes specially useful with nested components. For example,
+imagine we want to create a ``SuccessAlert`` component that's usable like this:
+
+.. code-block:: html+twig
+
+    {# templates/some_page.html.twig #}
+    {% component SuccessAlert %}
+        {% block content %}We will successfully <em>forward</em> this block content!{% endblock %}
+    {% endcomponent %}
+
+But we already have a generic ``Alert`` component, and we want to re-use it:
+
+.. code-block:: html+twig
+
+    {# templates/Alert.html.twig #}
+    <div class="alert alert-{{ type }}">
+        {% block content %}{% endblock %}
+    </div>
+
+To do this, the ``SuccessAlert`` component can grab the ``content`` block that's passed to it
+via the ``outerBlocks`` variable and forward it into ``Alert``:
+
+.. code-block:: twig
+
+    {# templates/SuccessAlert.html.twig #}
+    {% component Alert with { type: 'success' } %}
+        {% block content %}{{ block(outerBlocks.content) }}{% endblock %}
+    {% endcomponent %}
+
+Note that to pass a block multiple components down, each component needs to pass it.
+
+Context / Variables Inside of Blocks
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The content inside of the ``{% component ... %}`` should be viewed as living in its own,
+independent template, which extends the component's template. This has a few interesting consequences.
+
+First, once you're inside of ``{% component ... %}``, the ``this`` variable represents
+the component you're now rendering *and* you have access to all of that component's variables:
+
+.. code-block:: twig
+
+    {# templates/SuccessAlert.html.twig #}
+    {{ this.someFunction }} {# this refers to SuccessAlert #}
+
+    {% component Alert with { type: 'success' } %}
+        {% block content %}
+            {{ this.someFunction }} {# this refers to Alert! #}
+
+            {{ type }} {# references a "type" prop from Alert #}
+        {% endblock %}
+    {% endcomponent %}
+
+Conveniently, in addition to the variables from the ``Alert`` component, you still have
+access to whatever variables are available in the original template:
+
+.. code-block:: twig
+
+    {# templates/SuccessAlert.html.twig #}
+    {% set name = 'Fabien' %}
+    {% component Alert with { type: 'success' } %}
+        {% block content %}
+            Hello {{ name }}
+        {% endblock %}
+    {% endcomponent %}
+
+Note that ALL variables from upper components (e.g. ``SuccessAlert``) are available to lower
+components (e.g. ``Alert``). However, because variables are merged, variables with the same name
+are overridden by lower components (that's also why ``this`` refers to the embedded, or
+"current" component).
+
+The most interesting thing is that the content inside of ``{% component ... %}`` is
+executed as if it is "copy-and-pasted" into the block of the target template. This means
+you can access variables from the block you're overriding! For example:
+
+.. code-block:: twig
+
+    {# templates/SuccessAlert.html.twig #}
+    {% for message in messages %}
+        {% block alert_message %}
+            A default {{ message }}
+        {% endblock %}
+    {% endfor %}
+
+When overriding the ``alert_message`` block, you have access to the ``message`` variable:
+
+.. code-block:: twig
+
+    {# templates/some_page.html.twig #}
+    {% component SuccessAlert %}
+        {% block alert_message %}
+            I can override the alert_message block and access the {{ message }} too!
+        {% endblock %}
+    {% endcomponent %}
+
+
+.. versionadded:: 2.13
+
+    The ability to refer to the scope of higher components via the ``outerScope`` variable was added in 2.13.
+
+As mentioned before, variables from lower components are merged with those from upper components. When you need
+access to some properties or functions from higher components, that can be done via the ``outerScope...`` variable:
+
+.. code-block:: twig
+
+    {# templates/SuccessAlert.html.twig #}
+    {% set name = 'Fabien' %}
+    {% set message = 'Hello' %}
+    {% component Alert with { type: 'success', name: 'Bart' } %}
+        Hello {{ name }} {# Hello Bart #}
+
+        {{ message }} {{ outerScope.name }} {# Hello Fabien #}
+
+        {{ outerScope.this.someFunction }} {# this refers to SuccessAlert #}
+
+        {{ outerScope.this.someProp }} {# references a "someProp" prop from SuccessAlert #}
+    {% endcomponent %}
+
+You can keep referring to components higher up as well. Just add another ``outerScope``.
+Remember though that the ``outerScope`` reference only starts once you're INSIDE the (embedded) component.
+
+.. code-block:: twig
+
+    {# templates/FancyProfileCard.html.twig #}
+    {% component Card %}
+        {% block header %}
+            {% component Alert with { message: outerScope.this.someProp } %} {# not yet INSIDE the Alert template #}
+                {% block content %}
+                    {{ message }} {# same value as below, indirectly refers to FancyProfileCard::someProp #}
+                    {{ outerScope.outerScope.this.someProp }} {# directly refers to FancyProfileCard::someProp #}
+                {% endblock %}
+            {% endcomponent %}
+        {% endblock %}
+    {% endcomponent %}
+
+Component HTML Syntax
+---------------------
+
+.. versionadded:: 2.8
+
+    This syntax was been introduced in 2.8 and is still experimental: it may change in the future.
+
+Twig Components come with an HTML-like syntax to ease the readability of your template:
+
+.. code-block:: html+twig
+
+    <twig:Alert></twig:Alert>
+    // or use a self-closing tag
+    <twig:Alert />
+
+Passing Props as HTML Attributes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Passing props is done with HTML attributes. For example if you have this component::
+
+    #[AsTwigComponent]
+    class Alert
+    {
+        public string $message = '';
+        public bool $withActions = false;
+        public string $type = 'success';
+    }
+
+You can pass the ``message``, ``withActions`` or ``type`` props as attributes:
+
+.. code-block:: html+twig
+
+    // withActions will be set to true
+    <twig:Alert type="info" message="hello!" withActions />
+
+To pass in a dynamic value, prefix the attribute with ``:`` or use the
+normal ``{{ }}`` syntax:
+
+.. code-block:: html+twig
+
+    <twig:Alert message="hello!" :user="user.id" />
+
+    // equal to
+    <twig:Alert message="hello!" user="{{ user.id }}" />
+
+    // and pass object, or table, or anything you imagine
+    <twig:Alert :foo="['col' => ['foo', 'oof']]" />
+
+To forward attributes to another component, use `{{...}}` spread operator syntax.
+This requires Twig 3.7.0 or higher:
+
+.. code-block:: html+twig
+
+    <twig:Alert{{ ...myAttributes }} />
+
+.. _passing-blocks:
+
+Passing Content (Blocks) to Components
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can also pass content directly to your component:
+
+.. code-block:: html+twig
+
+    <twig:Alert type="success">
+        <div>Congratulations! You've won a free puppy!</div>
+    </twig:Alert>
+
+In your component template, this becomes a block named ``content``:
+
+.. code-block:: html+twig
+
+    <div class="alert alert-{{ type }}">
+        {% block content %}
+            // the content will appear in here
+        {% endblock %}
+     </div>
+
+In addition to the default block, you can also add named blocks:
+
+.. code-block:: html+twig
+
+    <twig:Alert type="success">
+        <div>Congrats on winning a free puppy!</div>
+
+        <twig:block name="footer">
+            <button class="btn btn-primary">Claim your prize</button>
+        </twig:block>
+    </twig:Alert>
+
+And in your component template you can access your embedded block
+
+.. code-block:: html+twig
+
+    <div class="alert alert-{{ type }}">
+        {% block content %}{% endblock %}
+        {% block footer %}{% endblock %}
+     </div>
+
+Anonymous Components
+--------------------
+
+Sometimes a component is simple enough that it doesn't have any complex logic or injected services.
+In this case, you can skip the class and only create the template. The component name is determined
+by the location of the template (see `Twig Template Namespaces`_):
+
+.. code-block:: html+twig
+
+    {# templates/components/Button/Primary.html.twig #}
+    <button {{ attributes.defaults({ class: 'primary' }) }}>
+        {% block content %}{% endblock %}
+    </button>
+
+Then use your component with ``:`` to navigate through sub-directories (if there are any):
+
+.. code-block:: html+twig
+
+    {# index.html.twig #}
+    ...
+    <div>
+       <twig:Button:Primary>Click Me!</twig:Button:Primary>
+    </div>
+
+    {# renders as: #}
+    <button class="primary">Click Me!</button>
+
+Like normal, you can pass extra attributes that will be rendered on the element:
+
+.. code-block:: html+twig
+
+    {# index.html.twig #}
+    ...
+    <div>
+       <twig:Button:Primary type="button" name="foo">Click Me!</twig:Button:Primary>
+    </div>
+
+    {# renders as: #}
+    <button class="primary" type="button" name="foo">Click Me!</button>
+
+You can also pass a variable (prop) into your template:
+
+.. code-block:: html+twig
+
+    {# index.html.twig #}
+    ...
+    <div>
+        <twig:Button icon="fa-plus" type="primary" role="button">Click Me!</twig:Button>
+    </div>
+
+To tell the system that ``icon`` and ``type`` are props and not attributes, use the ``{% props %}`` tag at the top of your template.
+
+.. code-block:: html+twig
+
+    {# templates/components/Button.html.twig #}
+    {% props icon = null, type = 'primary' %}
+
+    <button {{ attributes.defaults({ class: 'btn btn-'~type }) }}>
+        {% block content %}{% endblock %}
+        {% if icon %}
+            <span class="fa-solid fa-{{ icon }}"></span>
+        {% endif %}
+    </button>
+
 Debugging Components
 --------------------
 
diff --git a/src/TwigComponent/src/Command/TwigComponentDebugCommand.php b/src/TwigComponent/src/Command/TwigComponentDebugCommand.php
index 89a97b42..131de17e 100644
--- a/src/TwigComponent/src/Command/TwigComponentDebugCommand.php
+++ b/src/TwigComponent/src/Command/TwigComponentDebugCommand.php
@@ -38,7 +38,7 @@ class TwigComponentDebugCommand extends Command
         private ComponentFactory $componentFactory,
         private Environment $twig,
         private readonly array $componentClassMap,
-        ?string $anonymousDirectory = null,
+        string $anonymousDirectory = null,
     ) {
         parent::__construct();
         $this->anonymousDirectory = $anonymousDirectory ?? 'components';
diff --git a/src/TwigComponent/src/ComponentAttributes.php b/src/TwigComponent/src/ComponentAttributes.php
index 8b7d10a0..8606a1c8 100644
--- a/src/TwigComponent/src/ComponentAttributes.php
+++ b/src/TwigComponent/src/ComponentAttributes.php
@@ -21,9 +21,6 @@ use Symfony\WebpackEncoreBundle\Dto\AbstractStimulusDto;
  */
 final class ComponentAttributes implements \IteratorAggregate, \Countable
 {
-    /** @var array<string,true> */
-    private array $rendered = [];
-
     /**
      * @param array<string, string|bool> $attributes
      */
@@ -34,10 +31,7 @@ final class ComponentAttributes implements \IteratorAggregate, \Countable
     public function __toString(): string
     {
         return array_reduce(
-            array_filter(
-                array_keys($this->attributes),
-                fn (string $key) => !isset($this->rendered[$key])
-            ),
+            array_keys($this->attributes),
             function (string $carry, string $key) {
                 $value = $this->attributes[$key];
 
@@ -60,26 +54,6 @@ final class ComponentAttributes implements \IteratorAggregate, \Countable
         );
     }
 
-    public function __clone(): void
-    {
-        $this->rendered = [];
-    }
-
-    public function render(string $attribute): ?string
-    {
-        if (null === $value = $this->attributes[$attribute] ?? null) {
-            return null;
-        }
-
-        if (!\is_string($value)) {
-            throw new \LogicException(sprintf('Can only get string attributes (%s is a %s).', $attribute, get_debug_type($value)));
-        }
-
-        $this->rendered[$attribute] = true;
-
-        return $value;
-    }
-
     /**
      * @return array<string, string|bool>
      */
@@ -115,10 +89,6 @@ final class ComponentAttributes implements \IteratorAggregate, \Countable
             $attributes[$key] = $value;
         }
 
-        foreach (array_keys($this->rendered) as $attribute) {
-            unset($attributes[$attribute]);
-        }
-
         return new self($attributes);
     }
 
diff --git a/src/TwigComponent/src/DataCollector/TwigComponentDataCollector.php b/src/TwigComponent/src/DataCollector/TwigComponentDataCollector.php
index f0c8aaf4..874bb7bf 100644
--- a/src/TwigComponent/src/DataCollector/TwigComponentDataCollector.php
+++ b/src/TwigComponent/src/DataCollector/TwigComponentDataCollector.php
@@ -37,7 +37,7 @@ class TwigComponentDataCollector extends AbstractDataCollector implements LateDa
         $this->hasStub = class_exists(ClassStub::class);
     }
 
-    public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
+    public function collect(Request $request, Response $response, \Throwable $exception = null): void
     {
     }
 
diff --git a/src/TwigComponent/src/Event/PreRenderEvent.php b/src/TwigComponent/src/Event/PreRenderEvent.php
index efd44cca..43545a50 100644
--- a/src/TwigComponent/src/Event/PreRenderEvent.php
+++ b/src/TwigComponent/src/Event/PreRenderEvent.php
@@ -60,7 +60,7 @@ final class PreRenderEvent extends Event
     /**
      * Change the twig template used.
      */
-    public function setTemplate(string $template, ?int $index = null): self
+    public function setTemplate(string $template, int $index = null): self
     {
         $this->template = $template;
         $this->templateIndex = $index;
diff --git a/src/TwigComponent/src/Twig/PropsNode.php b/src/TwigComponent/src/Twig/PropsNode.php
index 2a5bc5bb..26592854 100644
--- a/src/TwigComponent/src/Twig/PropsNode.php
+++ b/src/TwigComponent/src/Twig/PropsNode.php
@@ -21,7 +21,7 @@ use Twig\Node\Node;
  */
 class PropsNode extends Node
 {
-    public function __construct(array $propsNames, array $values, $lineno = 0, ?string $tag = null)
+    public function __construct(array $propsNames, array $values, $lineno = 0, string $tag = null)
     {
         parent::__construct($values, ['names' => $propsNames], $lineno, $tag);
     }
diff --git a/src/TwigComponent/src/Twig/TwigPreLexer.php b/src/TwigComponent/src/Twig/TwigPreLexer.php
index 16d1f897..74db5c49 100644
--- a/src/TwigComponent/src/Twig/TwigPreLexer.php
+++ b/src/TwigComponent/src/Twig/TwigPreLexer.php
@@ -197,7 +197,7 @@ class TwigPreLexer
         return $output;
     }
 
-    private function consumeComponentName(?string $customExceptionMessage = null): string
+    private function consumeComponentName(string $customExceptionMessage = null): string
     {
         $start = $this->position;
         while ($this->position < $this->length && preg_match('/[A-Za-z0-9_:@\-.]/', $this->input[$this->position])) {
diff --git a/ux.symfony.com/composer.lock b/ux.symfony.com/composer.lock
index 7ea39b6f..ec560fd2 100644
--- a/ux.symfony.com/composer.lock
+++ b/ux.symfony.com/composer.lock
@@ -517,16 +517,16 @@
         },
         {
             "name": "doctrine/dbal",
-            "version": "3.8.1",
+            "version": "3.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/dbal.git",
-                "reference": "c9ea252cdce4da324ede3d6c5913dd89f769afd2"
+                "reference": "d244f2e6e6bf32bff5174e6729b57214923ecec9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/dbal/zipball/c9ea252cdce4da324ede3d6c5913dd89f769afd2",
-                "reference": "c9ea252cdce4da324ede3d6c5913dd89f769afd2",
+                "url": "https://api.github.com/repos/doctrine/dbal/zipball/d244f2e6e6bf32bff5174e6729b57214923ecec9",
+                "reference": "d244f2e6e6bf32bff5174e6729b57214923ecec9",
                 "shasum": ""
             },
             "require": {
@@ -542,9 +542,9 @@
                 "doctrine/coding-standard": "12.0.0",
                 "fig/log-test": "^1",
                 "jetbrains/phpstorm-stubs": "2023.1",
-                "phpstan/phpstan": "1.10.57",
+                "phpstan/phpstan": "1.10.56",
                 "phpstan/phpstan-strict-rules": "^1.5",
-                "phpunit/phpunit": "9.6.16",
+                "phpunit/phpunit": "9.6.15",
                 "psalm/plugin-phpunit": "0.18.4",
                 "slevomat/coding-standard": "8.13.1",
                 "squizlabs/php_codesniffer": "3.8.1",
@@ -610,7 +610,7 @@
             ],
             "support": {
                 "issues": "https://github.com/doctrine/dbal/issues",
-                "source": "https://github.com/doctrine/dbal/tree/3.8.1"
+                "source": "https://github.com/doctrine/dbal/tree/3.8.0"
             },
             "funding": [
                 {
@@ -626,7 +626,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-03T17:33:49+00:00"
+            "time": "2024-01-25T21:44:02+00:00"
         },
         {
             "name": "doctrine/deprecations",
@@ -1139,27 +1139,28 @@
         },
         {
             "name": "doctrine/lexer",
-            "version": "3.0.1",
+            "version": "2.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/lexer.git",
-                "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd"
+                "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd",
-                "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd",
+                "url": "https://api.github.com/repos/doctrine/lexer/zipball/39ab8fcf5a51ce4b85ca97c7a7d033eb12831124",
+                "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124",
                 "shasum": ""
             },
             "require": {
-                "php": "^8.1"
+                "doctrine/deprecations": "^1.0",
+                "php": "^7.1 || ^8.0"
             },
             "require-dev": {
-                "doctrine/coding-standard": "^12",
-                "phpstan/phpstan": "^1.10",
-                "phpunit/phpunit": "^10.5",
+                "doctrine/coding-standard": "^9 || ^10",
+                "phpstan/phpstan": "^1.3",
+                "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
                 "psalm/plugin-phpunit": "^0.18.3",
-                "vimeo/psalm": "^5.21"
+                "vimeo/psalm": "^4.11 || ^5.0"
             },
             "type": "library",
             "autoload": {
@@ -1196,7 +1197,7 @@
             ],
             "support": {
                 "issues": "https://github.com/doctrine/lexer/issues",
-                "source": "https://github.com/doctrine/lexer/tree/3.0.1"
+                "source": "https://github.com/doctrine/lexer/tree/2.1.0"
             },
             "funding": [
                 {
@@ -1212,7 +1213,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-05T11:56:58+00:00"
+            "time": "2022-12-14T08:49:07+00:00"
         },
         {
             "name": "doctrine/migrations",
@@ -1318,16 +1319,16 @@
         },
         {
             "name": "doctrine/orm",
-            "version": "2.18.0",
+            "version": "2.17.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/orm.git",
-                "reference": "f2176a9ce56cafdfd1624d54bfdb076819083d5b"
+                "reference": "ccfc97c32f63aaa0988ac6aa42e71c5590bb794d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/orm/zipball/f2176a9ce56cafdfd1624d54bfdb076819083d5b",
-                "reference": "f2176a9ce56cafdfd1624d54bfdb076819083d5b",
+                "url": "https://api.github.com/repos/doctrine/orm/zipball/ccfc97c32f63aaa0988ac6aa42e71c5590bb794d",
+                "reference": "ccfc97c32f63aaa0988ac6aa42e71c5590bb794d",
                 "shasum": ""
             },
             "require": {
@@ -1340,7 +1341,7 @@
                 "doctrine/event-manager": "^1.2 || ^2",
                 "doctrine/inflector": "^1.4 || ^2.0",
                 "doctrine/instantiator": "^1.3 || ^2",
-                "doctrine/lexer": "^2 || ^3",
+                "doctrine/lexer": "^2",
                 "doctrine/persistence": "^2.4 || ^3",
                 "ext-ctype": "*",
                 "php": "^7.1 || ^8.0",
@@ -1376,7 +1377,7 @@
             "type": "library",
             "autoload": {
                 "psr-4": {
-                    "Doctrine\\ORM\\": "src"
+                    "Doctrine\\ORM\\": "lib/Doctrine/ORM"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
@@ -1413,9 +1414,9 @@
             ],
             "support": {
                 "issues": "https://github.com/doctrine/orm/issues",
-                "source": "https://github.com/doctrine/orm/tree/2.18.0"
+                "source": "https://github.com/doctrine/orm/tree/2.17.4"
             },
-            "time": "2024-01-31T15:53:12+00:00"
+            "time": "2024-01-26T19:41:16+00:00"
         },
         {
             "name": "doctrine/persistence",
@@ -2035,16 +2036,16 @@
         },
         {
             "name": "league/commonmark",
-            "version": "2.4.2",
+            "version": "2.4.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/commonmark.git",
-                "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf"
+                "reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/91c24291965bd6d7c46c46a12ba7492f83b1cadf",
-                "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf",
+                "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/3669d6d5f7a47a93c08ddff335e6d945481a1dd5",
+                "reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5",
                 "shasum": ""
             },
             "require": {
@@ -2057,7 +2058,7 @@
             },
             "require-dev": {
                 "cebe/markdown": "^1.0",
-                "commonmark/cmark": "0.30.3",
+                "commonmark/cmark": "0.30.0",
                 "commonmark/commonmark.js": "0.30.0",
                 "composer/package-versions-deprecated": "^1.8",
                 "embed/embed": "^4.4",
@@ -2067,10 +2068,10 @@
                 "michelf/php-markdown": "^1.4 || ^2.0",
                 "nyholm/psr7": "^1.5",
                 "phpstan/phpstan": "^1.8.2",
-                "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0",
+                "phpunit/phpunit": "^9.5.21",
                 "scrutinizer/ocular": "^1.8.1",
-                "symfony/finder": "^5.3 | ^6.0 || ^7.0",
-                "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.0",
+                "symfony/finder": "^5.3 | ^6.0",
+                "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0",
                 "unleashedtech/php-coding-standard": "^3.1.1",
                 "vimeo/psalm": "^4.24.0 || ^5.0.0"
             },
@@ -2137,7 +2138,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-02T11:59:32+00:00"
+            "time": "2023-08-30T16:55:00+00:00"
         },
         {
             "name": "league/config",
@@ -4261,16 +4262,16 @@
         },
         {
             "name": "symfony/flex",
-            "version": "v2.4.4",
+            "version": "v2.4.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/flex.git",
-                "reference": "bec213c39511eda66663baa2ee7440c65f89c695"
+                "reference": "6b44ac75c7f07f48159ec36c2d21ef8cf48a21b1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/flex/zipball/bec213c39511eda66663baa2ee7440c65f89c695",
-                "reference": "bec213c39511eda66663baa2ee7440c65f89c695",
+                "url": "https://api.github.com/repos/symfony/flex/zipball/6b44ac75c7f07f48159ec36c2d21ef8cf48a21b1",
+                "reference": "6b44ac75c7f07f48159ec36c2d21ef8cf48a21b1",
                 "shasum": ""
             },
             "require": {
@@ -4306,7 +4307,7 @@
             "description": "Composer plugin for Symfony",
             "support": {
                 "issues": "https://github.com/symfony/flex/issues",
-                "source": "https://github.com/symfony/flex/tree/v2.4.4"
+                "source": "https://github.com/symfony/flex/tree/v2.4.3"
             },
             "funding": [
                 {
@@ -4322,7 +4323,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-05T18:04:53+00:00"
+            "time": "2024-01-02T11:08:32+00:00"
         },
         {
             "name": "symfony/form",
@@ -5638,16 +5639,16 @@
         },
         {
             "name": "symfony/polyfill-intl-grapheme",
-            "version": "v1.29.0",
+            "version": "v1.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
-                "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f"
+                "reference": "875e90aeea2777b6f135677f618529449334a612"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f",
-                "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612",
+                "reference": "875e90aeea2777b6f135677f618529449334a612",
                 "shasum": ""
             },
             "require": {
@@ -5658,6 +5659,9 @@
             },
             "type": "library",
             "extra": {
+                "branch-alias": {
+                    "dev-main": "1.28-dev"
+                },
                 "thanks": {
                     "name": "symfony/polyfill",
                     "url": "https://github.com/symfony/polyfill"
@@ -5696,7 +5700,7 @@
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0"
+                "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0"
             },
             "funding": [
                 {
@@ -5712,20 +5716,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-01-29T20:11:03+00:00"
+            "time": "2023-01-26T09:26:14+00:00"
         },
         {
             "name": "symfony/polyfill-intl-icu",
-            "version": "v1.29.0",
+            "version": "v1.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-icu.git",
-                "reference": "07094a28851a49107f3ab4f9120ca2975a64b6e1"
+                "reference": "e46b4da57951a16053cd751f63f4a24292788157"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/07094a28851a49107f3ab4f9120ca2975a64b6e1",
-                "reference": "07094a28851a49107f3ab4f9120ca2975a64b6e1",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/e46b4da57951a16053cd751f63f4a24292788157",
+                "reference": "e46b4da57951a16053cd751f63f4a24292788157",
                 "shasum": ""
             },
             "require": {
@@ -5736,6 +5740,9 @@
             },
             "type": "library",
             "extra": {
+                "branch-alias": {
+                    "dev-main": "1.28-dev"
+                },
                 "thanks": {
                     "name": "symfony/polyfill",
                     "url": "https://github.com/symfony/polyfill"
@@ -5780,7 +5787,7 @@
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.29.0"
+                "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.28.0"
             },
             "funding": [
                 {
@@ -5796,20 +5803,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-01-29T20:12:16+00:00"
+            "time": "2023-03-21T17:27:24+00:00"
         },
         {
             "name": "symfony/polyfill-intl-idn",
-            "version": "v1.29.0",
+            "version": "v1.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-idn.git",
-                "reference": "a287ed7475f85bf6f61890146edbc932c0fff919"
+                "reference": "ecaafce9f77234a6a449d29e49267ba10499116d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a287ed7475f85bf6f61890146edbc932c0fff919",
-                "reference": "a287ed7475f85bf6f61890146edbc932c0fff919",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/ecaafce9f77234a6a449d29e49267ba10499116d",
+                "reference": "ecaafce9f77234a6a449d29e49267ba10499116d",
                 "shasum": ""
             },
             "require": {
@@ -5822,6 +5829,9 @@
             },
             "type": "library",
             "extra": {
+                "branch-alias": {
+                    "dev-main": "1.28-dev"
+                },
                 "thanks": {
                     "name": "symfony/polyfill",
                     "url": "https://github.com/symfony/polyfill"
@@ -5864,7 +5874,7 @@
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.29.0"
+                "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.28.0"
             },
             "funding": [
                 {
@@ -5880,20 +5890,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-01-29T20:11:03+00:00"
+            "time": "2023-01-26T09:30:37+00:00"
         },
         {
             "name": "symfony/polyfill-intl-normalizer",
-            "version": "v1.29.0",
+            "version": "v1.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
-                "reference": "bc45c394692b948b4d383a08d7753968bed9a83d"
+                "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d",
-                "reference": "bc45c394692b948b4d383a08d7753968bed9a83d",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
+                "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
                 "shasum": ""
             },
             "require": {
@@ -5904,6 +5914,9 @@
             },
             "type": "library",
             "extra": {
+                "branch-alias": {
+                    "dev-main": "1.28-dev"
+                },
                 "thanks": {
                     "name": "symfony/polyfill",
                     "url": "https://github.com/symfony/polyfill"
@@ -5945,7 +5958,7 @@
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0"
+                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0"
             },
             "funding": [
                 {
@@ -5961,20 +5974,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-01-29T20:11:03+00:00"
+            "time": "2023-01-26T09:26:14+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.29.0",
+            "version": "v1.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec"
+                "reference": "42292d99c55abe617799667f454222c54c60e229"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
-                "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
+                "reference": "42292d99c55abe617799667f454222c54c60e229",
                 "shasum": ""
             },
             "require": {
@@ -5988,6 +6001,9 @@
             },
             "type": "library",
             "extra": {
+                "branch-alias": {
+                    "dev-main": "1.28-dev"
+                },
                 "thanks": {
                     "name": "symfony/polyfill",
                     "url": "https://github.com/symfony/polyfill"
@@ -6025,7 +6041,7 @@
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0"
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
             },
             "funding": [
                 {
@@ -6041,20 +6057,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-01-29T20:11:03+00:00"
+            "time": "2023-07-28T09:04:16+00:00"
         },
         {
             "name": "symfony/polyfill-php83",
-            "version": "v1.29.0",
+            "version": "v1.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php83.git",
-                "reference": "86fcae159633351e5fd145d1c47de6c528f8caff"
+                "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/86fcae159633351e5fd145d1c47de6c528f8caff",
-                "reference": "86fcae159633351e5fd145d1c47de6c528f8caff",
+                "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11",
+                "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11",
                 "shasum": ""
             },
             "require": {
@@ -6063,6 +6079,9 @@
             },
             "type": "library",
             "extra": {
+                "branch-alias": {
+                    "dev-main": "1.28-dev"
+                },
                 "thanks": {
                     "name": "symfony/polyfill",
                     "url": "https://github.com/symfony/polyfill"
@@ -6102,7 +6121,7 @@
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-php83/tree/v1.29.0"
+                "source": "https://github.com/symfony/polyfill-php83/tree/v1.28.0"
             },
             "funding": [
                 {
@@ -6118,20 +6137,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-01-29T20:11:03+00:00"
+            "time": "2023-08-16T06:22:46+00:00"
         },
         {
             "name": "symfony/polyfill-uuid",
-            "version": "v1.29.0",
+            "version": "v1.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-uuid.git",
-                "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853"
+                "reference": "9c44518a5aff8da565c8a55dbe85d2769e6f630e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/3abdd21b0ceaa3000ee950097bc3cf9efc137853",
-                "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853",
+                "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/9c44518a5aff8da565c8a55dbe85d2769e6f630e",
+                "reference": "9c44518a5aff8da565c8a55dbe85d2769e6f630e",
                 "shasum": ""
             },
             "require": {
@@ -6145,6 +6164,9 @@
             },
             "type": "library",
             "extra": {
+                "branch-alias": {
+                    "dev-main": "1.28-dev"
+                },
                 "thanks": {
                     "name": "symfony/polyfill",
                     "url": "https://github.com/symfony/polyfill"
@@ -6181,7 +6203,7 @@
                 "uuid"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-uuid/tree/v1.29.0"
+                "source": "https://github.com/symfony/polyfill-uuid/tree/v1.28.0"
             },
             "funding": [
                 {
@@ -6197,7 +6219,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-01-29T20:11:03+00:00"
+            "time": "2023-01-26T09:26:14+00:00"
         },
         {
             "name": "symfony/process",
@@ -6835,12 +6857,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/stimulus-bundle.git",
-                "reference": "da6b4a4f95a91390d38151ef67c77901a1210646"
+                "reference": "a7c30d95ff109f3b9c357d38d9aaf50ee512b8d4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/da6b4a4f95a91390d38151ef67c77901a1210646",
-                "reference": "da6b4a4f95a91390d38151ef67c77901a1210646",
+                "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/a7c30d95ff109f3b9c357d38d9aaf50ee512b8d4",
+                "reference": "a7c30d95ff109f3b9c357d38d9aaf50ee512b8d4",
                 "shasum": ""
             },
             "require": {
@@ -6881,7 +6903,7 @@
                 "symfony-ux"
             ],
             "support": {
-                "source": "https://github.com/symfony/stimulus-bundle/tree/v2.14.1"
+                "source": "https://github.com/symfony/stimulus-bundle/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -6897,7 +6919,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-03T01:11:47+00:00"
+            "time": "2024-01-30T15:40:36+00:00"
         },
         {
             "name": "symfony/stopwatch",
@@ -7911,12 +7933,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/ux-live-component.git",
-                "reference": "ab8710fa0990efb7b9d34b7ec91acac7b6e60d17"
+                "reference": "9ee071aaefeb7ccb04ba6f0365b35e0016a2b5b8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/ux-live-component/zipball/ab8710fa0990efb7b9d34b7ec91acac7b6e60d17",
-                "reference": "ab8710fa0990efb7b9d34b7ec91acac7b6e60d17",
+                "url": "https://api.github.com/repos/symfony/ux-live-component/zipball/9ee071aaefeb7ccb04ba6f0365b35e0016a2b5b8",
+                "reference": "9ee071aaefeb7ccb04ba6f0365b35e0016a2b5b8",
                 "shasum": ""
             },
             "require": {
@@ -7980,7 +8002,7 @@
                 "twig"
             ],
             "support": {
-                "source": "https://github.com/symfony/ux-live-component/tree/v2.14.1"
+                "source": "https://github.com/symfony/ux-live-component/tree/v2.14.0"
             },
             "funding": [
                 {
@@ -7996,7 +8018,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-03T01:11:47+00:00"
+            "time": "2024-01-30T15:40:38+00:00"
         },
         {
             "name": "symfony/ux-notify",
@@ -9965,27 +9987,25 @@
         },
         {
             "name": "nikic/php-parser",
-            "version": "v5.0.0",
+            "version": "v4.18.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nikic/PHP-Parser.git",
-                "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc"
+                "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4a21235f7e56e713259a6f76bf4b5ea08502b9dc",
-                "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc",
+                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999",
+                "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999",
                 "shasum": ""
             },
             "require": {
-                "ext-ctype": "*",
-                "ext-json": "*",
                 "ext-tokenizer": "*",
-                "php": ">=7.4"
+                "php": ">=7.0"
             },
             "require-dev": {
                 "ircmaxell/php-yacc": "^0.0.7",
-                "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
+                "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0"
             },
             "bin": [
                 "bin/php-parse"
@@ -9993,7 +10013,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "5.0-dev"
+                    "dev-master": "4.9-dev"
                 }
             },
             "autoload": {
@@ -10017,9 +10037,9 @@
             ],
             "support": {
                 "issues": "https://github.com/nikic/PHP-Parser/issues",
-                "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.0"
+                "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0"
             },
-            "time": "2024-01-07T17:17:35+00:00"
+            "time": "2023-12-10T21:03:43+00:00"
         },
         {
             "name": "phar-io/manifest",
@@ -11794,21 +11814,21 @@
         },
         {
             "name": "symfony/maker-bundle",
-            "version": "v1.53.0",
+            "version": "v1.52.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/maker-bundle.git",
-                "reference": "8d2f3f96704766837548d177fe3ae39ae94822d9"
+                "reference": "112f9466c94a46ca33dc441eee59a12cd1790757"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/8d2f3f96704766837548d177fe3ae39ae94822d9",
-                "reference": "8d2f3f96704766837548d177fe3ae39ae94822d9",
+                "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/112f9466c94a46ca33dc441eee59a12cd1790757",
+                "reference": "112f9466c94a46ca33dc441eee59a12cd1790757",
                 "shasum": ""
             },
             "require": {
                 "doctrine/inflector": "^2.0",
-                "nikic/php-parser": "^4.18|^5.0",
+                "nikic/php-parser": "^4.11",
                 "php": ">=8.1",
                 "symfony/config": "^6.3|^7.0",
                 "symfony/console": "^6.3|^7.0",
@@ -11832,7 +11852,7 @@
                 "symfony/phpunit-bridge": "^6.3|^7.0",
                 "symfony/security-core": "^6.3|^7.0",
                 "symfony/yaml": "^6.3|^7.0",
-                "twig/twig": "^3.0|^4.x-dev"
+                "twig/twig": "^2.0|^3.0"
             },
             "type": "symfony-bundle",
             "extra": {
@@ -11866,7 +11886,7 @@
             ],
             "support": {
                 "issues": "https://github.com/symfony/maker-bundle/issues",
-                "source": "https://github.com/symfony/maker-bundle/tree/v1.53.0"
+                "source": "https://github.com/symfony/maker-bundle/tree/v1.52.0"
             },
             "funding": [
                 {
@@ -11882,7 +11902,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-01T10:05:38+00:00"
+            "time": "2023-10-31T18:23:49+00:00"
         },
         {
             "name": "symfony/phpunit-bridge",
diff --git a/ux.symfony.com/src/Controller/Demo/LiveComponentDemoController.php b/ux.symfony.com/src/Controller/Demo/LiveComponentDemoController.php
index e9084546..52b7226b 100644
--- a/ux.symfony.com/src/Controller/Demo/LiveComponentDemoController.php
+++ b/ux.symfony.com/src/Controller/Demo/LiveComponentDemoController.php
@@ -41,7 +41,7 @@ class LiveComponentDemoController extends AbstractController
     }
 
     #[Route('/form-collection-type/{id}', name: 'app_demo_live_component_form_collection_type', defaults: ['id' => null])]
-    public function demoFormCollectionType(LiveDemoRepository $liveDemoRepository, Request $request, TodoListRepository $todoListRepository, ?TodoList $todoList = null): Response
+    public function demoFormCollectionType(LiveDemoRepository $liveDemoRepository, Request $request, TodoListRepository $todoListRepository, TodoList $todoList = null): Response
     {
         if (!$todoList) {
             $todoList = new TodoList();
@@ -106,7 +106,7 @@ class LiveComponentDemoController extends AbstractController
     }
 
     #[Route('/invoice/{id}', name: 'app_demo_live_component_invoice', defaults: ['id' => null])]
-    public function invoice(LiveDemoRepository $liveDemoRepository, ?Invoice $invoice = null): Response
+    public function invoice(LiveDemoRepository $liveDemoRepository, Invoice $invoice = null): Response
     {
         $invoice = $invoice ?? new Invoice();
 
diff --git a/ux.symfony.com/src/Controller/UxPackage/LiveComponentController.php b/ux.symfony.com/src/Controller/UxPackage/LiveComponentController.php
index 54876841..cfa94254 100644
--- a/ux.symfony.com/src/Controller/UxPackage/LiveComponentController.php
+++ b/ux.symfony.com/src/Controller/UxPackage/LiveComponentController.php
@@ -31,7 +31,7 @@ class LiveComponentController extends AbstractController
     }
 
     #[Route('/live-component/demos/{demo}', name: 'app_live_component_demo_redirect')]
-    public function redirectDemo(LiveDemoRepository $liveDemoRepository, ?string $demo = null): Response
+    public function redirectDemo(LiveDemoRepository $liveDemoRepository, string $demo = null): Response
     {
         if (null === $demo || '' === $demo) {
             return $this->redirectToRoute('app_demos');
diff --git a/ux.symfony.com/src/Controller/UxPackage/TurboController.php b/ux.symfony.com/src/Controller/UxPackage/TurboController.php
index 9de5bbe1..10975b38 100644
--- a/ux.symfony.com/src/Controller/UxPackage/TurboController.php
+++ b/ux.symfony.com/src/Controller/UxPackage/TurboController.php
@@ -30,7 +30,7 @@ class TurboController extends AbstractController
 {
     #[Route('/turbo', name: 'app_turbo')]
     #[Route('/turbo/{name}/the/{animal}', name: 'app_turbo_with_animal')]
-    public function __invoke(UxPackageRepository $packageRepository, ChatRepository $chatRepository, Request $request, PackageContext $packageContext, ?string $name = null, ?string $animal = null): Response
+    public function __invoke(UxPackageRepository $packageRepository, ChatRepository $chatRepository, Request $request, PackageContext $packageContext, string $name = null, string $animal = null): Response
     {
         $package = $packageRepository->find('turbo');
 
diff --git a/ux.symfony.com/src/LiveMemory/GameEngine.php b/ux.symfony.com/src/LiveMemory/GameEngine.php
index b65fa424..9aa81138 100644
--- a/ux.symfony.com/src/LiveMemory/GameEngine.php
+++ b/ux.symfony.com/src/LiveMemory/GameEngine.php
@@ -71,7 +71,7 @@ final class GameEngine
         $game->setScore($game->getScore() + ($game->getCardCount() * $pointPerMatch));
     }
 
-    private function endGame(Game $game, ?\DateTimeImmutable $endedAt = null): void
+    private function endGame(Game $game, \DateTimeImmutable $endedAt = null): void
     {
         $game->setEndedAt($endedAt ?? new \DateTimeImmutable('now'));
         if ($game->isCompleted()) {
diff --git a/ux.symfony.com/src/Model/RecipeFileTree.php b/ux.symfony.com/src/Model/RecipeFileTree.php
index 4a5df23e..c3e95b4b 100644
--- a/ux.symfony.com/src/Model/RecipeFileTree.php
+++ b/ux.symfony.com/src/Model/RecipeFileTree.php
@@ -31,14 +31,14 @@ class RecipeFileTree
         ;
     }
 
-    private function addFile(string $filename, ?string $description = null): self
+    private function addFile(string $filename, string $description = null): self
     {
         $this->files[$filename] = ['type' => 'file', 'description' => $description];
 
         return $this;
     }
 
-    private function addDirectory(string $path, ?string $description = null): self
+    private function addDirectory(string $path, string $description = null): self
     {
         $this->files[$path] = ['type' => 'directory', 'description' => $description];
 
diff --git a/ux.symfony.com/src/Serializer/UxPackageNormalizer.php b/ux.symfony.com/src/Serializer/UxPackageNormalizer.php
index ae7adf08..acdfd0c7 100644
--- a/ux.symfony.com/src/Serializer/UxPackageNormalizer.php
+++ b/ux.symfony.com/src/Serializer/UxPackageNormalizer.php
@@ -30,7 +30,7 @@ class UxPackageNormalizer implements NormalizerInterface
     /**
      * @param UxPackage $object
      */
-    public function normalize(mixed $object, ?string $format = null, array $context = []): array
+    public function normalize(mixed $object, string $format = null, array $context = []): array
     {
         $data = $this->normalizer->normalize($object, $format, $context);
 
@@ -41,7 +41,7 @@ class UxPackageNormalizer implements NormalizerInterface
         return $data;
     }
 
-    public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
+    public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
     {
         return $data instanceof UxPackage;
     }
diff --git a/ux.symfony.com/src/Service/UxPackageRepository.php b/ux.symfony.com/src/Service/UxPackageRepository.php
index 83f1727b..a63c756e 100644
--- a/ux.symfony.com/src/Service/UxPackageRepository.php
+++ b/ux.symfony.com/src/Service/UxPackageRepository.php
@@ -18,7 +18,7 @@ class UxPackageRepository
     /**
      * @return array<UxPackage>
      */
-    public function findAll(?string $query = null): array
+    public function findAll(string $query = null): array
     {
         $packages = [
             (new UxPackage(
diff --git a/ux.symfony.com/src/Twig/SearchPackages.php b/ux.symfony.com/src/Twig/SearchPackages.php
index b79b3f85..a08efa69 100644
--- a/ux.symfony.com/src/Twig/SearchPackages.php
+++ b/ux.symfony.com/src/Twig/SearchPackages.php
@@ -21,7 +21,7 @@ class SearchPackages
 {
     use DefaultActionTrait;
 
-    #[LiveProp(writable: true, url: true)]
+    #[LiveProp(writable: true)]
     public ?string $query = null;
 
     public function __construct(private UxPackageRepository $packageRepo)


Test patch:
diff --git a/src/Autocomplete/tests/Fixtures/Autocompleter/CustomGroupByProductAutocompleter.php b/src/Autocomplete/tests/Fixtures/Autocompleter/CustomGroupByProductAutocompleter.php
index c4407f00..da942cab 100644
--- a/src/Autocomplete/tests/Fixtures/Autocompleter/CustomGroupByProductAutocompleter.php
+++ b/src/Autocomplete/tests/Fixtures/Autocompleter/CustomGroupByProductAutocompleter.php
@@ -1,14 +1,15 @@
 <?php
 
-namespace Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter;
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
 
-use Doctrine\ORM\EntityRepository;
-use Doctrine\ORM\QueryBuilder;
-use Symfony\Component\HttpFoundation\RequestStack;
-use Symfony\Bundle\SecurityBundle\Security;
-use Symfony\UX\Autocomplete\Doctrine\EntitySearchUtil;
-use Symfony\UX\Autocomplete\EntityAutocompleterInterface;
-use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Product;
+namespace Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter;
 
 class CustomGroupByProductAutocompleter extends CustomProductAutocompleter
 {
diff --git a/src/Autocomplete/tests/Fixtures/Autocompleter/CustomProductAutocompleter.php b/src/Autocomplete/tests/Fixtures/Autocompleter/CustomProductAutocompleter.php
index 3ae0877a..912a864c 100644
--- a/src/Autocomplete/tests/Fixtures/Autocompleter/CustomProductAutocompleter.php
+++ b/src/Autocomplete/tests/Fixtures/Autocompleter/CustomProductAutocompleter.php
@@ -1,11 +1,20 @@
 <?php
 
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
 namespace Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter;
 
 use Doctrine\ORM\EntityRepository;
 use Doctrine\ORM\QueryBuilder;
-use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\UX\Autocomplete\Doctrine\EntitySearchUtil;
 use Symfony\UX\Autocomplete\EntityAutocompleterInterface;
 use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Product;
@@ -14,9 +23,8 @@ class CustomProductAutocompleter implements EntityAutocompleterInterface
 {
     public function __construct(
         private RequestStack $requestStack,
-        private EntitySearchUtil $entitySearchUtil
-    )
-    {
+        private EntitySearchUtil $entitySearchUtil,
+    ) {
     }
 
     public function getEntityClass(): string
diff --git a/src/Autocomplete/tests/Fixtures/Entity/Category.php b/src/Autocomplete/tests/Fixtures/Entity/Category.php
index 7774cf92..174341cb 100644
--- a/src/Autocomplete/tests/Fixtures/Entity/Category.php
+++ b/src/Autocomplete/tests/Fixtures/Entity/Category.php
@@ -1,10 +1,18 @@
 <?php
 
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
 namespace Symfony\UX\Autocomplete\Tests\Fixtures\Entity;
 
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
-use Doctrine\DBAL\Types\Types;
 use Doctrine\ORM\Mapping as ORM;
 
 #[ORM\Entity()]
diff --git a/src/Autocomplete/tests/Fixtures/Entity/Ingredient.php b/src/Autocomplete/tests/Fixtures/Entity/Ingredient.php
index bc810d53..edea1c03 100644
--- a/src/Autocomplete/tests/Fixtures/Entity/Ingredient.php
+++ b/src/Autocomplete/tests/Fixtures/Entity/Ingredient.php
@@ -1,5 +1,14 @@
 <?php
 
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
 namespace Symfony\UX\Autocomplete\Tests\Fixtures\Entity;
 
 use Doctrine\ORM\Mapping as ORM;
diff --git a/src/Autocomplete/tests/Fixtures/Entity/Product.php b/src/Autocomplete/tests/Fixtures/Entity/Product.php
index 8640290b..7db8056e 100644
--- a/src/Autocomplete/tests/Fixtures/Entity/Product.php
+++ b/src/Autocomplete/tests/Fixtures/Entity/Product.php
@@ -1,5 +1,14 @@
 <?php
 
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
 namespace Symfony\UX\Autocomplete\Tests\Fixtures\Entity;
 
 use Doctrine\Common\Collections\ArrayCollection;
diff --git a/src/Autocomplete/tests/Fixtures/Factory/CategoryFactory.php b/src/Autocomplete/tests/Fixtures/Factory/CategoryFactory.php
index e24abe09..b7963066 100644
--- a/src/Autocomplete/tests/Fixtures/Factory/CategoryFactory.php
+++ b/src/Autocomplete/tests/Fixtures/Factory/CategoryFactory.php
@@ -1,30 +1,39 @@
 <?php
 
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
 namespace Symfony\UX\Autocomplete\Tests\Fixtures\Factory;
 
 use Doctrine\ORM\EntityRepository;
 use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Category;
-use Zenstruck\Foundry\RepositoryProxy;
 use Zenstruck\Foundry\ModelFactory;
 use Zenstruck\Foundry\Proxy;
+use Zenstruck\Foundry\RepositoryProxy;
 
 /**
  * @extends ModelFactory<Category>
  *
- * @method static Category|Proxy createOne(array $attributes = [])
- * @method static Category[]|Proxy[] createMany(int $number, array|callable $attributes = [])
- * @method static Category|Proxy find(object|array|mixed $criteria)
- * @method static Category|Proxy findOrCreate(array $attributes)
- * @method static Category|Proxy first(string $sortedField = 'id')
- * @method static Category|Proxy last(string $sortedField = 'id')
- * @method static Category|Proxy random(array $attributes = [])
- * @method static Category|Proxy randomOrCreate(array $attributes = []))
- * @method static Category[]|Proxy[] all()
- * @method static Category[]|Proxy[] findBy(array $attributes)
- * @method static Category[]|Proxy[] randomSet(int $number, array $attributes = []))
- * @method static Category[]|Proxy[] randomRange(int $min, int $max, array $attributes = []))
+ * @method static Category|Proxy                   createOne(array $attributes = [])
+ * @method static Category[]|Proxy[]               createMany(int $number, array|callable $attributes = [])
+ * @method static Category|Proxy                   find(object|array|mixed $criteria)
+ * @method static Category|Proxy                   findOrCreate(array $attributes)
+ * @method static Category|Proxy                   first(string $sortedField = 'id')
+ * @method static Category|Proxy                   last(string $sortedField = 'id')
+ * @method static Category|Proxy                   random(array $attributes = [])
+ * @method static Category|Proxy                   randomOrCreate(array $attributes = []))
+ * @method static Category[]|Proxy[]               all()
+ * @method static Category[]|Proxy[]               findBy(array $attributes)
+ * @method static Category[]|Proxy[]               randomSet(int $number, array $attributes = []))
+ * @method static Category[]|Proxy[]               randomRange(int $min, int $max, array $attributes = []))
  * @method static EntityRepository|RepositoryProxy repository()
- * @method Category|Proxy create(array|callable $attributes = [])
+ * @method        Category|Proxy                   create(array|callable $attributes = [])
  */
 final class CategoryFactory extends ModelFactory
 {
diff --git a/src/Autocomplete/tests/Fixtures/Factory/IngredientFactory.php b/src/Autocomplete/tests/Fixtures/Factory/IngredientFactory.php
index ee238ce1..4669fa98 100644
--- a/src/Autocomplete/tests/Fixtures/Factory/IngredientFactory.php
+++ b/src/Autocomplete/tests/Fixtures/Factory/IngredientFactory.php
@@ -1,31 +1,40 @@
 <?php
 
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
 namespace Symfony\UX\Autocomplete\Tests\Fixtures\Factory;
 
 use Doctrine\ORM\EntityRepository;
 use Symfony\Component\Uid\UuidV4;
 use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Ingredient;
-use Zenstruck\Foundry\RepositoryProxy;
 use Zenstruck\Foundry\ModelFactory;
 use Zenstruck\Foundry\Proxy;
+use Zenstruck\Foundry\RepositoryProxy;
 
 /**
  * @extends ModelFactory<Ingredient>
  *
- * @method static Ingredient|Proxy createOne(array $attributes = [])
- * @method static Ingredient[]|Proxy[] createMany(int $number, array|callable $attributes = [])
- * @method static Ingredient|Proxy find(object|array|mixed $criteria)
- * @method static Ingredient|Proxy findOrCreate(array $attributes)
- * @method static Ingredient|Proxy first(string $sortedField = 'id')
- * @method static Ingredient|Proxy last(string $sortedField = 'id')
- * @method static Ingredient|Proxy random(array $attributes = [])
- * @method static Ingredient|Proxy randomOrCreate(array $attributes = []))
- * @method static Ingredient[]|Proxy[] all()
- * @method static Ingredient[]|Proxy[] findBy(array $attributes)
- * @method static Ingredient[]|Proxy[] randomSet(int $number, array $attributes = []))
- * @method static Ingredient[]|Proxy[] randomRange(int $min, int $max, array $attributes = []))
+ * @method static Ingredient|Proxy                 createOne(array $attributes = [])
+ * @method static Ingredient[]|Proxy[]             createMany(int $number, array|callable $attributes = [])
+ * @method static Ingredient|Proxy                 find(object|array|mixed $criteria)
+ * @method static Ingredient|Proxy                 findOrCreate(array $attributes)
+ * @method static Ingredient|Proxy                 first(string $sortedField = 'id')
+ * @method static Ingredient|Proxy                 last(string $sortedField = 'id')
+ * @method static Ingredient|Proxy                 random(array $attributes = [])
+ * @method static Ingredient|Proxy                 randomOrCreate(array $attributes = []))
+ * @method static Ingredient[]|Proxy[]             all()
+ * @method static Ingredient[]|Proxy[]             findBy(array $attributes)
+ * @method static Ingredient[]|Proxy[]             randomSet(int $number, array $attributes = []))
+ * @method static Ingredient[]|Proxy[]             randomRange(int $min, int $max, array $attributes = []))
  * @method static EntityRepository|RepositoryProxy repository()
- * @method Ingredient|Proxy create(array|callable $attributes = [])
+ * @method        Ingredient|Proxy                 create(array|callable $attributes = [])
  */
 final class IngredientFactory extends ModelFactory
 {
diff --git a/src/Autocomplete/tests/Fixtures/Factory/ProductFactory.php b/src/Autocomplete/tests/Fixtures/Factory/ProductFactory.php
index 4b99ac1f..975c10be 100644
--- a/src/Autocomplete/tests/Fixtures/Factory/ProductFactory.php
+++ b/src/Autocomplete/tests/Fixtures/Factory/ProductFactory.php
@@ -1,30 +1,39 @@
 <?php
 
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
 namespace Symfony\UX\Autocomplete\Tests\Fixtures\Factory;
 
 use Doctrine\ORM\EntityRepository;
 use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Product;
-use Zenstruck\Foundry\RepositoryProxy;
 use Zenstruck\Foundry\ModelFactory;
 use Zenstruck\Foundry\Proxy;
+use Zenstruck\Foundry\RepositoryProxy;
 
 /**
  * @extends ModelFactory<Product>
  *
- * @method static Product|Proxy createOne(array $attributes = [])
- * @method static Product[]|Proxy[] createMany(int $number, array|callable $attributes = [])
- * @method static Product|Proxy find(object|array|mixed $criteria)
- * @method static Product|Proxy findOrCreate(array $attributes)
- * @method static Product|Proxy first(string $sortedField = 'id')
- * @method static Product|Proxy last(string $sortedField = 'id')
- * @method static Product|Proxy random(array $attributes = [])
- * @method static Product|Proxy randomOrCreate(array $attributes = []))
- * @method static Product[]|Proxy[] all()
- * @method static Product[]|Proxy[] findBy(array $attributes)
- * @method static Product[]|Proxy[] randomSet(int $number, array $attributes = []))
- * @method static Product[]|Proxy[] randomRange(int $min, int $max, array $attributes = []))
+ * @method static Product|Proxy                    createOne(array $attributes = [])
+ * @method static Product[]|Proxy[]                createMany(int $number, array|callable $attributes = [])
+ * @method static Product|Proxy                    find(object|array|mixed $criteria)
+ * @method static Product|Proxy                    findOrCreate(array $attributes)
+ * @method static Product|Proxy                    first(string $sortedField = 'id')
+ * @method static Product|Proxy                    last(string $sortedField = 'id')
+ * @method static Product|Proxy                    random(array $attributes = [])
+ * @method static Product|Proxy                    randomOrCreate(array $attributes = []))
+ * @method static Product[]|Proxy[]                all()
+ * @method static Product[]|Proxy[]                findBy(array $attributes)
+ * @method static Product[]|Proxy[]                randomSet(int $number, array $attributes = []))
+ * @method static Product[]|Proxy[]                randomRange(int $min, int $max, array $attributes = []))
  * @method static EntityRepository|RepositoryProxy repository()
- * @method Product|Proxy create(array|callable $attributes = [])
+ * @method        Product|Proxy                    create(array|callable $attributes = [])
  */
 final class ProductFactory extends ModelFactory
 {
diff --git a/src/Autocomplete/tests/Fixtures/Form/AlternateRouteAutocompleteType.php b/src/Autocomplete/tests/Fixtures/Form/AlternateRouteAutocompleteType.php
index 264dafdc..2ef40251 100644
--- a/src/Autocomplete/tests/Fixtures/Form/AlternateRouteAutocompleteType.php
+++ b/src/Autocomplete/tests/Fixtures/Form/AlternateRouteAutocompleteType.php
@@ -1,5 +1,14 @@
 <?php
 
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
 namespace Symfony\UX\Autocomplete\Tests\Fixtures\Form;
 
 use Symfony\Component\Form\AbstractType;
@@ -15,7 +24,7 @@ class AlternateRouteAutocompleteType extends AbstractType
     {
         $resolver->setDefaults([
             'class' => Ingredient::class,
-            'choice_label' => function(Ingredient $ingredient) {
+            'choice_label' => function (Ingredient $ingredient) {
                 return $ingredient->getName();
             },
             'multiple' => true,
diff --git a/src/Autocomplete/tests/Fixtures/Form/CategoryAutocompleteType.php b/src/Autocomplete/tests/Fixtures/Form/CategoryAutocompleteType.php
index f4f1c5e3..5f96dc3f 100644
--- a/src/Autocomplete/tests/Fixtures/Form/CategoryAutocompleteType.php
+++ b/src/Autocomplete/tests/Fixtures/Form/CategoryAutocompleteType.php
@@ -1,15 +1,24 @@
 <?php
 
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
 namespace Symfony\UX\Autocomplete\Tests\Fixtures\Form;
 
 use Doctrine\ORM\EntityRepository;
-use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
-use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Category;
+use Symfony\Bundle\SecurityBundle\Security;
 use Symfony\Component\Form\AbstractType;
 use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\OptionsResolver\OptionsResolver;
-use Symfony\Bundle\SecurityBundle\Security;
 use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
+use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
+use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Category;
 
 #[AsEntityAutocompleteField]
 class CategoryAutocompleteType extends AbstractType
@@ -22,15 +31,15 @@ class CategoryAutocompleteType extends AbstractType
     {
         $resolver->setDefaults([
             'class' => Category::class,
-            'choice_label' => function(Category $category) {
+            'choice_label' => function (Category $category) {
                 return '<strong>'.$category->getName().'</strong>';
             },
-            'query_builder' => function(EntityRepository $repository) {
+            'query_builder' => function (EntityRepository $repository) {
                 return $repository->createQueryBuilder('category')
                     ->andWhere('category.name LIKE :search')
                     ->setParameter('search', '%foo%');
             },
-            'security' => function(Security $security) {
+            'security' => function (Security $security) {
                 if ($this->requestStack->getCurrentRequest()?->query->get('enforce_test_security')) {
                     return $security->isGranted('ROLE_USER');
                 }
diff --git a/src/Autocomplete/tests/Fixtures/Form/CategoryNoChoiceLabelAutocompleteType.php b/src/Autocomplete/tests/Fixtures/Form/CategoryNoChoiceLabelAutocompleteType.php
index 4ca1a479..9b80ea84 100644
--- a/src/Autocomplete/tests/Fixtures/Form/CategoryNoChoiceLabelAutocompleteType.php
+++ b/src/Autocomplete/tests/Fixtures/Form/CategoryNoChoiceLabelAutocompleteType.php
@@ -1,12 +1,21 @@
 <?php
 
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
 namespace Symfony\UX\Autocomplete\Tests\Fixtures\Form;
 
-use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
-use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Category;
 use Symfony\Component\Form\AbstractType;
 use Symfony\Component\OptionsResolver\OptionsResolver;
 use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
+use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
+use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Category;
 
 #[AsEntityAutocompleteField]
 class CategoryNoChoiceLabelAutocompleteType extends AbstractType
diff --git a/src/Autocomplete/tests/Fixtures/Form/IngredientAutocompleteType.php b/src/Autocomplete/tests/Fixtures/Form/IngredientAutocompleteType.php
index 2cb45555..c2b636ce 100644
--- a/src/Autocomplete/tests/Fixtures/Form/IngredientAutocompleteType.php
+++ b/src/Autocomplete/tests/Fixtures/Form/IngredientAutocompleteType.php
@@ -1,8 +1,19 @@
 <?php
 
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
 namespace Symfony\UX\Autocomplete\Tests\Fixtures\Form;
 
+use Doctrine\ORM\EntityRepository;
 use Symfony\Component\Form\AbstractType;
+use Symfony\Component\OptionsResolver\Options;
 use Symfony\Component\OptionsResolver\OptionsResolver;
 use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
 use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
@@ -15,10 +26,22 @@ class IngredientAutocompleteType extends AbstractType
     {
         $resolver->setDefaults([
             'class' => Ingredient::class,
-            'choice_label' => function(Ingredient $ingredient) {
+            'choice_label' => function (Ingredient $ingredient) {
                 return '<strong>'.$ingredient->getName().'</strong>';
             },
             'multiple' => true,
+            'query_builder' => function (Options $options): callable {
+                return function (EntityRepository $repository) use ($options) {
+                    $qb = $repository->createQueryBuilder('o');
+
+                    if ($options['extra_options']['banned_ingredient'] ?? false) {
+                        $qb->andWhere('o.name != :banned_ingredient')
+                            ->setParameter('banned_ingredient', $options['extra_options']['banned_ingredient']);
+                    }
+
+                    return $qb;
+                };
+            },
         ]);
     }
 
diff --git a/src/Autocomplete/tests/Fixtures/Form/ProductType.php b/src/Autocomplete/tests/Fixtures/Form/ProductType.php
index ce99325d..c2c5b35b 100644
--- a/src/Autocomplete/tests/Fixtures/Form/ProductType.php
+++ b/src/Autocomplete/tests/Fixtures/Form/ProductType.php
@@ -1,21 +1,38 @@
 <?php
 
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
 namespace Symfony\UX\Autocomplete\Tests\Fixtures\Form;
 
-use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Product;
 use Symfony\Component\Form\AbstractType;
 use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
 use Symfony\Component\Form\Extension\Core\Type\TextType;
 use Symfony\Component\Form\FormBuilderInterface;
 use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Product;
 
 class ProductType extends AbstractType
 {
     public function buildForm(FormBuilderInterface $builder, array $options): void
     {
         $builder
-            ->add('category', CategoryAutocompleteType::class)
-            ->add('ingredients', IngredientAutocompleteType::class)
+            ->add('category', CategoryAutocompleteType::class, [
+                'extra_options' => [
+                    'some' => 'cool_option',
+                ],
+            ])
+            ->add('ingredients', IngredientAutocompleteType::class, [
+                'extra_options' => [
+                    'banned_ingredient' => 'Modified Flour',
+                ],
+            ])
             ->add('portionSize', ChoiceType::class, [
                 'choices' => [
                     'extra small <span>🥨</span>' => 'xs',
diff --git a/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php b/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php
index a7d647ed..72d801b6 100644
--- a/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php
+++ b/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php
@@ -31,7 +31,7 @@ class AutocompleteFormRenderingTest extends KernelTestCase
             ->throwExceptions()
             ->get('/test-form')
             ->assertElementAttributeContains('#product_category', 'data-controller', 'custom-autocomplete symfony--ux-autocomplete--autocomplete')
-            ->assertElementAttributeContains('#product_category', 'data-symfony--ux-autocomplete--autocomplete-url-value', '/test/autocomplete/category_autocomplete_type')
+            ->assertElementAttributeContains('#product_category', 'data-symfony--ux-autocomplete--autocomplete-url-value', '/test/autocomplete/category_autocomplete_type?extra_options=')
             ->assertElementAttributeContains('#product_category', 'data-symfony--ux-autocomplete--autocomplete-min-characters-value', '2')
             ->assertElementAttributeContains('#product_category', 'data-symfony--ux-autocomplete--autocomplete-max-results-value', '25')
 
@@ -123,4 +123,58 @@ class AutocompleteFormRenderingTest extends KernelTestCase
             ->assertElementCount('#product_ingredients option', 0)
         ;
     }
+
+    public function testItUsesPassedExtraOptions()
+    {
+        $ingredient1 = IngredientFactory::createOne(['name' => 'Flour']);
+        $ingredient2 = IngredientFactory::createOne(['name' => 'Sugar']);
+        $ingredient3 = IngredientFactory::createOne(['name' => 'Modified Flour']);
+
+        $this->browser()
+            ->throwExceptions()
+            ->get('/test-form')
+            ->assertElementCount('#product_ingredients option', 0)
+            ->assertNotContains('Flour')
+            ->assertNotContains('Sugar')
+            ->assertNotContains('Modified Flour')
+            // request all three ingredients
+            ->post('/test-form', [
+                'body' => [
+                    'product' => [
+                        'ingredients' => [
+                            (string) $ingredient1->getId(),
+                            (string) $ingredient2->getId(),
+                            (string) $ingredient3->getId(),
+                        ],
+                    ],
+                ],
+            ])
+            // assert that "Modified Flour" is not included
+            ->assertElementCount('#product_ingredients option', 2)
+            ->assertContains('Flour')
+            ->assertContains('Sugar')
+            ->assertNotContains('Modified Flour')
+        ;
+    }
+
+    public function testItReturnsErrorWhenSendingMalformedExtraOptions(): void
+    {
+        $extraOptionsWithoutChecksum = $this->encodeData(['foo' => 'bar']);
+        $extraOptionsWithInvalidChecksum = $this->encodeData(['foo' => 'bar', '@checksum' => 'invalid']);
+        $extraOptionsWithValidChecksum = $this->encodeData(['foo' => 'bar', '@checksum' => 'O2nYjcGr/l8GmUuYUSfE52hoyEL0NtDhBzUbn17KVHQ=']);
+
+        $this->browser()
+            ->post(sprintf('/test/autocomplete/category_autocomplete_type?extra_options=%s', $extraOptionsWithoutChecksum))
+            ->assertStatus(400)
+            ->post(sprintf('/test/autocomplete/category_autocomplete_type?extra_options=%s', $extraOptionsWithInvalidChecksum))
+            ->assertStatus(400)
+            ->post(sprintf('/test/autocomplete/category_autocomplete_type?extra_options=%s', $extraOptionsWithValidChecksum))
+            ->assertStatus(200)
+        ;
+    }
+
+    private function encodeData(array $data): string
+    {
+        return base64_encode(json_encode($data));
+    }
 }
diff --git a/src/Autocomplete/tests/Integration/Doctrine/EntitySearchUtilTest.php b/src/Autocomplete/tests/Integration/Doctrine/EntitySearchUtilTest.php
index e10560fc..62a29001 100644
--- a/src/Autocomplete/tests/Integration/Doctrine/EntitySearchUtilTest.php
+++ b/src/Autocomplete/tests/Integration/Doctrine/EntitySearchUtilTest.php
@@ -61,7 +61,7 @@ class EntitySearchUtilTest extends KernelTestCase
     /**
      * @return array<Product>
      */
-    private function callAddSearchClass(string $search, ?array $searchableProperties = null): array
+    private function callAddSearchClass(string $search, array $searchableProperties = null): array
     {
         /** @var ManagerRegistry $registry */
         $registry = self::getContainer()->get('doctrine');
diff --git a/src/Autocomplete/tests/Unit/Calculator/ChecksumCalculatorTest.php b/src/Autocomplete/tests/Unit/Calculator/ChecksumCalculatorTest.php
new file mode 100644
index 00000000..58bcd127
--- /dev/null
+++ b/src/Autocomplete/tests/Unit/Calculator/ChecksumCalculatorTest.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\UX\Autocomplete\Tests\Unit\Calculator;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\UX\Autocomplete\Checksum\ChecksumCalculator;
+
+final class ChecksumCalculatorTest extends TestCase
+{
+    public function testCalculateChecksumForArray(): void
+    {
+        $this->assertSame(
+            'tZ34YQKgpttqybzPws0YJCOHV1QtMjQeuyy+rszdhXU=',
+            $this->createTestSubject()->calculateForArray(['test' => 'test']),
+        );
+    }
+
+    public function testCalculateTheSameChecksumForTheSameArrayButInDifferentOrder(): void
+    {
+        $this->assertSame(
+            $this->createTestSubject()->calculateForArray(['test' => 'test', 'test2' => 'test2']),
+            $this->createTestSubject()->calculateForArray(['test2' => 'test2', 'test' => 'test']),
+        );
+    }
+
+    private function createTestSubject(): ChecksumCalculator
+    {
+        return new ChecksumCalculator('s3cr3t');
+    }
+}
diff --git a/src/LiveComponent/src/Test/InteractsWithLiveComponents.php b/src/LiveComponent/src/Test/InteractsWithLiveComponents.php
index 6d2df7ef..9df7161f 100644
--- a/src/LiveComponent/src/Test/InteractsWithLiveComponents.php
+++ b/src/LiveComponent/src/Test/InteractsWithLiveComponents.php
@@ -20,7 +20,7 @@ use Symfony\UX\TwigComponent\ComponentFactory;
  */
 trait InteractsWithLiveComponents
 {
-    protected function createLiveComponent(string $name, array $data = [], ?KernelBrowser $client = null): TestLiveComponent
+    protected function createLiveComponent(string $name, array $data = [], KernelBrowser $client = null): TestLiveComponent
     {
         if (!$this instanceof KernelTestCase) {
             throw new \LogicException(sprintf('The "%s" trait can only be used on "%s" classes.', __TRAIT__, KernelTestCase::class));
diff --git a/src/LiveComponent/src/Test/TestLiveComponent.php b/src/LiveComponent/src/Test/TestLiveComponent.php
index c1e2b264..fcfa8b9a 100644
--- a/src/LiveComponent/src/Test/TestLiveComponent.php
+++ b/src/LiveComponent/src/Test/TestLiveComponent.php
@@ -148,7 +148,7 @@ final class TestLiveComponent
         return $this->client->getResponse();
     }
 
-    private function request(array $content = [], ?string $action = null): self
+    private function request(array $content = [], string $action = null): self
     {
         $csrfToken = $this->csrfToken();
 
diff --git a/src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php b/src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php
index 68873886..658245e0 100644
--- a/src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php
+++ b/src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php
@@ -193,7 +193,7 @@ class ComponentWithFormTest extends KernelTestCase
             ],
         ], $dehydratedProps['form']);
 
-        $getUrl = function (array $props, ?array $updatedProps = null) {
+        $getUrl = function (array $props, array $updatedProps = null) {
             $url = '/_components/form_with_many_different_fields_type?props='.urlencode(json_encode($props));
             if (null !== $updatedProps) {
                 $url .= '&updated='.urlencode(json_encode($updatedProps));
diff --git a/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php b/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php
index 7606dd32..4bb60d82 100644
--- a/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php
+++ b/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php
@@ -52,7 +52,7 @@ final class LiveComponentHydratorTest extends KernelTestCase
     use LiveComponentTestHelper;
     use ResetDatabase;
 
-    private function executeHydrationTestCase(callable $testFactory, ?int $minPhpVersion = null): void
+    private function executeHydrationTestCase(callable $testFactory, int $minPhpVersion = null): void
     {
         if (null !== $minPhpVersion && $minPhpVersion > \PHP_VERSION_ID) {
             $this->markTestSkipped(sprintf('Test requires PHP version %s or higher.', $minPhpVersion));
@@ -140,7 +140,7 @@ final class LiveComponentHydratorTest extends KernelTestCase
     /**
      * @dataProvider provideDehydrationHydrationTests
      */
-    public function testCanDehydrateAndHydrateComponentWithTestCases(callable $testFactory, ?int $minPhpVersion = null): void
+    public function testCanDehydrateAndHydrateComponentWithTestCases(callable $testFactory, int $minPhpVersion = null): void
     {
         $this->executeHydrationTestCase($testFactory, $minPhpVersion);
     }
@@ -1199,7 +1199,7 @@ final class LiveComponentHydratorTest extends KernelTestCase
     /**
      * @dataProvider provideInvalidHydrationTests
      */
-    public function testInvalidTypeHydration(callable $testFactory, ?int $minPhpVersion = null): void
+    public function testInvalidTypeHydration(callable $testFactory, int $minPhpVersion = null): void
     {
         $this->executeHydrationTestCase($testFactory, $minPhpVersion);
     }
@@ -1614,7 +1614,7 @@ class HydrationTest
         );
     }
 
-    public function expectsExceptionDuringHydration(string $exceptionClass, ?string $exceptionMessage = null): self
+    public function expectsExceptionDuringHydration(string $exceptionClass, string $exceptionMessage = null): self
     {
         $this->expectedHydrationException = $exceptionClass;
         $this->expectHydrationExceptionMessage = $exceptionMessage;
diff --git a/src/TwigComponent/src/Test/InteractsWithTwigComponents.php b/src/TwigComponent/src/Test/InteractsWithTwigComponents.php
index 810e332c..d9a50533 100644
--- a/src/TwigComponent/src/Test/InteractsWithTwigComponents.php
+++ b/src/TwigComponent/src/Test/InteractsWithTwigComponents.php
@@ -30,7 +30,7 @@ trait InteractsWithTwigComponents
     /**
      * @param array<string,string> $blocks
      */
-    protected function renderTwigComponent(string $name, array $data = [], ?string $content = null, array $blocks = []): RenderedComponent
+    protected function renderTwigComponent(string $name, array $data = [], string $content = null, array $blocks = []): RenderedComponent
     {
         if (!$this instanceof KernelTestCase) {
             throw new \LogicException(sprintf('The "%s" trait can only be used on "%s" classes.', __TRAIT__, KernelTestCase::class));
diff --git a/src/TwigComponent/tests/Fixtures/templates/components/RenderAttributes.html.twig b/src/TwigComponent/tests/Fixtures/templates/components/RenderAttributes.html.twig
deleted file mode 100644
index 59f13268..00000000
--- a/src/TwigComponent/tests/Fixtures/templates/components/RenderAttributes.html.twig
+++ /dev/null
@@ -1,7 +0,0 @@
-<div
-    foo="{{ attributes.render('foo') }}"
-    bar="{{ attributes.render('bar')|default('default') }}"
-    baz="default {{ attributes.render('baz') }}"
-    qux="{{ attributes.render('qux') }} default"
-    {{ attributes }}
-/>
diff --git a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php
index ee09fca9..a65c8f3e 100644
--- a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php
+++ b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php
@@ -216,51 +216,6 @@ final class ComponentExtensionTest extends KernelTestCase
         $this->assertStringContainsString('Hello FOO, 123, and 456', $output);
     }
 
-    /**
-     * @dataProvider renderingAttributesManuallyProvider
-     */
-    public function testRenderingAttributesManually(array $attributes, string $expected): void
-    {
-        $actual = trim($this->renderComponent('RenderAttributes', $attributes));
-
-        $this->assertSame($expected, trim($actual));
-    }
-
-    public static function renderingAttributesManuallyProvider(): iterable
-    {
-        yield [
-            ['class' => 'block'],
-            <<<HTML
-            <div
-                foo=""
-                bar="default"
-                baz="default "
-                qux=" default"
-                 class="block"
-            />
-            HTML,
-        ];
-
-        yield [
-            [
-                'class' => 'block',
-                'foo' => 'value',
-                'bar' => 'value',
-                'baz' => 'value',
-                'qux' => 'value',
-            ],
-            <<<HTML
-            <div
-                foo="value"
-                bar="value"
-                baz="default value"
-                qux="value default"
-                 class="block"
-            />
-            HTML,
-        ];
-    }
-
     private function renderComponent(string $name, array $data = []): string
     {
         return self::getContainer()->get(Environment::class)->render('render_component.html.twig', [
diff --git a/src/TwigComponent/tests/Unit/ComponentAttributesTest.php b/src/TwigComponent/tests/Unit/ComponentAttributesTest.php
index 7cad387a..35fcb5b7 100644
--- a/src/TwigComponent/tests/Unit/ComponentAttributesTest.php
+++ b/src/TwigComponent/tests/Unit/ComponentAttributesTest.php
@@ -199,29 +199,4 @@ final class ComponentAttributesTest extends TestCase
         $this->assertSame($attributes->all(), iterator_to_array($attributes));
         $this->assertCount(1, $attributes);
     }
-
-    public function testRenderSingleAttribute(): void
-    {
-        $attributes = new ComponentAttributes(['attr1' => 'value1', 'attr2' => 'value2']);
-
-        $this->assertSame('value1', $attributes->render('attr1'));
-        $this->assertNull($attributes->render('attr3'));
-    }
-
-    public function testRenderingSingleAttributeExcludesFromString(): void
-    {
-        $attributes = new ComponentAttributes(['attr1' => 'value1', 'attr2' => 'value2']);
-
-        $this->assertSame('value1', $attributes->render('attr1'));
-        $this->assertSame(' attr2="value2"', (string) $attributes);
-    }
-
-    public function testCannotRenderNonStringAttribute(): void
-    {
-        $attributes = new ComponentAttributes(['attr1' => false]);
-
-        $this->expectException(\LogicException::class);
-
-        $attributes->render('attr1');
-    }
 }