Skip to content

Latest commit

 

History

History
1008 lines (839 loc) · 47.9 KB

File metadata and controls

1008 lines (839 loc) · 47.9 KB

Upgrade guide

Upgrade to 2.8

⚠ Twig breaking changes

  • Removed bulk confirmation templates (bulk_base.html.twig, bulk_delete.html.twig), use admin/confirm_action.html.twig instead

For Twig templates using @RoadizRozier/admin/base.html.twig as parent template, make sure to update

  • content_title
  • content_count_filters
  • content_header_nav

blocks to use new header block instead.

Example:

{%- block header -%}
    {% include '@RoadizRozier/admin/head.html.twig' with {
        title: 'my_entities'|trans,
        filters: filters,
        buttons: [
            {
                label: 'add.entity'|trans,
                href: path('my_entity_add'),
                icon: 'rz-icon-ri--add-line',
            }
        ]
    } only %}
{%- endblock -%}

You need to update your content_filters block to use new widgets/rz_filters_bar.html.twig inside this block.

Example:

{% include "@RoadizRozier/widgets/rz_filters_bar.html.twig" with {
    filters: filters,
    display_select_all_button: true,
} only %}

⚠ Rozier menu icons changed

All backoffice menu icon classes now use the new UI icon set. If your project overrides menu entries in config/packages/roadiz_rozier.yaml and still uses old uk-icon-* classes, those icons will no longer display.

Update your menu entries to use rz-icon-ri--<name> classes (or rz-icon-rz--<name> for Roadiz-specific icons). Icon names are based on Remix Icon names: https://remixicon.com/

Example migration:

 # config/packages/roadiz_rozier.yaml
 roadiz_rozier:
     entries:
         dashboard:
-            icon: uk-icon-rz-dashboard
+            icon: rz-icon-ri--dashboard-line
         nodes:
-            icon: uk-icon-rz-global-nodes
+            icon: rz-icon-ri--command-line
             subentries:
                 all_nodes:
-                    icon: uk-icon-rz-all-nodes
+                    icon: rz-icon-rz--status-container-line
                 draft_nodes:
-                    icon: uk-icon-rz-draft-nodes
+                    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:

 "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):

 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):

 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.

-$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:

-$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:

-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:

  • admin/head.html.twig - Page header with title, breadcrumb, buttons
  • admin/confirm_action.html.twig - Generic confirmation page
  • widgets/rz_filters_bar.html.twig - Filter bar widget
  • widgets/rz_bulk_actions.html.twig - Bulk actions widget
  • New macros: rz_button, rz_badge, rz_actions_menu, rz_card

Upgrade to 2.7

⚠ Breaking changes

  • Upgrade Symfony dependencies to 7.4
  • NodeSourceWalkerContext requires a new service NodeTypeClassLocatorInterface in its constructor.
  • Removed Node::sterile property and Node::isSterile() method.
  • Removed deprecated Node status constants in favor of NodeStatus enum
  • Custom forms and contact form now return a constraint violation list in JSON format: roadiz_core.useConstraintViolationList: true. This requires to configure roadiz_core.customFormPostOperationName with your custom form operation name if you want to use this feature.
  • Interface setter methods now return static instead of self to allow proper fluent interface in subclasses. Make sure to update your class methods signatures if you implement any of the following interfaces:
    • AttributeValueInterface
    • AttributeValueTranslationInterface
    • BlocksAwareWebResponseInterface
    • ContextualizedDocumentInterface
    • DateTimedInterface
    • EntityListManagerInterface
    • LeafInterface
    • PositionedInterface
    • RealmsAwareWebResponseInterface
  • Removed obsolete roadiz/fonts-bundle
  • Replaced the rezozero/liform-bundle fork with the official limenius/liform-bundle. It ships no Symfony Flex recipe, so it is not auto-registered — add it to your config/bundles.php or cache:clear fails with "Parent definition Limenius\Liform\Transformer\AbstractTransformer does not exist":
    Limenius\LiformBundle\LimeniusLiformBundle::class => ['all' => true],
  • POST /api/token now rejects (401) a 2FA-enabled account authenticating with username+password only — a valid TOTP or backup code must be sent as an additional _auth_code field in the request body. Any API client (mobile app, SPA, script) authenticating a 2FA-enabled user must be updated to prompt for and send this field. Accounts without 2FA are unaffected.
  • POST /api/users/signup with an email that's already registered now returns the same success response as a fresh signup (the existing account holder is notified out-of-band instead) — it no longer returns a 422 identifying the email as taken. Frontend signup forms relying on that 422 to show an inline "email already used" message must be updated to rely on the out-of-band email instead.
  • Removed getFontsFilesPath and getFontsFilesBasePath methods from RZ\Roadiz\Documents\Models\FileAwareInterface
  • NodesSources now ships a built-in unpublishedAt date-time field and unpublishedAt becomes a reserved node-type field name. Projects that already declared an unpublished_at (as a custom node-type field, or as a project-level column) must migrate — see Built-in unpublishedAt scheduled expiration field.
  • NodeTypeInterface gained an isUnpublishable(): bool method. Any custom implementation must add it.

