diff --git a/UPGRADE.md b/UPGRADE.md index eee363f68..f3733d72b 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -67,6 +67,123 @@ Example migration: + icon: rz-icon-rz--status-draft-line ``` +## ⚠ Doctrine ORM 3 upgrade + +Roadiz 2.8 upgrades to **Doctrine ORM 3.6**, **Doctrine DBAL 4.4**, and **Doctrine Persistence 4.2**. This is a major dependency change that requires updates in your project code. + +### Updated packages + +| Package | Old version | New version | +|---------|------------|-------------| +| `doctrine/orm` | `~2.20.0` | `^3.6` | +| `doctrine/dbal` | `^3.10` | `^4.4` | +| `doctrine/persistence` | `^3.4` | `^4.2` | +| `doctrine/doctrine-bundle` | `^2.8` | `^2.19` | +| `doctrine/doctrine-fixtures-bundle` | `^3.6` | `^4.3` | +| `scienta/doctrine-json-functions` | `^4.2` | `^6.0` | + +Update your `composer.json` accordingly: + +```diff + "require": { +- "doctrine/doctrine-bundle": "^2.8.1", +- "doctrine/orm": "~2.20.0", +- "scienta/doctrine-json-functions": "^4.2", ++ "doctrine/doctrine-bundle": "^2.19", ++ "doctrine/orm": "^3.6", ++ "scienta/doctrine-json-functions": "^6.0", + }, + "require-dev": { +- "doctrine/doctrine-fixtures-bundle": "^3.6", ++ "doctrine/doctrine-fixtures-bundle": "^4.3", + } +``` + +### Doctrine configuration changes + +Remove `enable_lazy_ghost_objects` from your `config/packages/doctrine.yaml` (always-on in ORM 3): + +```diff + doctrine: + orm: + auto_generate_proxy_classes: true +- enable_lazy_ghost_objects: true +``` + +Register the backward-compatible `array` Doctrine DBAL type. The built-in `array` type was removed in DBAL 4, but `gedmo/doctrine-extensions` `AbstractLogEntry` still references it. Roadiz now ships a replacement type that extends `JsonType` (always writes JSON, reads both JSON and legacy PHP-serialized data): + +```diff + doctrine: + dbal: + url: '%env(resolve:DATABASE_URL)%' ++ types: ++ array: ++ class: RZ\Roadiz\CoreBundle\Doctrine\DBAL\Types\ArrayType +``` + +### Code changes required in your project + +**1. Replace `$this->_em` with `$this->getEntityManager()` in custom repositories** + +The protected `$_em` property is no longer accessible on `ServiceEntityRepository`. Use `$this->getEntityManager()` instead. + +```diff +-$query = $this->_em->createQuery('...'); ++$query = $this->getEntityManager()->createQuery('...'); +``` + +**2. Remove `cascade: ['merge']` and `cascade: ['all']` from entity mappings** + +The `merge` cascade operation is removed in ORM 3. Replace: +- `cascade: ['persist', 'merge']` with `cascade: ['persist']` +- `cascade: ['all']` with explicit cascades: `cascade: ['persist', 'remove']` (or just `cascade: ['persist']` for ManyToOne) + +**3. Replace `ClassMetadataInfo` with `ClassMetadata`** + +`Doctrine\ORM\Mapping\ClassMetadataInfo` is removed. Use `Doctrine\ORM\Mapping\ClassMetadata` instead. + +**4. Replace `EntityManager::detach()` calls** + +`EntityManager::detach()` is removed in ORM 3. Use `$em->clear()` for batch processing memory management, or extract entity data into plain arrays before removal. + +**5. Remove `JoinTable` from inverse ManyToMany sides** + +ORM 3 rejects `#[ORM\JoinTable]` on the inverse side (`mappedBy`) of a ManyToMany relationship. Only the owning side (`inversedBy`) should define the join table. + +**6. Replace `setParameters(array)` with individual `setParameter()` calls** + +`QueryBuilder::setParameters()` no longer accepts plain arrays. Use chained `setParameter()` calls instead: + +```diff +-$qb->setParameters([ +- 'foo' => $foo, +- 'bar' => $bar, +-]); ++$qb->setParameter('foo', $foo) ++ ->setParameter('bar', $bar); +``` + +**7. Replace `setFirstResult(null)` with `setFirstResult(0)`** + +`Query::setFirstResult()` no longer accepts `null`. + +**8. Remove legacy Doctrine Cache API usage** + +`Configuration::getResultCacheImpl()` and `Doctrine\Common\Cache\CacheProvider` are removed. Use `Configuration::getResultCache()` (PSR-6) instead: + +```diff +-use Doctrine\Common\Cache\CacheProvider; +-if ($configuration->getResultCacheImpl() instanceof CacheProvider) { +- $configuration->getResultCacheImpl()->deleteAll(); +-} ++$resultCache = $configuration->getResultCache(); ++$resultCache?->clear(); +``` + +**9. Remove `@throws` annotations for `ORMException` and `OptimisticLockException`** + +These classes are no longer `Throwable` in ORM 3. Remove any `@throws` PHPDoc annotations referencing them. + ## New admin templates New reusable templates for building back-office pages: diff --git a/composer.json b/composer.json index 6d08a63b0..0392ea4ac 100644 --- a/composer.json +++ b/composer.json @@ -53,10 +53,10 @@ "composer/package-versions-deprecated": "1.11.99.3", "deeplcom/deepl-php": "^1.12", "doctrine/collections": ">=1.6", - "doctrine/doctrine-bundle": "^2.8.1", + "doctrine/doctrine-bundle": "^2.19", "doctrine/doctrine-migrations-bundle": "^3.1", "doctrine/migrations": "^3.1.1", - "doctrine/orm": "~2.20.0", + "doctrine/orm": "^3.6", "dragonmantank/cron-expression": "^3.4", "endroid/qr-code": "^4.0", "enshrined/svg-sanitize": "^0.22", @@ -86,7 +86,7 @@ "scheb/2fa-google-authenticator": "^7.5", "scheb/2fa-totp": "^7.5", "scheb/2fa-trusted-device": "^7.5", - "scienta/doctrine-json-functions": "^4.2", + "scienta/doctrine-json-functions": "^6.0", "sentry/sentry-symfony": "^5.1", "solarium/solarium": "^6.3.6", "symfony-cmf/routing-bundle": "^3.1.0", @@ -149,7 +149,7 @@ "require-dev": { "async-aws/simple-s3": "^1.1", "deptrac/deptrac": "^3.0", - "doctrine/doctrine-fixtures-bundle": "^3.6", + "doctrine/doctrine-fixtures-bundle": "^4.3", "friendsofphp/php-cs-fixer": "^3.64", "league/flysystem-async-aws-s3": "^3.10", "league/flysystem-aws-s3-v3": "^3.10", diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 84f7975bb..c07ec3e28 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -3,10 +3,12 @@ doctrine: url: '%env(resolve:DATABASE_URL)%' profiling_collect_backtrace: '%kernel.debug%' use_savepoints: true + types: + array: + class: RZ\Roadiz\CoreBundle\Doctrine\DBAL\Types\ArrayType orm: auto_generate_proxy_classes: true default_entity_manager: default - enable_lazy_ghost_objects: true controller_resolver: auto_mapping: true entity_managers: diff --git a/config/reference.php b/config/reference.php index bf4d52e23..4c109f549 100644 --- a/config/reference.php +++ b/config/reference.php @@ -968,7 +968,7 @@ * options?: array, * mapping_types?: array, * default_table_options?: array, - * schema_manager_factory?: scalar|Param|null, // Default: "doctrine.dbal.legacy_schema_manager_factory" + * schema_manager_factory?: scalar|Param|null, // Default: "doctrine.dbal.default_schema_manager_factory" * result_cache?: scalar|Param|null, * slaves?: array, - * report_fields_where_declared?: bool|Param, // Set to "true" to opt-in to the new mapping driver mode that was added in Doctrine ORM 2.16 and will be mandatory in ORM 3.0. See https://github.com/doctrine/orm/pull/10455. // Default: false + * report_fields_where_declared?: bool|Param, // Set to "true" to opt-in to the new mapping driver mode that was added in Doctrine ORM 2.16 and will be mandatory in ORM 3.0. See https://github.com/doctrine/orm/pull/10455. // Default: true * validate_xml_mapping?: bool|Param, // Set to "true" to opt-in to the new mapping driver mode that was added in Doctrine ORM 2.14. See https://github.com/doctrine/orm/pull/6728. // Default: false * second_level_cache?: array{ * region_cache_driver?: string|array{ diff --git a/lib/Documents/composer.json b/lib/Documents/composer.json index 44bfb1ec1..505a1d978 100644 --- a/lib/Documents/composer.json +++ b/lib/Documents/composer.json @@ -25,7 +25,7 @@ "ext-zip": "*", "ext-simplexml": "*", "ext-fileinfo": "*", - "doctrine/orm": "~2.20.0", + "doctrine/orm": "^3.6", "enshrined/svg-sanitize": "^0.22", "intervention/image": "^3.11", "league/flysystem": "^3.0", @@ -43,7 +43,7 @@ }, "require-dev": { "api-platform/metadata": "^4.1.18", - "doctrine/doctrine-bundle": "^2.8.1", + "doctrine/doctrine-bundle": "^2.19", "php-coveralls/php-coveralls": "^2.4", "phpstan/phpstan": "^2.1.36", "phpstan/phpdoc-parser": "<2", diff --git a/lib/Models/composer.json b/lib/Models/composer.json index b063a894a..9b284736e 100644 --- a/lib/Models/composer.json +++ b/lib/Models/composer.json @@ -19,7 +19,7 @@ ], "require": { "php": ">=8.3", - "doctrine/orm": "~2.20.0", + "doctrine/orm": "^3.6", "api-platform/doctrine-orm": "^4.1.18", "api-platform/metadata": "^4.1.18", "roadiz/nodetype-contracts": "^3.1.1", @@ -31,7 +31,7 @@ "symfony/validator": "7.4.*" }, "require-dev": { - "doctrine/doctrine-bundle": "^2.8.1", + "doctrine/doctrine-bundle": "^2.19", "php-coveralls/php-coveralls": "^2.4", "phpstan/phpstan": "^2.1.36", "phpstan/phpdoc-parser": "<2", diff --git a/lib/RoadizCoreBundle/composer.json b/lib/RoadizCoreBundle/composer.json index 7c7eee72b..d38908264 100644 --- a/lib/RoadizCoreBundle/composer.json +++ b/lib/RoadizCoreBundle/composer.json @@ -26,9 +26,9 @@ "ext-mbstring": "*", "api-platform/doctrine-orm": "^4.1.18", "api-platform/symfony": "^4.1.18", - "doctrine/doctrine-bundle": "^2.8.1", + "doctrine/doctrine-bundle": "^2.19", "doctrine/doctrine-migrations-bundle": "^3.1", - "doctrine/orm": "~2.20.0", + "doctrine/orm": "^3.6", "dragonmantank/cron-expression": "^3.4", "gedmo/doctrine-extensions": "^3.19.0", "inlinestyle/inlinestyle": "~1.2.7", @@ -49,7 +49,7 @@ "roadiz/models": "2.8.x-dev", "roadiz/nodetype-contracts": "^3.1.1", "roadiz/random": "2.8.x-dev", - "scienta/doctrine-json-functions": "^4.2", + "scienta/doctrine-json-functions": "^6.0", "symfony-cmf/routing-bundle": "^3.1.0", "symfony/asset": "7.4.*", "symfony/cache": "7.4.*", diff --git a/lib/RoadizCoreBundle/migrations/Version20260602204708.php b/lib/RoadizCoreBundle/migrations/Version20260602204708.php new file mode 100644 index 000000000..524f2fb84 --- /dev/null +++ b/lib/RoadizCoreBundle/migrations/Version20260602204708.php @@ -0,0 +1,63 @@ +connection->fetchAllAssociative( + // Skip rows that already look like JSON ([...] or {...}); only legacy serialized payloads remain. + "SELECT id, data FROM user_log_entries WHERE data IS NOT NULL AND data <> '' AND data NOT LIKE '[%' AND data NOT LIKE '{%'" + ); + + foreach ($rows as $row) { + $value = @unserialize((string) $row['data'], ['allowed_classes' => false]); + if (false === $value && 'b:0;' !== $row['data']) { + // Not valid PHP-serialized data: store as JSON null to keep the column castable to JSON. + $this->addSql( + 'UPDATE user_log_entries SET data = :data WHERE id = :id', + ['data' => 'null', 'id' => $row['id']] + ); + continue; + } + + $json = json_encode($value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $this->addSql( + 'UPDATE user_log_entries SET data = :data WHERE id = :id', + ['data' => $json, 'id' => $row['id']] + ); + } + } + + public function down(Schema $schema): void + { + $rows = $this->connection->fetchAllAssociative( + "SELECT id, data FROM user_log_entries WHERE data IS NOT NULL AND data <> ''" + ); + + foreach ($rows as $row) { + $value = json_decode((string) $row['data'], true); + $this->addSql( + 'UPDATE user_log_entries SET data = :data WHERE id = :id', + ['data' => serialize($value), 'id' => $row['id']] + ); + } + } +} diff --git a/lib/RoadizCoreBundle/migrations/Version20260602204709.php b/lib/RoadizCoreBundle/migrations/Version20260602204709.php new file mode 100644 index 000000000..140c86357 --- /dev/null +++ b/lib/RoadizCoreBundle/migrations/Version20260602204709.php @@ -0,0 +1,37 @@ +addSql('ALTER TABLE attribute_translations CHANGE options options LONGTEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE nodes_tags CHANGE position position DOUBLE PRECISION DEFAULT 1 NOT NULL, CHANGE id id BINARY(16) NOT NULL'); + $this->addSql('ALTER TABLE user_log_entries CHANGE data data JSON DEFAULT NULL'); + $this->addSql('ALTER TABLE webhooks CHANGE id id BINARY(16) NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE attribute_translations CHANGE options options LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:simple_array)\''); + $this->addSql('ALTER TABLE nodes_tags CHANGE position position DOUBLE PRECISION DEFAULT \'1\' NOT NULL, CHANGE id id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\''); + $this->addSql('ALTER TABLE user_log_entries CHANGE data data LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:array)\''); + $this->addSql('ALTER TABLE webhooks CHANGE id id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\''); + } +} diff --git a/lib/RoadizCoreBundle/src/Api/Extension/ArchiveExtension.php b/lib/RoadizCoreBundle/src/Api/Extension/ArchiveExtension.php index 5947cc3a4..eaa87afa4 100644 --- a/lib/RoadizCoreBundle/src/Api/Extension/ArchiveExtension.php +++ b/lib/RoadizCoreBundle/src/Api/Extension/ArchiveExtension.php @@ -101,7 +101,7 @@ public function getResult( * disable pagination to get all archives */ $paginator->getQuery()->setMaxResults(null); - $paginator->getQuery()->setFirstResult(null); + $paginator->getQuery()->setFirstResult(0); foreach ($paginator as $result) { $dateTimeField = reset($result); diff --git a/lib/RoadizCoreBundle/src/Console/NodesOrphansCommand.php b/lib/RoadizCoreBundle/src/Console/NodesOrphansCommand.php index d0d226e2c..0cc649f8f 100644 --- a/lib/RoadizCoreBundle/src/Console/NodesOrphansCommand.php +++ b/lib/RoadizCoreBundle/src/Console/NodesOrphansCommand.php @@ -35,10 +35,6 @@ protected function configure(): void ); } - /** - * @throws \Doctrine\ORM\ORMException - * @throws \Doctrine\ORM\OptimisticLockException - */ #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/lib/RoadizCoreBundle/src/Doctrine/DBAL/Types/ArrayType.php b/lib/RoadizCoreBundle/src/Doctrine/DBAL/Types/ArrayType.php new file mode 100644 index 000000000..5d1e94815 --- /dev/null +++ b/lib/RoadizCoreBundle/src/Doctrine/DBAL/Types/ArrayType.php @@ -0,0 +1,38 @@ +inheritanceType) { - $metadata->setInheritanceType(ClassMetadataInfo::INHERITANCE_TYPE_JOINED); + $metadata->setInheritanceType(ClassMetadata::INHERITANCE_TYPE_JOINED); } elseif (Configuration::INHERITANCE_TYPE_SINGLE_TABLE === $this->inheritanceType) { - $metadata->setInheritanceType(ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_TABLE); + $metadata->setInheritanceType(ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE); /* * If inheritance type is single table, we need to set indexes on parent class: NodesSources */ diff --git a/lib/RoadizCoreBundle/src/Entity/Attribute.php b/lib/RoadizCoreBundle/src/Entity/Attribute.php index ec6ea25dd..ff2030cd9 100644 --- a/lib/RoadizCoreBundle/src/Entity/Attribute.php +++ b/lib/RoadizCoreBundle/src/Entity/Attribute.php @@ -38,9 +38,9 @@ class Attribute implements AttributeInterface * @var Collection */ #[ORM\OneToMany( - mappedBy: 'attribute', targetEntity: AttributeDocuments::class, - cascade: ['persist', 'merge'], + mappedBy: 'attribute', + cascade: ['persist'], orphanRemoval: true ), ORM\OrderBy(['position' => 'ASC']), diff --git a/lib/RoadizCoreBundle/src/Entity/AttributeDocuments.php b/lib/RoadizCoreBundle/src/Entity/AttributeDocuments.php index edd205bee..80cfd5df4 100644 --- a/lib/RoadizCoreBundle/src/Entity/AttributeDocuments.php +++ b/lib/RoadizCoreBundle/src/Entity/AttributeDocuments.php @@ -29,7 +29,7 @@ class AttributeDocuments implements PositionedInterface, PersistableInterface public function __construct( #[ORM\ManyToOne( targetEntity: Attribute::class, - cascade: ['persist', 'merge'], + cascade: ['persist'], fetch: 'EAGER', inversedBy: 'attributeDocuments' ), @@ -43,7 +43,7 @@ public function __construct( protected Attribute $attribute, #[ORM\ManyToOne( targetEntity: Document::class, - cascade: ['persist', 'merge'], + cascade: ['persist'], fetch: 'EAGER', inversedBy: 'attributeDocuments' ), diff --git a/lib/RoadizCoreBundle/src/Entity/Document.php b/lib/RoadizCoreBundle/src/Entity/Document.php index 8084a79fe..af3213b95 100644 --- a/lib/RoadizCoreBundle/src/Entity/Document.php +++ b/lib/RoadizCoreBundle/src/Entity/Document.php @@ -40,23 +40,23 @@ #[ORM\Entity(repositoryClass: DocumentRepository::class), ORM\Table(name: 'documents'), ORM\HasLifecycleCallbacks, - ORM\Index(columns: ['created_at'], name: 'document_created_at'), - ORM\Index(columns: ['updated_at'], name: 'document_updated_at'), + ORM\Index(name: 'document_created_at', columns: ['created_at']), + ORM\Index(name: 'document_updated_at', columns: ['updated_at']), ORM\Index(columns: ['raw']), - ORM\Index(columns: ['raw', 'created_at'], name: 'document_raw_created_at'), + ORM\Index(name: 'document_raw_created_at', columns: ['raw', 'created_at']), ORM\Index(columns: ['private']), - ORM\Index(columns: ['filename'], name: 'document_filename'), - ORM\Index(columns: ['file_hash'], name: 'document_file_hash'), - ORM\Index(columns: ['file_hash_algorithm'], name: 'document_hash_algorithm'), - ORM\Index(columns: ['file_hash', 'file_hash_algorithm'], name: 'document_file_hash_algorithm'), - ORM\Index(columns: ['embedId'], name: 'document_embed_id'), - ORM\Index(columns: ['embedId', 'embedPlatform'], name: 'document_embed_platform_id'), - ORM\Index(columns: ['embedPlatform'], name: 'document_embed_platform'), + ORM\Index(name: 'document_filename', columns: ['filename']), + ORM\Index(name: 'document_file_hash', columns: ['file_hash']), + ORM\Index(name: 'document_hash_algorithm', columns: ['file_hash_algorithm']), + ORM\Index(name: 'document_file_hash_algorithm', columns: ['file_hash', 'file_hash_algorithm']), + ORM\Index(name: 'document_embed_id', columns: ['embedId']), + ORM\Index(name: 'document_embed_platform_id', columns: ['embedId', 'embedPlatform']), + ORM\Index(name: 'document_embed_platform', columns: ['embedPlatform']), ORM\Index(columns: ['raw', 'private']), - ORM\Index(columns: ['duration'], name: 'document_duration'), - ORM\Index(columns: ['filesize'], name: 'document_filesize'), - ORM\Index(columns: ['imageWidth'], name: 'document_image_width'), - ORM\Index(columns: ['imageHeight'], name: 'document_image_height'), + ORM\Index(name: 'document_duration', columns: ['duration']), + ORM\Index(name: 'document_filesize', columns: ['filesize']), + ORM\Index(name: 'document_image_width', columns: ['imageWidth']), + ORM\Index(name: 'document_image_height', columns: ['imageHeight']), ORM\Index(columns: ['mime_type']), ApiFilter(PropertyFilter::class), ApiFilter(BaseFilter\OrderFilter::class, properties: [ @@ -138,7 +138,7 @@ class Document implements \Stringable, AdvancedDocumentInterface, HasThumbnailIn #[ORM\ManyToOne( targetEntity: Document::class, - cascade: ['all'], + cascade: ['persist'], fetch: 'EXTRA_LAZY', inversedBy: 'downscaledDocuments' )] @@ -206,7 +206,6 @@ class Document implements \Stringable, AdvancedDocumentInterface, HasThumbnailIn /** * @var Collection */ - #[ORM\JoinTable(name: 'documents_folders')] #[ORM\ManyToMany(targetEntity: Folder::class, mappedBy: 'documents')] #[SymfonySerializer\Ignore] protected Collection $folders; @@ -214,8 +213,8 @@ class Document implements \Stringable, AdvancedDocumentInterface, HasThumbnailIn * @var Collection */ #[ORM\OneToMany( - mappedBy: 'document', targetEntity: DocumentTranslation::class, + mappedBy: 'document', orphanRemoval: true )] #[SymfonySerializer\Ignore] @@ -238,7 +237,7 @@ class Document implements \Stringable, AdvancedDocumentInterface, HasThumbnailIn /** * @var Collection */ - #[ORM\OneToMany(mappedBy: 'rawDocument', targetEntity: Document::class, fetch: 'EXTRA_LAZY')] + #[ORM\OneToMany(targetEntity: Document::class, mappedBy: 'rawDocument', fetch: 'EXTRA_LAZY')] #[SymfonySerializer\Ignore] private Collection $downscaledDocuments; #[ORM\Column(type: 'string', length: 12, nullable: true)] diff --git a/lib/RoadizCoreBundle/src/Entity/NodesSources.php b/lib/RoadizCoreBundle/src/Entity/NodesSources.php index 040fedceb..8f8208b04 100644 --- a/lib/RoadizCoreBundle/src/Entity/NodesSources.php +++ b/lib/RoadizCoreBundle/src/Entity/NodesSources.php @@ -130,7 +130,7 @@ class NodesSources implements PersistableInterface, Loggable, \Stringable #[ORM\OneToMany( mappedBy: 'nodeSource', targetEntity: UrlAlias::class, - cascade: ['all'] + cascade: ['persist', 'remove'] )] #[SymfonySerializer\Ignore] private Collection $urlAliases; diff --git a/lib/RoadizCoreBundle/src/Entity/NodesTags.php b/lib/RoadizCoreBundle/src/Entity/NodesTags.php index 4e8b74fe3..5cae5c9b5 100644 --- a/lib/RoadizCoreBundle/src/Entity/NodesTags.php +++ b/lib/RoadizCoreBundle/src/Entity/NodesTags.php @@ -15,11 +15,11 @@ #[ORM\Entity(repositoryClass: NodesTagsRepository::class), ORM\Table(name: 'nodes_tags'), ORM\HasLifecycleCallbacks, - ORM\Index(columns: ['node_id', 'position'], name: 'nodes_tags_node_id_position'), - ORM\Index(columns: ['tag_id', 'position'], name: 'nodes_tags_tag_id_position'), - ORM\Index(columns: ['position'], name: 'nodes_tags_position'), - ORM\Index(columns: ['tag_id'], name: 'nodes_tags_tag_id'), - ORM\Index(columns: ['node_id'], name: 'nodes_tags_node_id'),] + ORM\Index(name: 'nodes_tags_node_id_position', columns: ['node_id', 'position']), + ORM\Index(name: 'nodes_tags_tag_id_position', columns: ['tag_id', 'position']), + ORM\Index(name: 'nodes_tags_position', columns: ['position']), + ORM\Index(name: 'nodes_tags_tag_id', columns: ['tag_id']), + ORM\Index(name: 'nodes_tags_node_id', columns: ['node_id']),] class NodesTags implements PositionedInterface, PersistableInterface { use UuidTrait; @@ -47,7 +47,7 @@ class NodesTags implements PositionedInterface, PersistableInterface SymfonySerializer\Groups(['nodes_sources', 'nodes_sources_base', 'node']),] private Tag $tag; - #[ORM\Column(type: 'float', nullable: false, options: ['default' => 1]), + #[ORM\Column(type: 'float', nullable: false, options: ['default' => '1']), SymfonySerializer\Ignore,] protected float $position = 0.0; diff --git a/lib/RoadizCoreBundle/src/Entity/Setting.php b/lib/RoadizCoreBundle/src/Entity/Setting.php index b5dc3cbec..c0be81504 100644 --- a/lib/RoadizCoreBundle/src/Entity/Setting.php +++ b/lib/RoadizCoreBundle/src/Entity/Setting.php @@ -71,7 +71,7 @@ class Setting implements PersistableInterface #[ORM\ManyToOne( targetEntity: SettingGroup::class, - cascade: ['persist', 'merge'], + cascade: ['persist'], inversedBy: 'settings' )] #[ORM\JoinColumn(name: 'setting_group_id', referencedColumnName: 'id', onDelete: 'SET NULL')] diff --git a/lib/RoadizCoreBundle/src/Entity/Tag.php b/lib/RoadizCoreBundle/src/Entity/Tag.php index 98cb30f51..5b56b2815 100644 --- a/lib/RoadizCoreBundle/src/Entity/Tag.php +++ b/lib/RoadizCoreBundle/src/Entity/Tag.php @@ -97,7 +97,7 @@ class Tag implements DateTimedInterface, LeafInterface, PersistableInterface, \S #[ORM\OneToMany( mappedBy: 'parent', targetEntity: Tag::class, - cascade: ['persist', 'merge'], + cascade: ['persist'], orphanRemoval: true )] #[ORM\OrderBy(['position' => 'ASC'])] @@ -110,7 +110,7 @@ class Tag implements DateTimedInterface, LeafInterface, PersistableInterface, \S #[ORM\OneToMany( mappedBy: 'tag', targetEntity: TagTranslation::class, - cascade: ['all'], + cascade: ['persist', 'remove'], orphanRemoval: true )] #[SymfonySerializer\Groups(['translated_tag'])] diff --git a/lib/RoadizCoreBundle/src/Entity/TagTranslation.php b/lib/RoadizCoreBundle/src/Entity/TagTranslation.php index 3ef58d7f2..08c1cf8f8 100644 --- a/lib/RoadizCoreBundle/src/Entity/TagTranslation.php +++ b/lib/RoadizCoreBundle/src/Entity/TagTranslation.php @@ -48,7 +48,7 @@ class TagTranslation implements PersistableInterface #[ORM\OneToMany( mappedBy: 'tagTranslation', targetEntity: TagTranslationDocuments::class, - cascade: ['persist', 'merge'], + cascade: ['persist'], orphanRemoval: true )] #[ORM\OrderBy(['position' => 'ASC'])] diff --git a/lib/RoadizCoreBundle/src/Entity/TagTranslationDocuments.php b/lib/RoadizCoreBundle/src/Entity/TagTranslationDocuments.php index a6a2d5164..ce49fd106 100644 --- a/lib/RoadizCoreBundle/src/Entity/TagTranslationDocuments.php +++ b/lib/RoadizCoreBundle/src/Entity/TagTranslationDocuments.php @@ -32,7 +32,7 @@ class TagTranslationDocuments implements PositionedInterface, PersistableInterfa public function __construct( #[ORM\ManyToOne( targetEntity: TagTranslation::class, - cascade: ['persist', 'merge'], + cascade: ['persist'], fetch: 'EAGER', inversedBy: 'tagTranslationDocuments' )] @@ -41,7 +41,7 @@ public function __construct( protected TagTranslation $tagTranslation, #[ORM\ManyToOne( targetEntity: Document::class, - cascade: ['persist', 'merge'], + cascade: ['persist'], fetch: 'EAGER', inversedBy: 'tagTranslations' )] diff --git a/lib/RoadizCoreBundle/src/Entity/UserLogEntry.php b/lib/RoadizCoreBundle/src/Entity/UserLogEntry.php index 73e731d47..e001efa22 100644 --- a/lib/RoadizCoreBundle/src/Entity/UserLogEntry.php +++ b/lib/RoadizCoreBundle/src/Entity/UserLogEntry.php @@ -4,19 +4,155 @@ namespace RZ\Roadiz\CoreBundle\Entity; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -use Gedmo\Loggable\Entity\MappedSuperclass\AbstractLogEntry; +use Gedmo\Loggable\LogEntryInterface; use RZ\Roadiz\CoreBundle\Repository\UserLogEntryRepository; /** * Add User to Gedmo\Loggable\Entity\LogEntry. */ -#[ORM\Entity(repositoryClass: UserLogEntryRepository::class), - ORM\Table(name: 'user_log_entries', options: ['row_format' => 'DYNAMIC']), - ORM\Index(columns: ['object_class'], name: 'log_class_lookup_idx'), - ORM\Index(columns: ['logged_at'], name: 'log_date_lookup_idx'), - ORM\Index(columns: ['username'], name: 'log_user_lookup_idx'), - ORM\Index(columns: ['object_id', 'object_class', 'version'], name: 'log_version_lookup_idx')] -class UserLogEntry extends AbstractLogEntry +#[ORM\Entity(repositoryClass: UserLogEntryRepository::class)] +#[ORM\Table(name: 'user_log_entries', options: ['row_format' => 'DYNAMIC'])] +#[ORM\Index(name: 'log_class_lookup_idx', columns: ['object_class'])] +#[ORM\Index(name: 'log_date_lookup_idx', columns: ['logged_at'])] +#[ORM\Index(name: 'log_user_lookup_idx', columns: ['username'])] +#[ORM\Index(name: 'log_version_lookup_idx', columns: ['object_id', 'object_class', 'version'])] +class UserLogEntry implements LogEntryInterface { + #[ORM\Column(type: Types::INTEGER)] + #[ORM\Id] + #[ORM\GeneratedValue] + protected ?int $id; + + /** + * @var self::ACTION_CREATE|self::ACTION_UPDATE|self::ACTION_REMOVE|null + */ + #[ORM\Column(type: Types::STRING, length: 8)] + protected ?string $action; + + /** + * @var \DateTime|null + */ + #[ORM\Column(name: 'logged_at', type: Types::DATETIME_MUTABLE)] + protected ?\DateTime $loggedAt; + + /** + * @var string|null + */ + #[ORM\Column(name: 'object_id', length: 64, nullable: true)] + protected ?string $objectId; + + /** + * @var class-string|null + */ + #[ORM\Column(name: 'object_class', type: Types::STRING, length: 191)] + protected ?string $objectClass; + + /** + * @var int|null + */ + #[ORM\Column(type: Types::INTEGER)] + protected ?int $version; + + + /** + * @var array|null + */ + #[ORM\Column(type: Types::JSON, nullable: true)] + protected ?array $data; + + /** + * @var string|null + */ + #[ORM\Column(length: 191, nullable: true)] + protected ?string $username; + + public function getId(): ?int + { + return $this->id; + } + + public function setId(?int $id): UserLogEntry + { + $this->id = $id; + return $this; + } + + public function getAction(): ?string + { + return $this->action; + } + + public function setAction(?string $action): UserLogEntry + { + $this->action = $action; + return $this; + } + + public function getLoggedAt(): ?\DateTime + { + return $this->loggedAt; + } + + public function setLoggedAt(): UserLogEntry + { + $this->loggedAt = new \DateTime(); + return $this; + } + + public function getObjectId(): ?string + { + return $this->objectId; + } + + public function setObjectId(?string $objectId): UserLogEntry + { + $this->objectId = $objectId; + return $this; + } + + public function getObjectClass(): ?string + { + return $this->objectClass; + } + + public function setObjectClass(?string $objectClass): UserLogEntry + { + $this->objectClass = $objectClass; + return $this; + } + + public function getVersion(): ?int + { + return $this->version; + } + + public function setVersion(?int $version): UserLogEntry + { + $this->version = $version; + return $this; + } + + public function getData(): ?array + { + return $this->data; + } + + public function setData(?array $data): UserLogEntry + { + $this->data = $data; + return $this; + } + + public function getUsername(): ?string + { + return $this->username; + } + + public function setUsername(?string $username): UserLogEntry + { + $this->username = $username; + return $this; + } } diff --git a/lib/RoadizCoreBundle/src/Entity/Webhook.php b/lib/RoadizCoreBundle/src/Entity/Webhook.php index 5009622d0..79bc2605d 100644 --- a/lib/RoadizCoreBundle/src/Entity/Webhook.php +++ b/lib/RoadizCoreBundle/src/Entity/Webhook.php @@ -14,12 +14,12 @@ #[ORM\Entity(repositoryClass: WebhookRepository::class), ORM\Table(name: 'webhooks'), - ORM\Index(columns: ['message_type'], name: 'webhook_message_type'), - ORM\Index(columns: ['created_at'], name: 'webhook_created_at'), - ORM\Index(columns: ['updated_at'], name: 'webhook_updated_at'), - ORM\Index(columns: ['automatic'], name: 'webhook_automatic'), - ORM\Index(columns: ['root_node'], name: 'webhook_root_node'), - ORM\Index(columns: ['last_triggered_at'], name: 'webhook_last_triggered_at'), + ORM\Index(name: 'webhook_message_type', columns: ['message_type']), + ORM\Index(name: 'webhook_created_at', columns: ['created_at']), + ORM\Index(name: 'webhook_updated_at', columns: ['updated_at']), + ORM\Index(name: 'webhook_automatic', columns: ['automatic']), + ORM\Index(name: 'webhook_root_node', columns: ['root_node']), + ORM\Index(name: 'webhook_last_triggered_at', columns: ['last_triggered_at']), ORM\HasLifecycleCallbacks] class Webhook implements \Stringable, WebhookInterface { diff --git a/lib/RoadizCoreBundle/src/EntityHandler/TranslationHandler.php b/lib/RoadizCoreBundle/src/EntityHandler/TranslationHandler.php index 92e8b917d..baeeded23 100644 --- a/lib/RoadizCoreBundle/src/EntityHandler/TranslationHandler.php +++ b/lib/RoadizCoreBundle/src/EntityHandler/TranslationHandler.php @@ -4,7 +4,6 @@ namespace RZ\Roadiz\CoreBundle\EntityHandler; -use Doctrine\Common\Cache\FlushableCache; use Doctrine\ORM\EntityManagerInterface; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\Core\Handlers\AbstractHandler; @@ -48,12 +47,10 @@ public function makeDefault(): static $this->objectManager->flush(); if ($this->objectManager instanceof EntityManagerInterface) { - $cache = $this->objectManager->getConfiguration()->getResultCacheImpl(); - if ($cache instanceof FlushableCache) { - $cache->flushAll(); - } - if ($cache instanceof ResettableInterface) { - $cache->reset(); + $resultCache = $this->objectManager->getConfiguration()->getResultCache(); + $resultCache?->clear(); + if ($resultCache instanceof ResettableInterface) { + $resultCache->reset(); } } diff --git a/lib/RoadizCoreBundle/src/EventSubscriber/NodesSourcesUniversalSubscriber.php b/lib/RoadizCoreBundle/src/EventSubscriber/NodesSourcesUniversalSubscriber.php index c70b94130..035cfe09a 100644 --- a/lib/RoadizCoreBundle/src/EventSubscriber/NodesSourcesUniversalSubscriber.php +++ b/lib/RoadizCoreBundle/src/EventSubscriber/NodesSourcesUniversalSubscriber.php @@ -4,8 +4,6 @@ namespace RZ\Roadiz\CoreBundle\EventSubscriber; -use Doctrine\ORM\OptimisticLockException; -use Doctrine\ORM\ORMException; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Event\NodesSources\NodesSourcesUpdatedEvent; use RZ\Roadiz\CoreBundle\Node\UniversalDataDuplicator; @@ -25,10 +23,6 @@ public static function getSubscribedEvents(): array ]; } - /** - * @throws ORMException - * @throws OptimisticLockException - */ public function duplicateUniversalContents(NodesSourcesUpdatedEvent $event): void { $source = $event->getNodeSource(); diff --git a/lib/RoadizCoreBundle/src/EventSubscriber/TranslationSubscriber.php b/lib/RoadizCoreBundle/src/EventSubscriber/TranslationSubscriber.php index 11f361811..77de346c8 100644 --- a/lib/RoadizCoreBundle/src/EventSubscriber/TranslationSubscriber.php +++ b/lib/RoadizCoreBundle/src/EventSubscriber/TranslationSubscriber.php @@ -4,7 +4,6 @@ namespace RZ\Roadiz\CoreBundle\EventSubscriber; -use Doctrine\Common\Cache\CacheProvider; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Event\Cache\CachePurgeRequestEvent; @@ -51,11 +50,6 @@ public function purgeCache(Event $event, string $eventName, EventDispatcherInter if ($resultCache instanceof ResettableInterface) { $resultCache->reset(); } - - // Legacy Doctrine result cache provider - if ($configuration->getResultCacheImpl() instanceof CacheProvider) { - $configuration->getResultCacheImpl()->deleteAll(); - } } $dispatcher->dispatch(new CachePurgeRequestEvent()); diff --git a/lib/RoadizCoreBundle/src/Importer/SettingsImporter.php b/lib/RoadizCoreBundle/src/Importer/SettingsImporter.php index f411ef39f..9b96afb1d 100644 --- a/lib/RoadizCoreBundle/src/Importer/SettingsImporter.php +++ b/lib/RoadizCoreBundle/src/Importer/SettingsImporter.php @@ -4,7 +4,6 @@ namespace RZ\Roadiz\CoreBundle\Importer; -use Doctrine\Common\Cache\CacheProvider; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\Setting; @@ -50,11 +49,6 @@ public function import(string $serializedData): bool if ($resultCache instanceof ResettableInterface) { $resultCache->reset(); } - - // Legacy Doctrine result cache provider - if ($configuration->getResultCacheImpl() instanceof CacheProvider) { - $configuration->getResultCacheImpl()->deleteAll(); - } } return true; diff --git a/lib/RoadizCoreBundle/src/ListManager/Paginator.php b/lib/RoadizCoreBundle/src/ListManager/Paginator.php index a9168e5f1..94a413a59 100644 --- a/lib/RoadizCoreBundle/src/ListManager/Paginator.php +++ b/lib/RoadizCoreBundle/src/ListManager/Paginator.php @@ -4,7 +4,7 @@ namespace RZ\Roadiz\CoreBundle\ListManager; -use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; @@ -240,7 +240,7 @@ protected function getSearchQueryBuilder(string $alias): QueryBuilder protected function getSearchableFields(): array { $metadata = $this->em->getClassMetadata($this->entityName); - if (!$metadata instanceof ClassMetadataInfo) { + if (!$metadata instanceof ClassMetadata) { throw new \RuntimeException('Entity has no metadata.'); } diff --git a/lib/RoadizCoreBundle/src/Model/AttributeGroupTrait.php b/lib/RoadizCoreBundle/src/Model/AttributeGroupTrait.php index 4c712ef87..38bbec622 100644 --- a/lib/RoadizCoreBundle/src/Model/AttributeGroupTrait.php +++ b/lib/RoadizCoreBundle/src/Model/AttributeGroupTrait.php @@ -31,9 +31,9 @@ trait AttributeGroupTrait * @var Collection */ #[ORM\OneToMany( - mappedBy: 'attributeGroup', targetEntity: AttributeGroupTranslationInterface::class, - cascade: ['all'], + mappedBy: 'attributeGroup', + cascade: ['persist', 'remove'], orphanRemoval: true ), Serializer\Groups(['attribute_group', 'attribute:export', 'attribute', 'node', 'nodes_sources']),] diff --git a/lib/RoadizCoreBundle/src/Model/AttributeTrait.php b/lib/RoadizCoreBundle/src/Model/AttributeTrait.php index df1663cca..661e67921 100644 --- a/lib/RoadizCoreBundle/src/Model/AttributeTrait.php +++ b/lib/RoadizCoreBundle/src/Model/AttributeTrait.php @@ -35,7 +35,7 @@ trait AttributeTrait #[ORM\ManyToOne( targetEntity: AttributeGroupInterface::class, - cascade: ['persist', 'merge'], + cascade: ['persist'], fetch: 'EAGER', inversedBy: 'attributes' ), @@ -47,9 +47,9 @@ trait AttributeTrait * @var Collection */ #[ORM\OneToMany( - mappedBy: 'attribute', targetEntity: AttributeTranslationInterface::class, - cascade: ['all'], + mappedBy: 'attribute', + cascade: ['persist', 'remove'], fetch: 'EAGER', orphanRemoval: true ), @@ -60,8 +60,8 @@ trait AttributeTrait * @var Collection */ #[ORM\OneToMany( - mappedBy: 'attribute', targetEntity: AttributeValueInterface::class, + mappedBy: 'attribute', cascade: ['persist', 'remove'], fetch: 'EXTRA_LAZY', orphanRemoval: true diff --git a/lib/RoadizCoreBundle/src/Node/NodeTranstyper.php b/lib/RoadizCoreBundle/src/Node/NodeTranstyper.php index 628094d77..1e855b413 100644 --- a/lib/RoadizCoreBundle/src/Node/NodeTranstyper.php +++ b/lib/RoadizCoreBundle/src/Node/NodeTranstyper.php @@ -101,21 +101,23 @@ public function transtype(Node $node, NodeTypeInterface $destinationNodeType, bo * Perform actual trans-typing */ $existingSources = $node->getNodeSources()->toArray(); + /** @var array> $existingRedirections */ $existingRedirections = []; /** @var NodesSources $existingSource */ foreach ($existingSources as $existingSource) { - $existingRedirections[$existingSource->getTranslation()->getLocale()] = array_map(function (Redirection $redirection) { - $this->managerRegistry->getManager()->detach($redirection); - - return $redirection; - }, $existingSource->getRedirections()->toArray()); + $existingRedirections[$existingSource->getTranslation()->getLocale()] = array_map( + fn (Redirection $redirection) => [ + 'query' => $redirection->getQuery(), + 'type' => $redirection->getType(), + ], + $existingSource->getRedirections()->toArray() + ); } $this->removeOldSources($node, $existingSources); /** @var NodesSources $existingSource */ foreach ($existingSources as $existingSource) { - $this->managerRegistry->getManager()->detach($existingSource); $this->doTranstypeSingleSource( $node, $existingSource, @@ -208,13 +210,13 @@ private function doTranstypeSingleSource( /* * Recreate redirections too. */ - /** @var Redirection $existingRedirection */ - foreach ($existingRedirections[$translation->getLocale()] as $existingRedirection) { + /** @var array{query: string, type: int} $redirectionData */ + foreach ($existingRedirections[$translation->getLocale()] as $redirectionData) { $newRedirection = new Redirection(); $this->getManager()->persist($newRedirection); $newRedirection->setRedirectNodeSource($source); - $newRedirection->setQuery($existingRedirection->getQuery()); - $newRedirection->setType($existingRedirection->getType()); + $newRedirection->setQuery($redirectionData['query']); + $newRedirection->setType($redirectionData['type']); } $this->logger->debug('Recreate aliases'); diff --git a/lib/RoadizCoreBundle/src/Node/UniqueNodeGenerator.php b/lib/RoadizCoreBundle/src/Node/UniqueNodeGenerator.php index 300f58574..9f7dd4463 100644 --- a/lib/RoadizCoreBundle/src/Node/UniqueNodeGenerator.php +++ b/lib/RoadizCoreBundle/src/Node/UniqueNodeGenerator.php @@ -4,8 +4,6 @@ namespace RZ\Roadiz\CoreBundle\Node; -use Doctrine\ORM\OptimisticLockException; -use Doctrine\ORM\ORMException; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\Contracts\NodeType\NodeTypeClassLocatorInterface; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; @@ -91,9 +89,6 @@ public function generate( /** * Try to generate a unique node from request variables. * - * @throws ORMException - * @throws OptimisticLockException - * * @deprecated Use generateFromDto() method instead */ public function generateFromRequest(Request $request): NodesSources diff --git a/lib/RoadizCoreBundle/src/Node/UniversalDataDuplicator.php b/lib/RoadizCoreBundle/src/Node/UniversalDataDuplicator.php index 4d6f1377c..c5139a7e0 100644 --- a/lib/RoadizCoreBundle/src/Node/UniversalDataDuplicator.php +++ b/lib/RoadizCoreBundle/src/Node/UniversalDataDuplicator.php @@ -31,7 +31,6 @@ public function __construct( * * @throws \Doctrine\ORM\NoResultException * @throws \Doctrine\ORM\NonUniqueResultException - * @throws \Doctrine\ORM\ORMException */ public function duplicateUniversalContents(NodesSources $source): bool { diff --git a/lib/RoadizCoreBundle/src/Repository/AttributeValueRepository.php b/lib/RoadizCoreBundle/src/Repository/AttributeValueRepository.php index 9e784a9aa..1669cf933 100644 --- a/lib/RoadizCoreBundle/src/Repository/AttributeValueRepository.php +++ b/lib/RoadizCoreBundle/src/Repository/AttributeValueRepository.php @@ -44,9 +44,7 @@ public function findByAttributable( ->leftJoin('a.group', 'ag') ->leftJoin('ag.attributeGroupTranslations', 'agt') ->andWhere($qb->expr()->eq('av.node', ':attributable')) - ->setParameters([ - 'attributable' => $attributable, - ]) + ->setParameter('attributable', $attributable) ->setCacheable(true); if ($orderByWeight) { @@ -81,10 +79,8 @@ public function findByAttributableAndTranslation( ->andWhere($qb->expr()->eq('at.translation', ':translation')) ->andWhere($qb->expr()->eq('agt.translation', ':translation')) ->addOrderBy('av.position', 'ASC') - ->setParameters([ - 'attributable' => $attributable, - 'translation' => $translation, - ]) + ->setParameter('attributable', $attributable) + ->setParameter('translation', $translation) ->setCacheable(true) ->getQuery() ->getResult(); diff --git a/lib/RoadizCoreBundle/src/Repository/CustomFormRepository.php b/lib/RoadizCoreBundle/src/Repository/CustomFormRepository.php index 6cf99b685..2f467ccc7 100644 --- a/lib/RoadizCoreBundle/src/Repository/CustomFormRepository.php +++ b/lib/RoadizCoreBundle/src/Repository/CustomFormRepository.php @@ -41,7 +41,7 @@ public function findAllWithRetentionTime(): array */ public function findByNodeAndField(Node $node, NodeTypeFieldInterface $field): array { - $query = $this->_em->createQuery(' + $query = $this->getEntityManager()->createQuery(' SELECT cf FROM RZ\Roadiz\CoreBundle\Entity\CustomForm cf INNER JOIN cf.nodes ncf WHERE ncf.fieldName = :fieldName AND ncf.node = :node @@ -57,7 +57,7 @@ public function findByNodeAndField(Node $node, NodeTypeFieldInterface $field): a */ public function findByNodeAndFieldName(Node $node, string $fieldName): array { - $query = $this->_em->createQuery(' + $query = $this->getEntityManager()->createQuery(' SELECT cf FROM RZ\Roadiz\CoreBundle\Entity\CustomForm cf INNER JOIN cf.nodes ncf WHERE ncf.fieldName = :fieldName AND ncf.node = :node diff --git a/lib/RoadizCoreBundle/src/Repository/DocumentRepository.php b/lib/RoadizCoreBundle/src/Repository/DocumentRepository.php index 05dd3132f..022a690fe 100644 --- a/lib/RoadizCoreBundle/src/Repository/DocumentRepository.php +++ b/lib/RoadizCoreBundle/src/Repository/DocumentRepository.php @@ -217,7 +217,7 @@ protected function createSearchBy( $qb->leftJoin($alias.'.documentTranslations', 'dt'); $criteriaFields = []; - foreach (self::getSearchableColumnsNames($this->_em->getClassMetadata(DocumentTranslation::class)) as $field) { + foreach (self::getSearchableColumnsNames($this->getEntityManager()->getClassMetadata(DocumentTranslation::class)) as $field) { $criteriaFields[$field] = '%'.strip_tags(\mb_strtolower($pattern)).'%'; } @@ -719,7 +719,7 @@ public function findFirstThumbnailDtoBy( */ public function findAllSettingDocuments(): array { - $query = $this->_em->createQuery(' + $query = $this->getEntityManager()->createQuery(' SELECT d FROM RZ\Roadiz\CoreBundle\Entity\Document d WHERE d.id IN ( SELECT s.value FROM RZ\Roadiz\CoreBundle\Entity\Setting s @@ -744,7 +744,7 @@ public function findAllUnused(): array protected function getAllDocumentsIdUsedInSettings(): array { - $qb2 = $this->_em->createQueryBuilder(); + $qb2 = $this->getEntityManager()->createQueryBuilder(); /* * Get documents used by settings @@ -768,7 +768,7 @@ protected function getAllDocumentsIdUsedInSettings(): array protected function getAllDocumentsIdUsedInCustomFormAnswers(): array { - $qb2 = $this->_em->createQueryBuilder(); + $qb2 = $this->getEntityManager()->createQueryBuilder(); /* * Get documents used by settings diff --git a/lib/RoadizCoreBundle/src/Repository/EntityRepository.php b/lib/RoadizCoreBundle/src/Repository/EntityRepository.php index a4d423ba0..953fad570 100644 --- a/lib/RoadizCoreBundle/src/Repository/EntityRepository.php +++ b/lib/RoadizCoreBundle/src/Repository/EntityRepository.php @@ -7,7 +7,7 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; -use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\ORM\Query; @@ -203,7 +203,7 @@ public function countBy(mixed $criteria): int return 0; } - public static function getSearchableColumnsNames(ClassMetadataInfo $metadata): array + public static function getSearchableColumnsNames(ClassMetadata $metadata): array { /* * Get fields needed for a search query diff --git a/lib/RoadizCoreBundle/src/Repository/FolderRepository.php b/lib/RoadizCoreBundle/src/Repository/FolderRepository.php index e90a5db96..582d35c01 100644 --- a/lib/RoadizCoreBundle/src/Repository/FolderRepository.php +++ b/lib/RoadizCoreBundle/src/Repository/FolderRepository.php @@ -5,8 +5,6 @@ namespace RZ\Roadiz\CoreBundle\Repository; use Doctrine\ORM\NonUniqueResultException; -use Doctrine\ORM\OptimisticLockException; -use Doctrine\ORM\ORMException; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; @@ -32,9 +30,6 @@ public function __construct( /** * Find a folder according to the given path or create it. - * - * @throws ORMException - * @throws OptimisticLockException */ public function findOrCreateByPath(string $folderPath, ?TranslationInterface $translation = null): ?Folder { @@ -76,14 +71,14 @@ public function findOrCreateByPath(string $folderPath, ?TranslationInterface $tr * with given name */ if (null === $translation) { - $translation = $this->_em->getRepository(Translation::class)->findDefault() ?? throw new \InvalidArgumentException('No default translation found.'); + $translation = $this->getEntityManager()->getRepository(Translation::class)->findDefault() ?? throw new \InvalidArgumentException('No default translation found.'); } $folderTranslation = new FolderTranslation($folder, $translation); $folderTranslation->setName($folderName); - $this->_em->persist($folder); - $this->_em->persist($folderTranslation); - $this->_em->flush(); + $this->getEntityManager()->persist($folder); + $this->getEntityManager()->persist($folderTranslation); + $this->getEntityManager()->flush(); return $folder; } @@ -192,7 +187,7 @@ protected function createSearchBy( $qb->leftJoin('obj.translatedFolders', 'tf'); $criteriaFields = []; - foreach (self::getSearchableColumnsNames($this->_em->getClassMetadata(FolderTranslation::class)) as $field) { + foreach (self::getSearchableColumnsNames($this->getEntityManager()->getClassMetadata(FolderTranslation::class)) as $field) { $criteriaFields[$field] = '%'.strip_tags(\mb_strtolower($pattern)).'%'; } diff --git a/lib/RoadizCoreBundle/src/Repository/NodeRepository.php b/lib/RoadizCoreBundle/src/Repository/NodeRepository.php index b475f7fef..d43f0d55d 100644 --- a/lib/RoadizCoreBundle/src/Repository/NodeRepository.php +++ b/lib/RoadizCoreBundle/src/Repository/NodeRepository.php @@ -894,13 +894,13 @@ public function findAllAncestors(NodeInterface|int|string $node, int $maxDepth = if ($node instanceof NodeInterface) { $node = $node->getId(); } - $resultCache = $this->_em->getConfiguration()->getResultCache(); + $resultCache = $this->getEntityManager()->getConfiguration()->getResultCache(); $cacheItem = $resultCache?->getItem('noderepository_findAllAncestors_'.$node); if ($cacheItem?->isHit()) { // @phpstan-ignore-next-line return $cacheItem->get(); } - $statement = $this->_em->getConnection()->prepare(<<getEntityManager()->getConnection()->prepare(<<innerJoin($alias.'.nodeSources', self::NODESSOURCES_ALIAS); $criteriaFields = []; - foreach (self::getSearchableColumnsNames($this->_em->getClassMetadata(NodesSources::class)) as $field) { + foreach (self::getSearchableColumnsNames($this->getEntityManager()->getClassMetadata(NodesSources::class)) as $field) { $criteriaFields[$field] = '%'.strip_tags(\mb_strtolower($pattern)).'%'; } foreach ($criteriaFields as $key => $value) { diff --git a/lib/RoadizCoreBundle/src/Repository/NodesCustomFormsRepository.php b/lib/RoadizCoreBundle/src/Repository/NodesCustomFormsRepository.php index 34feb53f2..de8827354 100644 --- a/lib/RoadizCoreBundle/src/Repository/NodesCustomFormsRepository.php +++ b/lib/RoadizCoreBundle/src/Repository/NodesCustomFormsRepository.php @@ -23,7 +23,7 @@ public function __construct( public function getLatestPositionForFieldName(Node $node, string $fieldName): int { - $query = $this->_em->createQuery(' + $query = $this->getEntityManager()->createQuery(' SELECT MAX(ncf.position) FROM RZ\Roadiz\CoreBundle\Entity\NodesCustomForms ncf WHERE ncf.node = :node AND ncf.fieldName = :fieldName') ->setParameter('node', $node) diff --git a/lib/RoadizCoreBundle/src/Repository/NodesSourcesDocumentsRepository.php b/lib/RoadizCoreBundle/src/Repository/NodesSourcesDocumentsRepository.php index acbefb415..f2a13db61 100644 --- a/lib/RoadizCoreBundle/src/Repository/NodesSourcesDocumentsRepository.php +++ b/lib/RoadizCoreBundle/src/Repository/NodesSourcesDocumentsRepository.php @@ -38,7 +38,7 @@ public function findByNodesSourcesAndFieldName(NodesSources $nodeSource, string public function getLatestPositionForFieldName(NodesSources $nodeSource, string $fieldName): int { - $query = $this->_em->createQuery(' + $query = $this->getEntityManager()->createQuery(' SELECT MAX(nsd.position) FROM RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments nsd WHERE nsd.nodeSource = :nodeSource AND nsd.fieldName = :fieldName') ->setParameter('nodeSource', $nodeSource) diff --git a/lib/RoadizCoreBundle/src/Repository/NodesToNodesRepository.php b/lib/RoadizCoreBundle/src/Repository/NodesToNodesRepository.php index b48c722c5..b3de1602b 100644 --- a/lib/RoadizCoreBundle/src/Repository/NodesToNodesRepository.php +++ b/lib/RoadizCoreBundle/src/Repository/NodesToNodesRepository.php @@ -23,7 +23,7 @@ public function __construct( public function getLatestPositionForFieldName(Node $node, string $fieldName): int { - $query = $this->_em->createQuery(' + $query = $this->getEntityManager()->createQuery(' SELECT MAX(ntn.position) FROM RZ\Roadiz\CoreBundle\Entity\NodesToNodes ntn WHERE ntn.nodeA = :nodeA AND ntn.fieldName = :fieldName') ->setParameter('nodeA', $node) diff --git a/lib/RoadizCoreBundle/src/Repository/SettingGroupRepository.php b/lib/RoadizCoreBundle/src/Repository/SettingGroupRepository.php index b8fd24245..a31c6cd0e 100644 --- a/lib/RoadizCoreBundle/src/Repository/SettingGroupRepository.php +++ b/lib/RoadizCoreBundle/src/Repository/SettingGroupRepository.php @@ -32,7 +32,7 @@ public function __construct( */ public function exists(string $name): bool { - $query = $this->_em->createQuery(' + $query = $this->getEntityManager()->createQuery(' SELECT COUNT(s.id) FROM RZ\Roadiz\CoreBundle\Entity\SettingGroup s WHERE s.name = :name') ->setParameter('name', $name); @@ -42,7 +42,7 @@ public function exists(string $name): bool public function findAllNames(): array { - $query = $this->_em->createQuery('SELECT s.name FROM RZ\Roadiz\CoreBundle\Entity\SettingGroup s'); + $query = $this->getEntityManager()->createQuery('SELECT s.name FROM RZ\Roadiz\CoreBundle\Entity\SettingGroup s'); return array_map(current(...), $query->getScalarResult()); } diff --git a/lib/RoadizCoreBundle/src/Repository/TagRepository.php b/lib/RoadizCoreBundle/src/Repository/TagRepository.php index d99f88a7e..1b542fb33 100644 --- a/lib/RoadizCoreBundle/src/Repository/TagRepository.php +++ b/lib/RoadizCoreBundle/src/Repository/TagRepository.php @@ -583,7 +583,7 @@ protected function createSearchBy( $qb->leftJoin($alias.'.translatedTags', 'tt'); $criteriaFields = []; - foreach (self::getSearchableColumnsNames($this->_em->getClassMetadata(TagTranslation::class)) as $field) { + foreach (self::getSearchableColumnsNames($this->getEntityManager()->getClassMetadata(TagTranslation::class)) as $field) { $criteriaFields[$field] = '%'.strip_tags(\mb_strtolower($pattern)).'%'; } foreach ($criteriaFields as $key => $value) { @@ -655,9 +655,6 @@ protected function prepareComparisons(array &$criteria, QueryBuilder $qb, string /** * Find a tag according to the given path or create it. - * - * @throws \Doctrine\ORM\ORMException - * @throws \Doctrine\ORM\OptimisticLockException */ public function findOrCreateByPath(string $tagPath, ?TranslationInterface $translation = null): ?Tag { @@ -674,7 +671,7 @@ public function findOrCreateByPath(string $tagPath, ?TranslationInterface $trans if (null === $tag) { /** @var TagTranslation|null $ttag */ - $ttag = $this->_em->getRepository(TagTranslation::class)->findOneByName($tagName); + $ttag = $this->getEntityManager()->getRepository(TagTranslation::class)->findOneByName($tagName); if (null !== $ttag) { $tag = $ttag->getTag(); } @@ -693,7 +690,7 @@ public function findOrCreateByPath(string $tagPath, ?TranslationInterface $trans $parentTag = $this->findOrCreateByPath(implode('/', array_slice($tags, 0, -1)), $translation); } if (null === $translation) { - $translation = $this->_em->getRepository(Translation::class)->findDefault() ?? throw new \RuntimeException('No default translation found.'); + $translation = $this->getEntityManager()->getRepository(Translation::class)->findDefault() ?? throw new \RuntimeException('No default translation found.'); } $tag = new Tag(); @@ -706,9 +703,9 @@ public function findOrCreateByPath(string $tagPath, ?TranslationInterface $trans $tag->setParent($parentTag); } - $this->_em->persist($translatedTag); - $this->_em->persist($tag); - $this->_em->flush(); + $this->getEntityManager()->persist($translatedTag); + $this->getEntityManager()->persist($tag); + $this->getEntityManager()->flush(); } return $tag; @@ -728,7 +725,7 @@ public function findByPath(string $tagPath): ?Tag $tag = $this->findOneByTagName(StringHandler::slugify($tagName)); if (null === $tag) { - $ttag = $this->_em->getRepository(TagTranslation::class)->findOneByName($tagName); + $ttag = $this->getEntityManager()->getRepository(TagTranslation::class)->findOneByName($tagName); if (null !== $ttag) { $tag = $ttag->getTag(); } diff --git a/lib/RoadizCoreBundle/src/Repository/TagTranslationDocumentsRepository.php b/lib/RoadizCoreBundle/src/Repository/TagTranslationDocumentsRepository.php index 398f8f9c9..7ef211d82 100644 --- a/lib/RoadizCoreBundle/src/Repository/TagTranslationDocumentsRepository.php +++ b/lib/RoadizCoreBundle/src/Repository/TagTranslationDocumentsRepository.php @@ -35,7 +35,7 @@ public function __construct( */ public function getLatestPosition($tagTranslation): int { - $query = $this->_em->createQuery('SELECT MAX(ttd.position) + $query = $this->getEntityManager()->createQuery('SELECT MAX(ttd.position) FROM RZ\Roadiz\CoreBundle\Entity\TagTranslationDocuments ttd WHERE ttd.tagTranslation = :tagTranslation') ->setParameter('tagTranslation', $tagTranslation); diff --git a/lib/RoadizCoreBundle/src/Repository/UrlAliasRepository.php b/lib/RoadizCoreBundle/src/Repository/UrlAliasRepository.php index bfef788c8..239a10ce1 100644 --- a/lib/RoadizCoreBundle/src/Repository/UrlAliasRepository.php +++ b/lib/RoadizCoreBundle/src/Repository/UrlAliasRepository.php @@ -38,7 +38,7 @@ public function findAllFromNode(int|string|null $nodeId): iterable if (null === $nodeId) { return []; } - $query = $this->_em->createQuery(' + $query = $this->getEntityManager()->createQuery(' SELECT ua FROM RZ\Roadiz\CoreBundle\Entity\UrlAlias ua INNER JOIN ua.nodeSource ns INNER JOIN ns.node n @@ -54,7 +54,7 @@ public function findAllFromNode(int|string|null $nodeId): iterable */ public function exists(string $alias): bool { - $query = $this->_em->createQuery(' + $query = $this->getEntityManager()->createQuery(' SELECT COUNT(ua.alias) FROM RZ\Roadiz\CoreBundle\Entity\UrlAlias ua WHERE ua.alias = :alias') ->setParameter('alias', $alias); diff --git a/lib/RoadizCoreBundle/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php b/lib/RoadizCoreBundle/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php index d27e8c7fe..c2cc2a931 100644 --- a/lib/RoadizCoreBundle/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php +++ b/lib/RoadizCoreBundle/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php @@ -82,12 +82,10 @@ private function getIdentifiers(NodesSources $source): array ->andWhere($qb->expr()->eq('n.visible', ':visible')) ->andWhere($qb->expr()->eq('n.home', ':home')) ->andWhere($qb->expr()->eq('ns.translation', ':translation')) - ->setParameters([ - 'parentIds' => $parentIds, - 'visible' => true, - 'home' => false, - 'translation' => $source->getTranslation(), - ]) + ->setParameter('parentIds', $parentIds) + ->setParameter('visible', true) + ->setParameter('home', false) + ->setParameter('translation', $source->getTranslation()) ->getQuery() ->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true) ->setCacheable(true) diff --git a/lib/RoadizCoreBundle/src/Traits/LoginRequestTrait.php b/lib/RoadizCoreBundle/src/Traits/LoginRequestTrait.php index a5dba0aec..0466411a9 100644 --- a/lib/RoadizCoreBundle/src/Traits/LoginRequestTrait.php +++ b/lib/RoadizCoreBundle/src/Traits/LoginRequestTrait.php @@ -24,9 +24,6 @@ abstract protected function getUserViewer(): UserViewer; /** * @return bool TRUE if confirmation has been sent. FALSE if errors - * - * @throws \Doctrine\ORM\ORMException - * @throws \Doctrine\ORM\OptimisticLockException */ public function sendConfirmationEmail( FormInterface $form, diff --git a/lib/RoadizCoreBundle/src/Translation/TranslationViewer.php b/lib/RoadizCoreBundle/src/Translation/TranslationViewer.php index f80018082..c38d4543f 100644 --- a/lib/RoadizCoreBundle/src/Translation/TranslationViewer.php +++ b/lib/RoadizCoreBundle/src/Translation/TranslationViewer.php @@ -4,7 +4,6 @@ namespace RZ\Roadiz\CoreBundle\Translation; -use Doctrine\ORM\ORMException; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Entity\Node; @@ -72,8 +71,6 @@ public function getRepository(): TranslationRepository * * @param bool $absolute Generate absolute url or relative paths * - * @throws ORMException - * * @deprecated */ public function getTranslationMenuAssignation(Request $request, bool $absolute = false): array diff --git a/lib/RoadizRozierBundle/src/Controller/Ajax/AjaxTagsController.php b/lib/RoadizRozierBundle/src/Controller/Ajax/AjaxTagsController.php index d1740e2bf..714ef4aa2 100644 --- a/lib/RoadizRozierBundle/src/Controller/Ajax/AjaxTagsController.php +++ b/lib/RoadizRozierBundle/src/Controller/Ajax/AjaxTagsController.php @@ -4,8 +4,6 @@ namespace RZ\Roadiz\RozierBundle\Controller\Ajax; -use Doctrine\ORM\OptimisticLockException; -use Doctrine\ORM\ORMException; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; use RZ\Roadiz\CoreBundle\Entity\Tag; @@ -313,9 +311,6 @@ public function searchAction( /** * Create a new Tag. - * - * @throws ORMException - * @throws OptimisticLockException */ #[Route( path: '/rz-admin/ajax/tag/create', diff --git a/lib/RoadizRozierBundle/src/Controller/SettingController.php b/lib/RoadizRozierBundle/src/Controller/SettingController.php index 835b0b46e..8b74b9198 100644 --- a/lib/RoadizRozierBundle/src/Controller/SettingController.php +++ b/lib/RoadizRozierBundle/src/Controller/SettingController.php @@ -4,7 +4,6 @@ namespace RZ\Roadiz\RozierBundle\Controller; -use Doctrine\Common\Cache\CacheProvider; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Bag\Settings; @@ -255,11 +254,6 @@ protected function resetSettingsCache(): void if ($resultCache instanceof ResettableInterface) { $resultCache->reset(); } - - // Legacy Doctrine result cache provider - if ($configuration->getResultCacheImpl() instanceof CacheProvider) { - $configuration->getResultCacheImpl()->deleteAll(); - } } $this->eventDispatcher->dispatch(new CachePurgeRequestEvent()); diff --git a/lib/RoadizRozierBundle/src/Controller/User/UserController.php b/lib/RoadizRozierBundle/src/Controller/User/UserController.php index af16c12ae..de870f561 100644 --- a/lib/RoadizRozierBundle/src/Controller/User/UserController.php +++ b/lib/RoadizRozierBundle/src/Controller/User/UserController.php @@ -160,7 +160,7 @@ public function editDetailsAction(Request $request, int $id): Response { $this->additionalAssignation($request); - /** @var mixed|object|null $item */ + /** @var object|null $item */ $item = $this->em()->find($this->getEntityClass(), $id); if (!$item instanceof PersistableInterface) { throw $this->createNotFoundException(); diff --git a/lib/RoadizSolrBundle/composer.json b/lib/RoadizSolrBundle/composer.json index fc60f8aee..1b6837c3f 100644 --- a/lib/RoadizSolrBundle/composer.json +++ b/lib/RoadizSolrBundle/composer.json @@ -25,7 +25,7 @@ "ext-zip": "*", "ext-json": "*", "ext-mbstring": "*", - "doctrine/orm": "~2.20.0", + "doctrine/orm": "^3.6", "nelmio/solarium-bundle": "^5.1", "roadiz/core-bundle": "2.8.x-dev", "roadiz/documents": "2.8.x-dev", diff --git a/lib/RoadizSolrBundle/src/Indexer/DocumentIndexer.php b/lib/RoadizSolrBundle/src/Indexer/DocumentIndexer.php index 097145faf..d005dca52 100644 --- a/lib/RoadizSolrBundle/src/Indexer/DocumentIndexer.php +++ b/lib/RoadizSolrBundle/src/Indexer/DocumentIndexer.php @@ -75,8 +75,7 @@ public function reindexAll(): void $buffer->addDocument($document); } $this->io?->progressAdvance(); - // detach from Doctrine, so that it can be Garbage-Collected immediately - $this->managerRegistry->getManager()->detach($row); + $this->managerRegistry->getManager()->clear(); } $buffer->flush(); diff --git a/lib/RoadizSolrBundle/src/Indexer/NodesSourcesIndexer.php b/lib/RoadizSolrBundle/src/Indexer/NodesSourcesIndexer.php index e8fbb0f3e..e789bb232 100644 --- a/lib/RoadizSolrBundle/src/Indexer/NodesSourcesIndexer.php +++ b/lib/RoadizSolrBundle/src/Indexer/NodesSourcesIndexer.php @@ -127,8 +127,7 @@ public function reindexAll(int $batchCount = 1, int $batchNumber = 0): void $buffer->addDocument($solarium->getDocument() ?? throw new \RuntimeException('No document created for indexing')); $this->io?->progressAdvance(); - // detach from Doctrine, so that it can be Garbage-Collected immediately - $this->managerRegistry->getManager()->detach($row); + $this->managerRegistry->getManager()->clear(); } $buffer->flush(); diff --git a/lib/RoadizTwoFactorBundle/composer.json b/lib/RoadizTwoFactorBundle/composer.json index 263de9b96..4a978cdf1 100644 --- a/lib/RoadizTwoFactorBundle/composer.json +++ b/lib/RoadizTwoFactorBundle/composer.json @@ -21,7 +21,7 @@ "prefer-stable": true, "require": { "php": ">=8.3", - "doctrine/orm": "~2.20.0", + "doctrine/orm": "^3.6", "endroid/qr-code": "^4.0", "roadiz/core-bundle": "2.8.x-dev", "roadiz/rozier-bundle": "2.8.x-dev", diff --git a/lib/RoadizUserBundle/composer.json b/lib/RoadizUserBundle/composer.json index 8ff1479f8..0df629f86 100644 --- a/lib/RoadizUserBundle/composer.json +++ b/lib/RoadizUserBundle/composer.json @@ -21,7 +21,7 @@ "require": { "php": ">=8.3", "api-platform/symfony": "^4.1.18", - "doctrine/orm": "~2.20.0", + "doctrine/orm": "^3.6", "roadiz/core-bundle": "2.8.x-dev", "roadiz/models": "2.8.x-dev", "symfony/framework-bundle": "7.4.*",