Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion config/packages/doctrine.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions config/reference.php
Original file line number Diff line number Diff line change
Expand Up @@ -968,7 +968,7 @@
* options?: array<string, mixed>,
* mapping_types?: array<string, scalar|Param|null>,
* default_table_options?: array<string, scalar|Param|null>,
* 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<string, array{ // Default: []
* url?: scalar|Param|null, // A URL with connection information; any parameter value parsed from this string will override explicitly set parameters
Expand Down Expand Up @@ -1039,7 +1039,7 @@
* orm?: array{
* default_entity_manager?: scalar|Param|null,
* auto_generate_proxy_classes?: scalar|Param|null, // Auto generate mode possible values are: "NEVER", "ALWAYS", "FILE_NOT_EXISTS", "EVAL", "FILE_NOT_EXISTS_OR_CHANGED", this option is ignored when the "enable_native_lazy_objects" option is true // Default: false
* enable_lazy_ghost_objects?: bool|Param, // Enables the new implementation of proxies based on lazy ghosts instead of using the legacy implementation // Default: false
* enable_lazy_ghost_objects?: bool|Param, // Enables the new implementation of proxies based on lazy ghosts instead of using the legacy implementation // Default: true
* enable_native_lazy_objects?: bool|Param, // Enables the new native implementation of PHP lazy objects instead of generated proxies // Default: false
* proxy_dir?: scalar|Param|null, // Configures the path where generated proxy classes are saved when using non-native lazy objects, this option is ignored when the "enable_native_lazy_objects" option is true // Default: "%kernel.build_dir%/doctrine/orm/Proxies"
* proxy_namespace?: scalar|Param|null, // Defines the root namespace for generated proxy classes when using non-native lazy objects, this option is ignored when the "enable_native_lazy_objects" option is true // Default: "Proxies"
Expand Down Expand Up @@ -1085,7 +1085,7 @@
* fetch_mode_subselect_batch_size?: scalar|Param|null,
* repository_factory?: scalar|Param|null, // Default: "doctrine.orm.container_repository_factory"
* schema_ignore_classes?: list<scalar|Param|null>,
* 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{
Expand Down
4 changes: 2 additions & 2 deletions lib/Documents/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions lib/Models/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions lib/RoadizCoreBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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.*",
Expand Down
63 changes: 63 additions & 0 deletions lib/RoadizCoreBundle/migrations/Version20260602204708.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace RZ\Roadiz\Migrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Convert user_log_entries.data from PHP-serialized (legacy DBAL "array" type) to JSON.
*
* Must run BEFORE Version20260602204709 which alters the column to a native JSON type:
* the ALTER would otherwise fail on every row still holding a PHP-serialized string.
*/
final class Version20260602204708 extends AbstractMigration
{
public function getDescription(): string
{
return 'Convert user_log_entries.data from PHP-serialized to JSON before switching the column to JSON type.';
}

public function up(Schema $schema): void
{
$rows = $this->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']]
);
}
}
}
37 changes: 37 additions & 0 deletions lib/RoadizCoreBundle/migrations/Version20260602204709.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace RZ\Roadiz\Migrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260602204709 extends AbstractMigration
{
public function getDescription(): string
{
return 'Fixed column types and uuid column comment';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->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)\'');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 0 additions & 4 deletions lib/RoadizCoreBundle/src/Console/NodesOrphansCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Loading
Loading