Security-audit hardening (no configuration required)

A batch of quick security fixes landed with sensible built-in defaults — no project changes are required to upgrade, but you may want to review or tune them:

  • New rate limiters, pre-configured: RoadizUserBundle and RoadizTwoFactorBundle now each prepend a default framework.rate_limiter entry and dedicated framework.cache pool (password_request_email and two_factor_login respectively) via PrependExtensionInterface, so the container compiles out of the box. Declare a limiter/pool of the same name yourself to override the default (project config always takes precedence over the bundle's):
    # config/packages/framework.yaml — optional, only to override the bundled defaults
    framework:
        rate_limiter:
            password_request_email:
                policy: 'fixed_window'
                limit: 5
                interval: '1 hour'
                cache_pool: 'cache.password_request_email_limiter'
            two_factor_login:
                policy: 'token_bucket'
                limit: 5
                rate: { interval: '1 minutes', amount: 5 }
                cache_pool: 'cache.two_factor_login_limiter'
    • password_request_email caps password-reset requests per targeted email address, in addition to the pre-existing per-IP limiter — closes an IP-rotating mailbox-flooding gap in UserPasswordRequestProcessor.
    • two_factor_login throttles the 2fa_login_check step (Scheb 2FA ships no built-in throttling for that step); the subscriber is auto-registered by RoadizTwoFactorBundle, so it applies as soon as the bundle is enabled.
    • Each limiter gets its own bundle-provided cache pool (cache.password_request_email_limiter, cache.two_factor_login_limiter), so you don't need to touch config/packages/cache.yaml either — only add a pool of the same name there if you want a different adapter/backend for it.
  • Document uploads (backoffice and public custom-form fields) now reject a denylist of web-executable file extensions (.php, .phtml, .html, .js, .htaccess, etc. — see AbstractDocumentFactory::getForbiddenFileExtensions()); SVG uploads are sanitized server-side before storage.
  • The backoffice document upload endpoint (DocumentController::uploadAction) requires a valid CSRF token again (it was previously disabled). The built-in Dropzone uploader already sends one via the existing ajax token header; only a custom uploader built directly against this endpoint would need updating.
  • OpenID Connect id_token signatures are now actually verified (SignedWith was missing from the validation constraints), and a token asserting email_verified: false is now rejected. Review your IdP's JWKS endpoint and key rotation if you use OpenID login.
  • API Platform's NotFilter now respects the same isPropertyEnabled() allowlist as other filters (parity with IntersectionFilter) — filtering on a non-searchable property is now a no-op instead of silently building a working predicate.
  • Nodes/NodesSources gated by a DENY-behaviour Realm are now excluded from every read path (GET /api/nodes, /api/nodes_sources, /api/pages/{id}, /api/articles, etc.), not just GET /api/web_response_by_path — closes a gap where a realm-protected page's raw NodesSources data was still readable directly.
  • POST /api/token for a 2FA-enabled account now requires a valid TOTP/backup code as an extra _auth_code field (see Breaking changes above) — Scheb 2FA is session-based and could not previously run on the stateless api_login firewall.
  • Both login_link firewalls (api and main) now bind password into signature_properties, so a password change invalidates any outstanding login-link email. The main firewall's link also gained check_post_only: true (parity with the API link) — clicking the raw emailed link now lands on a small "confirm sign-in" page that auto-submits a POST, instead of authenticating on a plain GET, so a mail-scanner's link prefetch can no longer consume it.
  • POST /api/users/signup with an already-registered email now returns the same success response as a fresh signup instead of a 422 (see Breaking changes above) — the existing account holder gets a "someone tried to sign up with your email" notification with a password-reset link instead.
  • Webhook entities gained an optional secret field: when set, outbound webhook POSTs carry an X-Roadiz-Signature: sha256=<hmac> header (HMAC-SHA256 of the raw JSON body) so receivers can verify the payload came from this instance. Existing webhooks without a secret are unaffected (no header sent).

New custom-form webhook system

  • When a CustomForm is submitted, Roadiz can now dispatch the submission to external systems (CRMs or any HTTP endpoint) automatically.
  • Webhooks are async: submissions emit a CustomFormAnswerSubmittedEvent, which queues a CustomFormWebhookMessage and processes it via Symfony Messenger.
  • Built-in providers include Brevo, Mailchimp, HubSpot, Zoho CRM, and a generic HTTP option; you can also plug in custom providers.
  • Field mapping and provider settings are configured per form in the admin UI; this controls how form fields map to provider-specific fields.
  • The system is idempotent per CustomFormAnswer ID and uses Messenger retry policies on failure

Built-in unpublishedAt scheduled expiration field

NodesSources now provides a built-in, nullable unpublishedAt date-time column (nodes_sources.unpublished_at), symmetrical to publishedAt. It lets editors schedule content expiration: a node-source is publicly visible only when

node.status = PUBLISHED
AND publishedAt <= now
AND (unpublishedAt IS NULL OR unpublishedAt > now)

Enable it per node-type with the new unpublishable: true option (mirroring publishable). unpublishedAt defaults to null (never expires), so enabling it is backward-compatible for existing content.

A core bundle migration adds the column and its indexes. It is guarded: if nodes_sources.unpublished_at already exists it is a no-op, so it will not clash with a column you added yourself.

If your project already has an unpublished_at

unpublishedAt is now a reserved name and a built-in property, so an existing project-level unpublished_at will collide (a custom node-type field named unpublished_at would generate a duplicate $unpublishedAt property on the generated entity). You must remove the legacy definition and reconcile the schema:

  1. Remove the custom field from your node-type(s). Delete the unpublished_at field from every config/node_types/*.yaml (or from the node-type definition in database), then regenerate entities:

    bin/console app:node-types:regenerate   # or your project's node-type sync/update command
  2. Add a project migration (bin/console make:migration, then adjust it) to preserve legacy data and drop the legacy schema. Adapt the table name(s) to your node-type(s):

    public function up(Schema $schema): void
    {
        // Copy legacy per-node-type values up into the built-in column, then drop the custom column.
        if ($schema->hasTable('ns_article') && $schema->getTable('ns_article')->hasColumn('unpublished_at')) {
            $this->addSql('UPDATE nodes_sources ns INNER JOIN ns_article a ON a.id = ns.id SET ns.unpublished_at = a.unpublished_at WHERE a.unpublished_at IS NOT NULL');
            $this->addSql('ALTER TABLE ns_article DROP unpublished_at');
        }
    
        // If you previously added a project-level column/index on nodes_sources, drop the redundant index
        // so it does not conflict with the built-in one created by the core bundle migration.
        if ($schema->getTable('nodes_sources')->hasIndex('nsapp_unpublished_at')) {
            $this->addSql('DROP INDEX nsapp_unpublished_at ON nodes_sources');
        }
    }
  3. Run the migrations and verify the schema is in sync:

    bin/console doctrine:migrations:migrate
    bin/console doctrine:schema:validate

If you implement NodeTypeInterface yourself, also add the new isUnpublishable(): bool method.

Other changes

  • Roadiz can integrate with external translation services to automatically translate Markdown fields.
  • Switched to attributes for mapping Routes in Roadiz Core and Rozier bundles
  • Fluent setters on key interfaces return static to support subclassing.
  • Back-office sidebar bookmarks are now configurable via roadiz_rozier.bookmarks
  • Project admin logo is now configurable in config/packages/roadiz_core.yaml
roadiz_core:
    projectLogoUrl: '%env(string:APP_PROJECT_LOGO_URL)%'
  • New RZ\Roadiz\RozierBundle\EntityThumbnail\EntityThumbnailProviderInterface system to get a thumbnail URL for any Roadiz entity.
  • Password-reset confirmation emails are now dispatched asynchronously via Messenger instead of synchronously, closing a timing side-channel that let an attacker distinguish existing from non-existing accounts.
  • Embed/podcast feed fetches (AbstractEmbedFinder, AbstractPodcastFinder) are now capped at a 5MB response size and a timeout, to prevent a memory/DoS issue on very large or slow feeds.
  • Outbound webhooks now go through the same private-network-blocking HTTP client already used by media finders (SSRF hardening).
  • CustomForm.color values are now validated with an anchored hex-color regex and escaped in the backoffice list template.

Upgrade to 2.6

⚠ Breaking changes

  • Roadiz requires php 8.3 minimum
  • Upgraded to ApiPlatform 4.x
  • Upgraded to Symfony 7.3
    • New Scheduler component to replace cron jobs with a scheduler worker service
  • Dropped RoadizCompatBundle and all its classes
  • Dropped Themes\Rozier\RozierApp and all Themes\Rozier namespace. Controllers, templates and services have been moved to RZ\Roadiz\RozierBundle namespace.
  • Dropped Roles entity, use native Symfony Roles hierarchy to define your roles instead
  • Dropped RoleArrayVoter BC, you cannot use isGranted and denyUnlessGranted methods with arrays
  • New CaptchaServiceInterface to make captcha support any provider service.
  • All Solr and SearchEngine related logic has been moved to the new roadiz/solr-bundle bundle.
  • ThemeAwareNodeRouter and ThemeAwareNodeUrlMatcher classes have been removed
  • All deprecated AbstractField constants have been removed (in favor of FieldType enum)
  • NodesSourcesRepository::findBySearchQuery method has been removed to remove dependency on SearchEngine
  • NodesSourcesHeadInterface has been simplified: getPolicyUrl, getHomePageUrl and getHomePage methods have been removed
  • Roadiz Core solr configuration has been deprecated, use nelmio/solarium-bundle configuration instead.
    • All Solr services now depends on ClientRegistryInterface
    • All Solr commands must provide a clientName argument to validateSolrState.
    • SolrPaginator renamed to SearchEnginePaginator
    • SolrSearchListManager renamed to SearchEngineListManager
  • Removed too technical Roadiz settings in favor of Symfony configuration parameters:
Old setting name Configuration Parameter
custom_public_scheme roadiz_core.customPublicScheme
custom_preview_scheme roadiz_core.customPreviewScheme
force_locale roadiz_core.forceLocale
force_locale_with_urlaliases roadiz_core.forceLocaleWithUrlAliases
leaflet_map_tile_url roadiz_core.leafletMapTileUrl
maps_default_location roadiz_core.mapsDefaultLocation
  • EmailManager has been deprecated, use symfony/notifier instead.
  • email_sender Setting has been removed, use framework.mailer.envelope.sender configuration parameter instead.
  • EmailManager::getOrigin() method has been removed, this will use framework.mailer.envelope.sender configuration parameter.
  • Added DocumentDto to expose NodesSources documents in API Platform with contextualized hotspot and imageCropAlignment properties.
  • Added new ROLE_ACCESS_USERS_DETAIL role to allow user details edition (GDPR) and moved user language into default UserType form.
  • ContactFormManager::setReceiver has been renamed to setRecipients and accepts an array of RecipientInterface.

Upgrade your composer.json

  • Set roadiz packages to 2.6.*
  • Set symfony packages to 7.3.*
  • Allow symfony 7.3 on extra.symfony.require key
    "extra": {
        "symfony": {
            "allow-contrib": false,
-           "require": "6.4.*",
+           "require": "7.3.*",
        }
    }
  • Remove symfony/proxy-manager-bridge and doctrine/annotations packages, they are no longer required.
  • Remove roadiz/compat-bundle and roadiz/rozier packages

Remove Roadiz CompatBundle

  • Remove RZ\Roadiz\CompatBundle\RoadizCompatBundle::class from your config/bundles.php file
  • Remove config/packages/roadiz_compat.yaml file
  • Remove roadiz/compat-bundle from your composer.json
  • Replace all Rozier theme classes with equivalent from RZ\Roadiz\RozierBundle\ namespace, if your project adds admin controllers and templates
  • Run composer update -o

Upgrade you project code base for Symfony 7.3

  • Replace Symfony\Component\Security\Core\Security with Symfony\Bundle\SecurityBundle\Security
  • Remove security.enable_authenticator_manager option from your config/packages/security.yaml
  • Doctrine annotation have been removed:
    • Switch all Doctrine entity mappings from type: annotation to type: attribute
    • Remove any routes using type: annotation
  • All Normalizer classes must comply to the new method signatures for normalize, supportsNormalization, supportsDenormalization methods: public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
-public function normalize(mixed $object, ?string $format = null, array $context = []): mixed
+public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
-public function supportsNormalization(mixed $data, ?string $format = null): bool
+public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
-public function supportsDenormalization(mixed $data, string $type, ?string $format = null): bool
+public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
  • Replace Symfony\Component\Messenger\Handler\MessageHandlerInterface interface with Symfony\Component\Messenger\Attribute\AsMessageHandler attribute on your message handlers.
+use Symfony\Component\Messenger\Attribute\AsMessageHandler;
-use Symfony\Component\Messenger\Handler\MessageHandlerInterface;

+#[AsMessageHandler]
-final readonly class RenderErroredMessageHandler implements MessageHandlerInterface
+final readonly class RenderErroredMessageHandler

Upgrade your Messenger configuration with Scheduler

  • Add #[AsCronTask(expression: '0 3 * * *', jitter: 60, arguments: '--no-debug -n -q')] to your project cron tasks to run them with the new Scheduler worker.
  • Remove cron from your Dockerfile, and compose.yaml
  • For Docker users: replace your cron service with a new scheduler worker to consume messages
# compose.yaml
services:
    # ...
-    cron:
-        <<: *app_template
-        entrypoint: 'docker-cron-entrypoint'
-        restart: unless-stopped
-        user: root
+    scheduler:
+        <<: *app_template
+        hostname: scheduler
+        stop_signal: SIGTERM
+        entrypoint: [ "php", "-d", "memory_limit=-1", "/app/bin/console", "messenger:consume", "scheduler_default", "--time-limit=1800" ]
+        restart: unless-stopped

Upgrade your API Platform configuration

Add formats and serializer.hydra_prefix configuration if not already present in your api_platform.yaml file.

# config/packages/api_platform.yaml
api_platform:
    # ...
    formats:
        jsonld: ['application/ld+json']
        json: ['application/json']
        x-www-form-urlencoded: ['application/x-www-form-urlencoded']
    serializer:
        hydra_prefix: true

And rename openapiContext to openapi on your api-resources configuration files for each operation.

Enable new DocumentDto

To expose hotspot and imageCropAlignment properties in your API Platform, you need to enable the new DocumentDto.

# config/packages/roadiz_core.yaml
roadiz_core:
    useDocumentDto: true
[
  {
    "id": 1222,
    "filename": "associes_groupe_01.jpg",
    "mimeType": "image/jpeg",
    "imageWidth": 2500,
    "imageHeight": 1667,
    "mediaDuration": 0,
    "imageAverageColor": "#8d8a89",
    "relativePath": "501fad4a/associes_groupe_01.jpg",
    "imageCropAlignment": "center",
    "hotspot": {
      "x": 0.55,
      "y": 0.38
    },
    "type": "image",
    "processable": true
  }
]

Upgrade your Roadiz roles hierarchy

Migrations will automatically convert database roles to JSON roles in users and usergroups tables. But you need to update your security.yaml file to define your roles hierarchy.

# config/packages/security.yaml
security:
    role_hierarchy:
        ROLE_PASSWORDLESS_USER:
            - ROLE_PUBLIC_USER
        ROLE_EMAIL_VALIDATED:
            - ROLE_PUBLIC_USER
        ROLE_PUBLIC_USER:
            - ROLE_USER
        ROLE_BACKEND_USER:
            - ROLE_USER
        ROLE_SUPERADMIN:
            - ROLE_PUBLIC_USER
            - ROLE_ACCESS_VERSIONS
            - ROLE_ACCESS_ATTRIBUTES
            - ROLE_ACCESS_ATTRIBUTES_DELETE
            - ROLE_ACCESS_CUSTOMFORMS
            - ROLE_ACCESS_CUSTOMFORMS_RETENTION
            - ROLE_ACCESS_CUSTOMFORMS_DELETE
            - ROLE_ACCESS_DOCTRINE_CACHE_DELETE
            - ROLE_ACCESS_DOCUMENTS
            - ROLE_ACCESS_DOCUMENTS_LIMITATIONS
            - ROLE_ACCESS_DOCUMENTS_DELETE
            - ROLE_ACCESS_DOCUMENTS_CREATION_DATE
            - ROLE_ACCESS_GROUPS
            - ROLE_ACCESS_NODE_ATTRIBUTES
            - ROLE_ACCESS_NODES
            - ROLE_ACCESS_NODES_DELETE
            - ROLE_ACCESS_NODES_SETTING
            - ROLE_ACCESS_NODES_STATUS
            - ROLE_ACCESS_NODETYPES
            - ROLE_ACCESS_NODETYPES_DELETE
            - ROLE_ACCESS_REDIRECTIONS
            - ROLE_ACCESS_SETTINGS
            - ROLE_ACCESS_TAGS
            - ROLE_ACCESS_TAGS_DELETE
            - ROLE_ACCESS_TRANSLATIONS
            - ROLE_ACCESS_USERS
            - ROLE_ACCESS_USERS_DELETE
            - ROLE_ACCESS_WEBHOOKS
            - ROLE_BACKEND_USER
            - ROLE_ACCESS_LOGS
            - ROLE_ACCESS_REALMS
            - ROLE_ACCESS_REALM_NODES
            - ROLE_ACCESS_FONTS
            - ROLE_ALLOWED_TO_SWITCH

And remove roles routes from your Roadiz Rozier menu entries:

 # config/packages/roadiz_rozier.yaml
 roadiz_rozier:
     user_system:
         name: 'user.system'
         route: ~
         icon: uk-icon-rz-users
-        roles: ['ROLE_ACCESS_USERS', 'ROLE_ACCESS_ROLES', 'ROLE_ACCESS_GROUPS']
+        roles: ['ROLE_ACCESS_USERS', 'ROLE_ACCESS_GROUPS']
         subentries:
             manage_users:
                 name: 'manage.users'
                 route: usersHomePage
                 icon: uk-icon-rz-user
                 roles: ['ROLE_ACCESS_USERS']
-            manage_roles:
-                name: 'manage.roles'
-                route: rolesHomePage
-                icon: uk-icon-rz-roles
-                roles: ['ROLE_ACCESS_ROLES']
             manage_groups:
                 name: 'manage.groups'
                 route: groupsHomePage
                 icon: uk-icon-rz-groups
                 roles: ['ROLE_ACCESS_GROUPS']

Upgrade your Roadiz Core bundle configuration

  • Add forceLocale and forceLocaleWithUrlAliases parameters
  • Move your Recaptcha configuration to roadiz_core.recaptcha parameters
  • Replace $recaptchaPrivateKey and $recaptchaPublicKey constructor arguments with CaptchaServiceInterface in your custom services.
 # config/packages/roadiz_core.yaml
 roadiz_core:
     # ...
+    # Replace your public website URL with a dedicated domain name. It can be useful when using *headless* Roadiz version.
+    customPublicScheme:   null
+    # Replace "?_preview=1" query string to preview website content with a dedicated domain name. It can be useful when using *headless* Roadiz version.
+    customPreviewScheme:  null
+    # Force displaying translation locale in every generated node-source paths.
+    # This should be enabled if you redirect users based on their language on homepage.
+    forceLocale: false
+    # Force displaying translation locale in generated node-source paths even if there is an url-alias in it.
+    forceLocaleWithUrlAliases: false
     # ...
     medias:
         unsplash_client_id: '%env(string:APP_UNSPLASH_CLIENT_ID)%'
         soundcloud_client_id: '%env(string:APP_SOUNDCLOUD_CLIENT_ID)%'
         google_server_id: '%env(string:APP_GOOGLE_SERVER_ID)%'
-        recaptcha_private_key: '%env(string:APP_CAPTCHA_PRIVATE_KEY)%'
-        recaptcha_public_key: '%env(string:APP_CAPTCHA_PUBLIC_KEY)%'
         ffmpeg_path: '%env(string:APP_FFMPEG_PATH)%'
+    captcha:
+        private_key: '%env(string:APP_CAPTCHA_PRIVATE_KEY)%'
+        public_key: '%env(string:APP_CAPTCHA_PUBLIC_KEY)%'
+        verify_url: '%env(string:APP_CAPTCHA_VERIFY_URL)%'

Upgrade your captcha protected Form types

CaptchaServiceInterface will simplify your captcha form types and remove the need for recaptcha_private_key, recaptcha_public_key and verify_url parameters.

     public function __construct(
-        private readonly ?string $recaptchaPrivateKey,
-        private readonly ?string $recaptchaPublicKey,
-        private readonly string $verifyUrl = 'https://www.google.com/recaptcha/api/siteverify',
+        private readonly \RZ\Roadiz\CoreBundle\Captcha\CaptchaServiceInterface $captchaService,
     ) {
     }
 
     public function buildForm(FormBuilderInterface $builder, array $options): void
     {
         $builder->add('email', EmailType::class, [
             'label' => 'newsletter.email',
             'required' => true,
             'attr' => [
                 'autocomplete' => 'email',
             ],
             'constraints' => [
                 new NotBlank(),
                 new Email(),
             ],
         ]);
 
-        if (
-            !empty($this->recaptchaPublicKey)
-            && !empty($this->recaptchaPrivateKey)
-        ) {
-            $builder->add('g-recaptcha-response', RecaptchaType::class, [
-                'mapped' => false,
-                'label' => false,
-                'required' => true,
-                'configs' => [
-                    'publicKey' => $this->recaptchaPublicKey,
-                ],
-                'constraints' => [
-                    new Recaptcha([
-                        'fieldName' => 'g-recaptcha-response',
-                        'privateKey' => $this->recaptchaPrivateKey,
-                        'verifyUrl' => $this->verifyUrl,
-                    ]),
-                ],
-            ]);
-        }
+        if ($this->captchaService->isEnabled()) {
+           $builder->add($this->captchaService->getFieldName(), \RZ\Roadiz\CoreBundle\Form\CaptchaType::class, [
+                'mapped' => false,
+           ]);
+        }
     }

Upgrade your Mailer configuration

# config/packages/mailer.yaml
framework:
    # ...
    mailer:
        # Use the default sender address for all emails
        envelope:
            sender: '%env(MAILER_ENVELOP_SENDER)%'
###> symfony/mailer ###
MAILER_DSN=smtp://mailer:1025
MAILER_ENVELOP_SENDER="Roadiz Dev Website<[email protected]>"
###< symfony/mailer ###

Upgrade your email templates

disclaimer and mailContact variables have been renamed to email_disclaimer and support_email_address in email templates. These variables are now automatically provided by RoadizExtension.

Upgrade your Solr configuration

Roadiz removed Apache Solr from its Core bundle. To re-enable it, you need to install the Solr bundle.

composer require roadiz/solr-bundle
  • Move your Solr endpoint configuration from config/packages/roadiz_core.yml to config/packages/nelmio_solarium.yaml
  • Use RZ\Roadiz\SolrBundle\ClientRegistryInterface to get your Solr client.
  • Regenerate your NodesSources entities with bin/console generate:nsentities to update repositories __construct methods.
  • NodesSourcesRepository::__construct signature has changed
  • NodesSourcesRepository::findBySearchQuery method has been removed to remove dependency on SearchEngine.
  • All Solr commands have been moved to RZ\Roadiz\CoreBundle\SearchEngine\Console namespace.
  • RZ\Roadiz\CoreBundle\Api\ListManager\SolrPaginator has been renamed to RZ\Roadiz\CoreBundle\Api\ListManager\SearchEnginePaginator
  • RZ\Roadiz\CoreBundle\Api\ListManager\SolrSearchListManager has been renamed to RZ\Roadiz\CoreBundle\Api\ListManager\SearchEngineListManager

Upgrade rezozero/intervention-request-bundle

  • Roadiz requires rezozero/intervention-request-bundle to ~5.0.1

Use composition instead of inheritance for Abstract entities

  • All Abstract entities now use composition instead of inheritance.
  • Replace extending AbstractEntity with PersistableInterface and SequentialIdTrait in your entities.
  • Replace extending AbstractDateTimed with DateTimedInterface and DateTimedTrait in your entities.
  • Replace extending AbstractPositioned with PositionedInterface and PositionedTrait in your entities.
  • Use SequentialIdTrait to provide integer id property in your entities.
  • Use UuidTrait to provide Uuid id property in your entities.
  • Replace $this->initAbstractDateTimed(); calls with $this->initDateTimedTrait(); in your entities.

Interface changes

  • ExplorerItemInterface::getId() now returns string|int|Uuid

Removed Themes from routing and events

  • NodesSourcesPathGeneratingEvent does not have theme property anymore.

Upgrade to 2.5

Removed node_types and node_type_fields tables

2.5 drops the node_types and node_type_fields database tables: node-type definitions now live in static configuration files, not in the database. You cannot jump straight to 2.5 — you must stop on the 2.4 line first to export them, otherwise your node-type definitions are lost with the dropped tables.

  1. Upgrade to v2.4.11 first (required intermediate stop — do not skip it).
  2. Run bin/console nodetypes:export-files on 2.4.11 to generate the static node-type configuration files from the database.
  3. Commit the generated files.
  4. Back up your database (highly recommended before proceeding).
  5. Upgrade to 2.5 and run the new migrations, which drop the now-exported tables.

Removed useless user properties

  • Dropped phone, job and birthday columns from users table, they are rarely used and aren't GDPR friendly.

Upgraded rezozero/intervention-request-bundle

Roadiz requires rezozero/intervention-request-bundle to ~4.0.0 It's possible to remove it from composer.json, and Composer will automatically use the correct version.

Upgraded jms/serializer-bundle

Roadiz requires jms/serializer-bundle to ~5.5.1 It's possible to remove it from composer.json, and Composer will automatically use the correct version.

Deprecated Recaptcha validation (since v2.5.30)

Roadiz exposes a new Captcha validation service, which is generic and can be used with any captcha service.

Upgrade to 2.4

⚠ Breaking changes

  • Roadiz requires php 8.2 minimum
  • Upgraded to ApiPlatform 3.3 - requires config changes
    • Prefix all resource files with resources: for example:
# config/api_resources/node.yml
resources:
    RZ\Roadiz\CoreBundle\Entity\Node:
        operations:
            ApiPlatform\Metadata\Get:
                method: 'GET'
                normalizationContext:
                    groups:
                        - node
                        - tag_base
                        - translation_base
                        - document_display
                        - document_display_sources
                    enable_max_depth: true
  • Deleted Controller::findTranslationForLocale, Controller::renderJson, Controller::denyResourceExceptForFormats, Controller::getHandlerFactory, Controller::getPreviewResolver methods
  • Deleted deprecated AppController::makeResponseCachable
  • Removed sensio/framework-extra-bundle, upgraded sentry/sentry-symfony and doctrine/annotations
  • Upgraded rollerworks/password-strength-bundle, removed Top500Provider.php
  • Removed Embed finder for Twitch (they disabled OEmbed on their API)
  • All AbstractEmbedFinder sub-classes require HttpClientInterface, dropped GuzzleRequestMessage, changed HttpRequestMessageInterface
  • Changed WebResponseDataTransformerInterface::transform signature to allow passing an existing WebResponseInterface
  • Changed all node exports to CSV format to be able to stream response.
  • Pass NodesSources repository entityClass to parent constructor. Changed NodesSourcesRepository constructor signature.
  • AbstractPathNormalizer::__construct signature changed (added Stopwatch).

Upgrade to 2.3

⚠ Breaking changes

Switched to ApiPlatform 3.2

Make sure to upgrade bundles.php file and api_platform.yaml configuration:

  • Merge collectionOperations and itemOperations into operations for each resource using ApiPlatform\Metadata\Get or ApiPlatform\Metadata\GetCollection classes
  • Regenerate your api platform resource YAML files, or rename getByPath operation to %entity%_get_by_path
  • @type in API responses for NodesSources may contain NS prefix: for example for a Page node-type, @type will be NSPage, make sure to upgrade frontend project to support this or create a NodesSourcesTypeNormalizer in your project which unset resource_class from context to avoid this:
/**
 * Roadiz WebResponse::$item and tree-walker items are typed against interfaces
 * (PersistableInterface, Collection). API Platform 3 puts that declared interface
 * into `resource_class`, so embedded NodesSources go through the anonymous JSON-LD
 * context builder, which stamps `@type` from the PHP class short name (e.g. "NSPage"
 * instead of "Page"). Resetting `resource_class` when it is not a NodesSources lets
 * API Platform resolve the real resource and emit the correct `@type`.
 
 * @param array<string, mixed> $context
 *
 * @return array<mixed>|string|int|float|bool|\ArrayObject<int|string, mixed>|null
 */
public function normalize(mixed $object, ?string $format = null, array $context = []): mixed
{
    if (
        $object instanceof NodesSources
        && isset($context['resource_class'])
        && is_string($context['resource_class'])
        && !is_a($context['resource_class'], NodesSources::class, true)
    ) {
        unset($context['resource_class']);
    }

    return $this->decorated->normalize($object, $format, $context);
}

Other changes

  • Solr: Removed $proximity argument from search and searchWithHighlight SearchHandlerInterface methods
  • Make sure you don't have fields with name longer than 50 characters before migrating. Migration can be skipped if so.
  • Removed NodeTypeField id join column from NodesCustomForms, NodesSourcesDocuments and NodesToNodes relation tables to use field_name string column for loose relation. Make sure to backup your database before performing this migration.
  • node_type_name JSON property is no-longer required in node-type JSON export files.
  • Nodes: NodesSources metaKeyword and Node priority fields will be dropped.
  • Settings: Setting encryption and crypto keys have been dropped, migrate all your secrets to symfony:secrets to get only one secure vault.

Remove any crypto configuration from config/packages/roadiz_core.yml:

    security:
        private_key_name: default
  • getResultItems method will always return array<SolrSearchResultItem> no matter item type or highlighting.
  • Command constructor signatures changed
  • Controller::get and Controller::has methods have been removed

Upgrade to 2.2

  • Requires PHP 8.1 minimum
  • Upgraded to Symfony 6.4 LTS

Logger configuration

Log namespace changed to RZ\Roadiz\CoreBundle\Logger\Entity\Log. Make sure you update config/packages/doctrine.yaml with:

# config/packages/doctrine.yaml
doctrine:
    dbal:
        url: '%env(resolve:DATABASE_URL)%'
    orm:
        auto_generate_proxy_classes: true
        default_entity_manager: default
        entity_managers:
            # Put `logger` entity manager first to select it as default for Log entity
            logger:
                naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
                mappings:
                    ## Just sharding EM to avoid having Logs in default EM
                    ## and flushing bad entities when storing log entries.
                    RoadizCoreLogger:
                        is_bundle: false
                        type: attribute
                        dir: '%kernel.project_dir%/vendor/roadiz/core-bundle/src/Logger/Entity'
                        prefix: 'RZ\Roadiz\CoreBundle\Logger\Entity'
                        alias: RoadizCoreLogger
            default:
                dql:
                    string_functions:
                        JSON_CONTAINS: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Mysql\JsonContains
                naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
                auto_mapping: true
                mappings:
                    ## Keep RoadizCoreLogger to avoid creating different migrations since we are using
                    ## the same database for both entity managers. Just sharding EM to avoid
                    ## having Logs in default EM and flushing bad entities when storing log entries.
                    RoadizCoreLogger:
                        is_bundle: false
                        type: attribute
                        dir: '%kernel.project_dir%/vendor/roadiz/core-bundle/src/Logger/Entity'
                        prefix: 'RZ\Roadiz\CoreBundle\Logger\Entity'
                        alias: RoadizCoreLogger
                    App:
                        is_bundle: false
                        type: attribute
                        dir: '%kernel.project_dir%/src/Entity'
                        prefix: 'App\Entity'
                        alias: App
                    RoadizCoreBundle:
                        is_bundle: true
                        type: attribute
                        dir: 'src/Entity'
                        prefix: 'RZ\Roadiz\CoreBundle\Entity'
                        alias: RoadizCoreBundle
                    RZ\Roadiz\Core:
                        is_bundle: false
                        type: annotation
                        dir: '%kernel.project_dir%/vendor/roadiz/models/src/Core/AbstractEntities'
                        prefix: 'RZ\Roadiz\Core\AbstractEntities'
                        alias: AbstractEntities
                    App\GeneratedEntity:
                        is_bundle: false
                        type: attribute
                        dir: '%kernel.project_dir%/src/GeneratedEntity'
                        prefix: 'App\GeneratedEntity'
                        alias: App\GeneratedEntity
	            # ...

Upgrade to 2.1

First Roadiz version to use a monorepository structure. All Roadiz components are now in the same lib folder (except for nodetype-contracts).

⚠ Breaking changes

roadiz/models namespace root is now ./src. Change your Doctrine entities path:

RZ\Roadiz\Core:
    is_bundle: false
    type: attribute
    dir: '%kernel.project_dir%/vendor/roadiz/models/src/Roadiz/Core/AbstractEntities'
    prefix: 'RZ\Roadiz\Core\AbstractEntities'
    alias: AbstractEntities

ApiPlatform 2.7

You must migrate your config/api_resources/*.yml files to use new ApiPlatform interfaces and resource YML syntax

  • Remove and regenerate your NS entities with bin/console generate:nsentities to update namespaces
  • Remove and regenerate your Resource configs with bin/console generate:api-resources
    • If you do not want to remove existing config, you'll have to move itemOperations and collectionOperations to single operations node and add class with ApiPlatform\Metadata\Get or ApiPlatform\Metadata\GetCollection
    • Rename iri to types and wrap single values into array
    • Rename path to uriTemplate
    • Rename normalization_context to normalizationContext
    • Rename openapi_context to openapiContext
    • Move shortName to each operation
    • Rename attributes to extraProperties (for /archives endpoints)
    • Add uriTemplate for your custom endpoints (for /archives endpoints)
    • Prefix all named operations with api_ to avoid conflict with non API routes
  • All filters and extensions use new interfaces
  • Removed all deprecated DataTransformer and Dto classes
  • Once everything is migrated changed metadata_backward_compatibility_layer: false in config/packages/api_platform.yaml