Upgrade Notes
Pimcore 2026.3.3
[Assets]
- [Thumbnails]
Asset\Service::getStreamedResponseByUri()no longer lets aLeague\Flysystem\FilesystemExceptionescape when the thumbnail resolves but the thumbnail storage cannot serve it - a permission or I/O problem, a briefly unavailable remote adapter, or the file disappearing between two storage calls. The helper is public API for custom asset delivery (see Restricting Public Asset Access) and is typed?StreamedResponse; it now returnsnullfor that case as well, the same way it already does when the thumbnail cannot be resolved at all, so a project delivery controller renders its own 404 or placeholder instead of failing with a 500. The failure is logged at error level, because a plain cache miss is already absorbed insidegetStreamedResponseForThumbnail()and everything reaching this point is a real storage fault. Two things are deliberately unchanged: the@internalAsset\Service::getStreamedResponseForThumbnail()still lets the exception through (its@throws \League\Flysystem\FilesystemExceptioncovers it), and with it the public thumbnail route, which calls that method directly.
[Generic Execution Engine]
- [Job runs] The processed elements of a job step are now counted with an atomic database update (new
@internalserviceJobRunElementCounterInterface) instead of a read-modify-write on theJobRunentity, and the error bookkeeping of a failed element is persisted before the element is counted. With more than onemessenger:consumeworker on thepimcore_generic_execution_enginetransport, concurrent increments were lost, so a job run with several selected elements (e.g. a Studio batch edit) never reached its element count, stayedrunningforever and never published its finished state, although every element had been processed.JobRunRepositoryInterfaceis unchanged.
[Workflow]
- [Marking Stores] The
state_tablemarking store no longer writes the new place toelement_workflow_stateright away when a transition withchangePublishedState: save_versionis applied through the workflow manager (Manager::applyWithAdditionalData()with$saveSubject = true, which is what the admin UIs use). The place is instead kept pending on the element and stored with the draft version, and it is written to the table once the element is fully saved (e.g. when the draft is published). Discarding the draft now discards the place change as well, consistent with the attribute based marking stores (single_state, ...). See #18653. Consumers readingelement_workflow_statedirectly therefore see the state of the published element while such a draft exists. Transitions with any otherchangePublishedState, global actions and direct$workflow->apply()calls keep persisting the place immediately. Note for Pimcore Studio: Studio currently applies transitions to the published element instead of the latest draft (see pimcore/platform-version#455); until that is fixed, the pending place is stored with the version the transition creates, but the user's earlier draft edits are not part of it. - Added
Pimcore\Workflow\MarkingStore\PendingMarkingStoreInterface(persistPendingMarking(),getPersistedMarking(),getPendingMarking(),setPendingMarking()) for marking stores that persist the marking independently of the subject and want to support the behavior above;StateTableMarkingStoreimplements it.Pimcore\Model\Element\AbstractElementgot the@internalmethods__getPendingWorkflowMarking(),__setPendingWorkflowMarking()and__getPendingWorkflowMarkings()which carry such a pending marking (double underscore prefix like__getDataVersionTimestamp(), so that the members cannot collide with the generated members of a data object class); the value is part of version dumps but excluded from the cache.
Pimcore 2026.3.2
[Assets]
- [Storage operation queue] New config option
pimcore.assets.storage_operation_queue.resolve_frontend_paths(defaulttrue, i.e. no behavior change). With the queue enabled andpimcore.assets.frontend_prefixes.sourceconfigured,Asset::getFrontendFullPath()(andAsset::getFullPath()in a frontend context) builds URLs of assets under a pending folder move from the pre-move physical path, which is correct for a prefix pointing straight at the storage (CDN, bucket) but wrong for a prefix served through Pimcore (e.g. a route resolving the asset viaAsset::getByPath()), where such URLs returned a 404 and stayed broken once the queue was processed. Set the option tofalsein that case so URLs always carry the current logical path. See Frontend URLs. Asset::getFrontendFullPath()is no longer marked@internal: it is the supported way to obtain the prefixed,FrontendEvents::ASSET_PATH-aware frontend path of an asset from any context (CLI, workers, admin), wheregetFullPath()returns the plain path.
[Security]
- [Documents] Controllers used by documents, snippets,
pimcore_inc, therenderleteditable and the action ofurlslugdata object fields must now be registered controllers. The controller reference is document, template or class definition data, so Pimcore asks Symfony's controller resolver to only accept registered controllers there (the_check_controller_is_allowedrequest attribute). Previously any publicClass::methodwas resolved, including static methods that run shell commands (pimcore/pimcore#19477, follow-up to GHSA-hf99-42jw-j5hp). Registered means: a service taggedcontroller.service_arguments(the default forApp\Controller\in the Symfony skeleton), a subclass ofSymfony\Bundle\FrameworkBundle\Controller\AbstractController(which includesPimcore\Controller\FrontendController), or a class with the#[AsController]attribute. Any other class now fails with a400 Bad Request; tag the service, add#[AsController], or register the type viaControllerResolver::allowControllers(). Sub-requests rendered inline from a document page inherit the check, sorender(controller(...))calls in document templates that point to such a class are affected as well. This is a compatibility note, not a protection for template code: templates are still trusted, and acontroller()reference in a template can set the attribute itself.
[Users]
- [Users] User and role folder names are now unique per parent folder instead of globally, consistent with folder behavior for assets, documents and data objects. User and role names themselves remain globally unique (they are login/security identifiers) — this is still enforced on database level. Technically, the
UNIQUE KEY type_nameon theuserstable is replaced by a unique index over a new stored generated columnuniqueName(migrationVersion20261008090000). The migration cannot fail on existing data, as the new constraint is identical for users/roles and strictly weaker for folders. Note:\Pimcore\Model\User\Folder::getByName()/\Pimcore\Model\User\Role\Folder::getByName()return the oldest match (lowest id) if multiple folders with the same name exist under different parents.
[DataObjects]
- [Object Bricks] Localized relations, relation metadata and URL slugs inside object bricks are read, written and deleted per brick, and
_in brick keys and field names no longer acts as a wildcard. Two bricks in the same container whose localized fields share a field name no longer show or overwrite each other's values (#9701). On an affected installation, a brick that showed the relations of another brick before the upgrade shows only its own relations after it. The stored relations are not changed, but relations that a brick lost by being overwritten cannot be restored automatically. - [Object Bricks] Removing a brick also removes the relation metadata and URL slugs of its localized fields. Such slugs no longer keep routing to the object and no longer come back when the brick is added again. URL slugs of localized fields in bricks resolve their action from their own brick, so resolving such a slug no longer deletes it when the first allowed brick has no field of that name.
- [Object Bricks] Removing a brick only removes the metadata of the advanced many-to-many relations placed directly in that brick. Before, it removed the metadata of all advanced many-to-many relations with the same field name on the object, also in other bricks and on the object itself.
- [Object Bricks] The migration
Version20261007120000adds the brick type to URL slugs of localized fields in bricks that were stored without it (before Pimcore 11). A slug is left unchanged, and reported by the migration, when more than one brick of the object has a localized URL slug field with that name, or when the brick already has a current slug for that field. - [Select] An empty default value of a select field is now stored as
nullinstead of an empty string. This also applies to an empty default returned by a dynamic options provider.Select::getDefaultValue()and exported class definitions therefore shownullfor such fields, and code that compares the default strictly against''needs adjusting. Definitions that already contain an empty default are normalized when they are loaded.
Pimcore 2026.3.1
[Assets]
- [Versions] New opt-in config option
pimcore.assets.versions.skip_initial_version(defaultfalse): when enabled, no version is created when an asset is added (uploaded). Instead, the persisted state of the asset is versioned lazily right before its first modification is applied, followed by the regular version of the modification, so the upload state of edited assets remains restorable while assets that are never modified don't occupy any version storage. Assets added while the option is enabled carry the custom settingpimcore-asset-initial-version-skippeduntil they have a version, so disabling the option later doesn't lose their upload state. Folders are not affected (they are never versioned),Asset::saveVersion()called directly still always creates a version (preceded by the version of the persisted state for an asset without versions). The persisted state is also versioned when the binary data is replaced with versioning disabled (Version::disable()), and the save is aborted if the persisted binary data cannot be read. The@internalhelpersAsset::isInitialVersionSkipped(),Asset::isVersionCreationEnabledByConfig(),Asset::saveVersionOfPersistedState()andAsset\Dao::hasVersionsForUpdate()were added. See Versioning. - [Thumbnails] A
covertransformation now applies the asset's focal point even when the transformation has nopositioningargument configured at all. Previously the focal point was only injected while iterating the configured arguments, so it was silently dropped for thumbnail definitions that omitpositioning(common in hand-written config files) and the image was cropped centered instead. Both focal point coordinates are now required and0is treated as a valid coordinate (left/top edge) rather than as "no focal point"; the newPimcore\Model\Asset\Image::getFocalPoint()is the single source of truth for that rule and is used by both the thumbnail processor and the CDN URL listener. Operational note: the name of a generated rendition is derived from the thumbnail configuration and the asset checksum only, so renditions that were generated before this fix stay valid cache hits and keep their centered crop. Installations with focal points on already-thumbnailed assets have to clear the affected renditions once after the upgrade, e.g. withbin/console pimcore:thumbnails:clear.
[Versioning]
- New
Pimcore\Model\Version\Adapter\ElementDelegateVersionStorageAdapterto store version data in a different storage per element type (asset, document, object), e.g. asset versions on S3 and document and object versions on the local filesystem.FileSystemVersionStorageAdapteraccepts an optional Flysystem storage as constructor argument (default:pimcore.version.storage) and is no longer marked@internal, and adapters can implement the new optionalElementTypeAwareStorageTypeInterface, whichVersion::save()uses to determine the storage type. Nothing changes unless the new adapter is configured. See Versioning.
Pimcore 2026.3.0
[General]
-
[DataObject] Class definition files are now cached in-process by
Pimcore\Model\DataObject\ClassDefinition\DefinitionFileCache(@internal), so clearing the runtime cache in long-running scripts no longer re-includes the definition file on everyClassDefinition::getById()call (previously very slow and eventually failing with "Too many open files"). The cache is validated against the definition file's modification time and is invalidated whenever Pimcore writes or deletes a definition file. Behavioral note: afterRuntimeCache::clear()(orPimcore::collectGarbage()),getById()/getByName()may now return the sameClassDefinitioninstance as before the clear (instead of a freshly included copy) as long as the definition file is unchanged — unsaved in-memory modifications of a class definition are therefore no longer discarded by a runtime cache clear. UseClassDefinition::getById($id, force: true)to force a fresh include from disk. -
[Legacy Admin UI] The legacy admin controllers of the bundled
ApplicationLogger,CustomReportsandSEObundles have been removed, together with their/admin/bundle/*routes, those bundles'config/pimcore/routing.yamlfiles and thecontroller.service_argumentsservice registrations. They were only ever called bypimcore/admin-ui-classic-bundle, which is archived and requirespimcore/pimcore: ^12.3, so it cannot be installed on the 2026 line at all. All of their functionality is available through the Studio backend API (pimcore/studio-backend-bundle). This continues the removal of legacy admin controllers started in 2026.1.0.Pimcore\Bundle\ApplicationLoggerBundle\Controller\LogController(/admin/bundle/applicationlogger/log/{show,priority-json,component-json,show-file-object}) — replaced byPimcore\Bundle\StudioBackendBundle\Bundle\ApplicationLogger\Controller\{Collection,ListPriorities,ListComponents,FileObject}Controller.Pimcore\Bundle\CustomReportsBundle\Controller\Reports\CustomReportController(/admin/bundle/customreports/custom-report/*) — replaced byPimcore\Bundle\StudioBackendBundle\Bundle\CustomReport\Controller\*.Pimcore\Bundle\SeoBundle\Controller\RedirectsController(/admin/bundle/seo/redirects/*) — replaced byPimcore\Bundle\StudioBackendBundle\Bundle\Seo\Controller\Redirect\*.Pimcore\Bundle\SeoBundle\Controller\SettingsController(/admin/bundle/seo/robots-txt) — replaced byPimcore\Bundle\StudioBackendBundle\Bundle\Seo\Controller\RobotsTxt\{Get,Update}Controller.
Also removed:
Pimcore\Bundle\ApplicationLoggerBundle\Service\TranslationService/TranslationServiceInterfaceand their service definition. They existed only to giveLogControllerits log-level labels and translated into the legacyadmintranslation domain; Studio resolves those labels itself. If a project injectedTranslationServiceInterfacesomewhere, drop the dependency — withLogControllergone there is nothing left for it to serve.With this, no bundled Pimcore controller is mounted under
/adminany more, and none extendsPimcore\Controller\UserAwareControlleror usesPimcore\Controller\Traits\JsonHelperTrait. Both, along with the services the removed controllers used (Pimcore\Bundle\SeoBundle\Redirect\Csv,Pimcore\Bundle\SeoBundle\Redirect\RedirectHandler,Pimcore\Bundle\ApplicationLoggerBundle\Handler\ApplicationLoggerDb, …), are kept and remain available for custom controllers. -
[Composer] Bumped minimum requirements of
scheb/2fa-bundleandscheb/2fa-google-authenticatorto8.6.1and ofphpdocumentor/reflection-docblockto5.6.7(5.x line) /6.0.3(6.x line). These are floor raises within the majors already required since 2026.1.0 and carry no BC impact of their own (see the 2026.1.0 notes below for the major-version upgrade guidance).
[Console]
- [Commands]
pimcore:bundle:installandpimcore:bundle:uninstallnow provide shell completion for thebundleargument, suggesting the names of the active Pimcore bundles (bundles loaded by the kernel; registered-but-disabled bundles are not included). Completion requires a one-time setup for your shell — see Console Completion. - [Commands] The
pimcore:bundle:*commands were refactored to the#[AsCommand]attribute. The@internalAbstractBundleCommandchanged accordingly:buildName()was removed andconfigureDescriptionAndHelp()was replaced byconfigureBundleHelp(), which derives the help text from the attribute description;completeBundleArgument()was added. The@internalhelperPimcore\Bundle\CoreBundle\Command\Bundle\Helper\PostStateChangeis now areadonlyclass; a subclass must itself be declaredreadonly. - [Commands] The table output of
pimcore:bundle:listuses the narrowI?/UI?column headers (with an explaining legend) again instead ofInstallable/Uninstallable. The--jsonoutput is unchanged and keeps the full key names.
[Maintenance]
- [DataObjects] The
cleanupBrickTablesandcleanupFieldcollectionTablesmaintenance tasks now drop orphanedobject_brick_*/object_collection_*tables (including their data) when no existing brick/fieldcollection definition owns them - previously such tables were only reported as an error on every maintenance run. This affects installations where definitions were removed on the filesystem (e.g. by deletingvar/classes/*.phpfiles during a deployment) instead of through the regular delete path. A table is only dropped when no candidate parse of its name resolves to a live definition, and the tasks do nothing when the class definition directory itself is unavailable; still, make sure the definitions invar/classes/are in sync with your database before running maintenance. See Cleanup Data Storage. - The
@internalPimcore\Maintenance\Tasks\DataObject\DataObjectTaskHelperInterfacechanged:getCollectionNames(string $dir)was replaced bygetObjectBrickCollectionNames()/getFieldcollectionCollectionNames(), andmatchCollectionKeys(),dropOrphanedTable()were added;cleanupTable()now returnsbool.
[GenericExecutionEngine]
- [JobRun] Log entries stored in the
generic_execution_engine_job_run.logcolumn are now delimited by a short versioned frame (a version token wrapped in ASCII record separators,0x1E) instead of a newline, so a newline that belongs to a single (multi-line) log message is no longer mistaken for an entry boundary. The version token is framed rather than using a bare0x1Eso that legacy payloads, which were stored verbatim and may already contain a stray0x1E, are never split on such a byte. Logs written in the previous newline-delimited format are still read on a best-effort basis, so no migration is required. The parsing of the column has moved fromJobRun::getLogs()into the new@internalPimcore\Bundle\GenericExecutionEngineBundle\Utils\LogParser, and the@internalvalue objectLogLinenow takes the timestamp and message as separate constructor arguments and no longer exposesappendLogLine().
[Assets]
- [Thumbnails] The cache lifetime used for the
Cache-ControlandExpiresHTTP headers when a thumbnail is delivered on-the-fly through the thumbnail service is now configurable viapimcore.assets.thumbnails.cache_lifetime(in seconds). It defaults to604800(one week), which preserves the previous hard-coded behavior. - Added a new optional
$parametersargument toAsset::saveVersion()to allow passing custom arguments to thePRE_UPDATE/POST_UPDATE/POST_UPDATE_FAILUREversioning events, analogous toConcrete::saveVersion(). To stay backwards-compatible for classes overridingsaveVersion(), the argument is documented in the docblock but not yet part of the method signature (it is read viafunc_get_arg()); it will become a regular signature parameter in the next major version. - [Security]
Asset::correctPath()now also renames an asset filename ending in.html,.htm,.xhtml,.xht,.shtml,.jsor.mjsby appending a.txtsuffix whenever that filename itself is newly set or changed (creation or rename), the same way it has always done for PHP-family extensions and.htaccess- these extensions would otherwise be served with an executable/active content-type (text/html,application/xhtml+xml,application/javascript, ...) and could be used for stored XSS, e.g. via a WebDAV upload/rename from a user with only create/rename permission on an asset folder (GHSA-4xrp-5ggg-fg5p). An existing asset already stored under one of these extensions keeps its filename on a save that doesn't change it, including moving it to a different folder (which changes only its path, not its filename), so no existing installation loses access to already-referenced.html/.jsassets..svgis intentionally not covered by this change - see the PR description of the fix for GHSA-4xrp-5ggg-fg5p for why. - [Security] [Thumbnails]
Asset\Service::getStreamedResponseFromImageThumbnail()only mirrors a thumbnail under the extension requested in the URL (e.g. an auto-optimizedjpegthumbnail requested asfoo.png) when that extension is one of the configuredpimcore.assets.thumbnails.allowed_formatsand the path reference is a generated thumbnail. A path reference that passes the original asset through unchanged (typeisasset, e.g. an SVG asset with a scale-only thumbnail configuration) and a requested extension outsideallowed_formatsare now streamed as before, but no copy is written to the thumbnail storage under the requested extension anymore, so a request can no longer create files with an arbitrary extension in the public thumbnail directory. The extension used to derive the mirrored file name is lowercased and escaped before it is used in the replacement. No code or configuration changes are required unless a project relied on thumbnail files being created under an extension that is not inallowed_formats; add the format there if it is meant to be supported.- Action required after upgrading: the fix only prevents new files from being written. Files created under an arbitrary extension before the upgrade stay in the thumbnail storage, which by default is the public web root directory
public/var/tmp/thumbnails(pimcore.thumbnail.storage), and a file with an executable extension (e.g..php) is served and executed by the web server. Because the extension was freely chosen by the requester, a list of known-dangerous extensions is not sufficient; list every file whose extension is not an expected thumbnail format instead, e.g.find public/var/tmp/thumbnails -type f -regextype posix-extended \( -not -iregex '.*\.(avif|eps|gif|jpe?g|pjpeg|png|svg|tiff?|webm|webp|mp4|print)$' -o -iname '*.php*' -o -iname '*.pht*' \)(the second and third conditions also catch double extensions such asfoo.php.png; adapt the path if you configured a different directory or a non-local adapter forpimcore.thumbnail.storage, and the extensions if you configured additionalallowed_formatsor video formats). Review the list, then delete every file you did not create deliberately (add-deleteto the command only after reviewing it); real thumbnails are regenerated on demand, so removing a file is safe. Also review your web server access logs for requests to such files, and treat a hit as a possible compromise.
- Action required after upgrading: the fix only prevents new files from being written. Files created under an arbitrary extension before the upgrade stay in the thumbnail storage, which by default is the public web root directory
[Documents]
- [Renderlets] Custom renderlet configuration parameters are now passed to renderlet controllers as query parameters. Accessing these custom parameters via request attributes is deprecated and will be removed in Pimcore 2027. Update custom renderlet controllers from
$request->attributes->get('myParam')to$request->query->get('myParam').
[DataObject]
- [Deployment]
pimcore:deployment:classes-rebuildnow regenerates missing or outdated node-local PHP class files (var/classes/DataObject/<Class>.phpand the correspondingListing.php) even when the database is already up-to-date, so secondary nodes in a shared-database cluster no longer require--force. Affected classes are reported assavedinstead ofskippedin verbose output.Pimcore\Model\DataObject\ClassDefinition\ClassDefinitionManagergained the public methodhasStalePhpClassFiles(). - [Relations] The
ownernamecolumn has been widened fromVARCHAR(70)toVARCHAR(190)in the per-class relation tables (object_relations_*), the advanced-relation metadata tables (object_metadata_*) andobject_url_slugs. The generatedownernamefor a localized field nested inside an object brick or field collection (e.g./objectbrick~<field>/<brickKey>/localizedfield~localizedfield) can exceed 70 characters, which caused "Data too long for column 'ownername'" on save under strict SQL mode. Existing installations are updated automatically by the migrationVersion20260721000000; no code or configuration changes are required.
[Database]
- Added
Pimcore\Db\Helper::updateOrInsert()next toHelper::upsert(), which itself is unchanged. It addresses exactly the rowsupsert()does (anUPDATE ... WHERE $keys, no other row is ever touched, no trigger runs on a row the criteria do not match) and has the same return value (last insert id on insert,nullon update), but runs theUPDATEfirst. ThatUPDATEalso records whether it matched a row (throughLAST_INSERT_ID(<random token>)in one of its assignments, read back only when no row changed), so a row that matched but did not change - it already holds the values, or aBEFORE UPDATEtrigger reset them - is done after that one statement and its triggers run once; only a missing row goes throughupsert(), whose duplicate handling also covers a row inserted concurrently. After such anUPDATEthe connection's last insert id holds that token. The core DAOs use it wherever the row exists at save time:objects,assets,documents,tags, and the class store, query, localized and brick tables on an update; every other core write is a plain insert or a cold path and stays onupsert(). Two differences toupsert()are observable, both only by custom triggers: on the update of a row whose values change,upsert()'sINSERTfailed on the duplicate and ran the table'sBEFORE INSERTtriggers first (rolled back with the failed statement), whileupdateOrInsert()runs them only when it actually tries to insert; and theUPDATEsetsLAST_INSERT_ID()to the match token before the row'sBEFORE UPDATEandAFTER UPDATEtriggers run, so a trigger readingLAST_INSERT_ID()sees the token rather than the id of an earlier insert (a trigger's own inserts do not disturb the match detection, the server restores the value when a trigger ends). No upgrade changes are needed unless custom triggers rely onBEFORE INSERTbeing invoked for existing rows or onLAST_INSERT_ID()inside an update trigger. - Several columns using the deprecated, ambiguous
utf8/utf8_bin/utf8_general_cicharset/collation names have been modernized ininstall.sql:lock_keys,assets_image_thumbnail_cache.filename,search_backend_data.key,tags.name,properties.cpathandusers_workspaces_asset/document/object.cpathnow use realutf8mb4.assets.filename/pathanddocuments.key/pathmove to the explicitutf8mb3name instead (their compositefullpathindex already uses the full 3072-byte InnoDB index-prefix budget at 3 bytes/char and would overflow it at 4 bytes/char) - note MySQL has deprecatedutf8mb3itself too, so this remains a known limitation pending a future index/schema redesign, not a fully modernized state. Existing installations are updated automatically by the migrationVersion20260729120000; no code or configuration changes are required.- This migration only touches a column when its current collation and length still match the stock legacy definition; a column a project has already widened or otherwise customized is left untouched (a notice is logged) instead of being silently reset.
- The migration is irreversible (
down()throws) — revertingutf8mb4columns back toutf8/utf8mb3could silently replace stored 4-byte characters (e.g. emoji) with?given this application's intentionally permissivesql_mode=''. Restore from a backup if you need to roll back. - The
ALTER TABLE/CONVERT TO CHARACTER SETstatements rewrite the affected columns' storage and typically run as full table rebuilds, which can take time and hold locks onassets,documents,objectsandpropertieson large installations — plan to run this migration during a maintenance window on such installs.
Pimcore 2026.2.14
[Assets]
- [Thumbnails]
ImageThumbnailTrait::getStream()(used byAsset\Image\Thumbnail,Asset\Video\ImageThumbnailandAsset\Document\ImageThumbnail) now returnsnullas documented instead of lettingLeague\Flysystem\UnableToReadFileescape when the thumbnail file no longer exists on the thumbnail storage, e.g. because of a stale entry in the thumbnail status cache. The stale status cache entry is invalidated in that case, so the thumbnail is regenerated on the next request instead of failing again. A read failure for a file that still exists on the storage (or whose existence cannot be determined) is still thrown as before.Asset\Service::getStreamedResponseFromImageThumbnail()accordingly returnsnullfor a missing stream and for a failed generation (previously aTypeErrorfrom writing anullstream to the storage) instead of throwing.
[Database]
- [Doctrine] The shipped
doctrine.dbal.connections.default.default_table_optionsused the keycollate, which Doctrine DBAL 4 (in use since Pimcore 12.0) silently ignores in favour ofcollation. As a result, every table created through the Doctrine schema API - bundle installers and migrations working on theSchemaobject, the ORM schema tool - was created withDEFAULT CHARSET=utf8mb4but without aCOLLATEclause, so MySQL/MariaDB applied the charset's built-in default collation (utf8mb4_general_cion MariaDB / MySQL 5.7,utf8mb4_0900_ai_cion MySQL 8) instead of the configuredutf8mb4_unicode_520_ci. The key is nowcollation, so newly created tables get the configured collation again. Thewebdav_lockstable ininstall.sqlalso received the missingCOLLATEclause. Existing tables are not changed automatically. Known affected tables on installations set up or upgraded since Pimcore 12.0 are the ones created by bundle installers, e.g.bundle_studio_*andtranslations_studio(Studio backend),generic_execution_engine_*, the Generic Data Index, Backend Power Tools and Portal Engine tables, as well aswebdav_locks. A mismatch only matters when string columns of differently collated tables are compared directly (Illegal mix of collations) or when consistent sorting across tables is required; adapting existing tables is therefore optional. Use the detection queries from the 2026.1.0 "Tasks to Do Prior the Update" section to list tables and columns still using the charset default collation, and the queries below to generate theALTER TABLEstatements. Do not convert columns that intentionally use a different collation (e.g.utf8mb4_binfor case-sensitive keys and JSON data - the Studio grid and saved-search configuration tables contain such columns); the generators below exclude them by only matching the default collations and by handling mixed tables column by column. Review the generated statements before running them in a maintenance window;CONVERT TOandMODIFYrewrite the table.-- 1) Table default collation (metadata only, affects columns added later)
SELECT CONCAT('ALTER TABLE `', TABLE_NAME, '` DEFAULT COLLATE utf8mb4_unicode_520_ci;')
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'your_database_name' AND TABLE_TYPE = 'BASE TABLE'
AND TABLE_COLLATION IN ('utf8mb4_general_ci', 'utf8mb4_0900_ai_ci')
ORDER BY TABLE_NAME;
-- 2) Whole-table conversion, only for tables where every string column uses a default collation
SELECT CONCAT('ALTER TABLE `', t.TABLE_NAME, '` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;')
FROM INFORMATION_SCHEMA.TABLES t
WHERE t.TABLE_SCHEMA = 'your_database_name' AND t.TABLE_TYPE = 'BASE TABLE'
AND t.TABLE_COLLATION IN ('utf8mb4_general_ci', 'utf8mb4_0900_ai_ci')
AND NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS c
WHERE c.TABLE_SCHEMA = t.TABLE_SCHEMA AND c.TABLE_NAME = t.TABLE_NAME
AND c.COLLATION_NAME IS NOT NULL
AND c.COLLATION_NAME NOT IN ('utf8mb4_general_ci', 'utf8mb4_0900_ai_ci')
)
ORDER BY t.TABLE_NAME;
-- 3) Column-level statements for the remaining tables that also contain intentionally different collations.
-- DEFAULT / ON UPDATE / COMMENT clauses are not reproduced: check COLUMN_DEFAULT and EXTRA and add them manually.
SELECT CONCAT('ALTER TABLE `', c.TABLE_NAME, '` MODIFY `', c.COLUMN_NAME, '` ', c.COLUMN_TYPE,
' CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci',
IF(c.IS_NULLABLE = 'NO', ' NOT NULL', ' NULL'), ';') AS statement,
c.COLUMN_DEFAULT, c.EXTRA
FROM INFORMATION_SCHEMA.COLUMNS c
JOIN INFORMATION_SCHEMA.TABLES t ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME AND t.TABLE_TYPE = 'BASE TABLE'
WHERE c.TABLE_SCHEMA = 'your_database_name'
AND c.COLLATION_NAME IN ('utf8mb4_general_ci', 'utf8mb4_0900_ai_ci')
AND EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS o
WHERE o.TABLE_SCHEMA = c.TABLE_SCHEMA AND o.TABLE_NAME = c.TABLE_NAME
AND o.COLLATION_NAME IS NOT NULL
AND o.COLLATION_NAME NOT IN ('utf8mb4_general_ci', 'utf8mb4_0900_ai_ci')
)
ORDER BY c.TABLE_NAME, c.ORDINAL_POSITION;
Pimcore 2026.2.12
[Documents]
- [Areabricks] In editmode, areabrick names and descriptions are now translated via the
studiotranslation domain whenever that domain is registered (i.e. Pimcore Studio is installed), so these UI labels show up in Studio's translations instead of the website'smessagesdomain. Labels that were already translated in themessagesdomain keep working as a read-only fallback, but missing keys are no longer auto-created there - they are created in thestudiodomain instead. Installations without thestudiodomain keep translating them viamessagesas before.
[Routing]
- [Pimcore Context] The
routematcher is no longer accepted underpimcore.context.<name>.routes(e.g.{ route: my_api }); config using it now fails withUnrecognized option "route" under "pimcore.context.<name>.routes.0". Available options are "host", "methods", "path".Matching a Pimcore context by route name never actually worked: the context is resolved (and cached on the request) by severalkernel.requestlisteners -CustomAdminEntryPointCheckListener,RoutingListener,FullPageCacheListener- that all run before Symfony's ownRouterListenerpopulates the request's_routeattribute, so aroute:entry could never match and the request silently fell back to thedefaultcontext. If you have such a config, switch to apath:(optionally combined withhost:/methods:) matcher instead:This does not affectpimcore:
context:
api:
routes:
- { path: ^/my/api/ }pimcore.web_profiler.toolbar.excluded_routes, which still supportsroute:.
Pimcore 2026.2.5
[Custom Reports]
- Added a
pimcore_custom_reports.enabled_adaptersconfig option to enable/disable individual Custom Report data-source adapters (e.g. the built-insqladapter) per project. Adapters not listed default to enabled; a disabled adapter is removed from the sharedpimcore.custom_report.adapter.factoriesservice locator, so it becomes unavailable to every consumer (the classic admin controller and the Studio backend bundle alike).We recommend disabling the built-inpimcore_custom_reports:
enabled_adapters:
sql: falsesqladapter unless specifically needed: any user with thereports_configpermission can otherwise define arbitrarySELECTstatements against the application's database, including tables never intended to be exposed. See Custom Reports for details.
[Assets]
- [Assets] New opt-in asset storage operation queue (
pimcore.assets.storage_operation_queue.enabled, defaultfalse): when enabled, moving or deleting asset folders no longer performs the physical per-object storage operations synchronously. Instead the operation is recorded and the storage adapter transparently resolves reads until the newpimcore:assets:storage-queue:processcommand (which must be scheduled, e.g. as a nightly cron) applies it. Backends with native directory rename (local filesystem, SFTP) keep renaming natively — including their thumbnails and asset_cache renditions, for free; object storages (S3, Azure, GCS) get O(1) folder moves for originals, while their derived content (thumbnails, asset_cache) is instead cleaned up and regenerated on demand rather than moved.pimcore:assets:storage-queue:statusreports pending operations and warns about stale rows. See Asset Storage Operation Queue for semantics and caveats. Disabled by default — no behavior change unless enabled. - [Thumbnails] Automatic clipping (
pimcore.assets.image.thumbnails.clip_auto_support) is only applied again when the image actually carries a clipping path, i.e. the Photoshop image resource8BIM 2999('Name of clipping path'). Images that only contain saved Photoshop paths (8BIM 2000-2998) are no longer clipped, since clipping them by an arbitrary path removed their whole content and resulted in empty thumbnails. Existing thumbnails of such images have to be cleared to be re-generated.
Pimcore 2026.2.0
[General]
-
[Assets][Thumbnails][CDN] New core events for thumbnail-config lifecycle were introduced to support CDN purge integration:
Pimcore\Event\ImageThumbnailConfigEvents::POST_UPDATEandPOST_DELETEPimcore\Event\VideoThumbnailConfigEvents::POST_UPDATEandPOST_DELETE- Payload classes:
Pimcore\Event\Model\Asset\Image\Thumbnail\ConfigEventandPimcore\Event\Model\Asset\Video\Thumbnail\ConfigEvent(both exposegetConfig(): Config). - The events are dispatched from
Asset\Image\Thumbnail\Config\Dao::save()/delete()andAsset\Video\Thumbnail\Config\Dao::save()/delete()after the underlying settings store and cache writes succeed. They fire on every save/delete path, including admin UI, API and programmatic changes (the magic__calldelegation also routes through the Dao). Subscribers can use them to react to thumbnail-pipeline changes — Pimcore's bundledCdnPurgeListeneruses them to dispatchthumb-{configName}CDN purges.
-
[Composer] Bumped minimum requirement of
friendsofsymfony/jsrouting-bundleto3.6.0.
[Maintenance Mode]
-
The legacy file-based maintenance mode backward-compatibility layer has been fully removed. The following deprecated static methods have been deleted from
Pimcore\Tool\Admin:getMaintenanceModeFile()getMaintenanceModeScheduleLoginFile()activateMaintenanceMode()deactivateMaintenanceMode()isInMaintenanceMode()isMaintenanceModeScheduledForLogin()scheduleMaintenanceModeOnLogin()unscheduleMaintenanceModeOnLogin()
If you are still using any of these methods, replace them with the
Pimcore\Maintenance\Mode\MaintenanceModeHelperInterfaceservice. Inject it via the service container and useactivate(),deactivate(), andisActive()instead. -
The BC fallback that read the legacy
maintenance.phpconfiguration file has also been removed fromMaintenanceModeCommand,MaintenancePageListener,Bootstrap, andConsole\Application. Any existingvar/config/maintenance.phpfile will be ignored. Ensure maintenance mode is managed exclusively throughMaintenanceModeHelperInterface. -
Admin::getMinimizedScriptPath()has been removed fromPimcore\Tool\Admin.
[Translations]
Translation::DOMAIN_ADMINconstant is deprecated since 2026.2 and will be removed in a future release. Avoid referencing theadmintranslation domain directly.Translator::$adminPathandTranslator::$adminTranslationMappingproperties are deprecated since 2026.2.
Pimcore 2026.1.0
Tasks to Do Prior the Update
Symfony 7.3+ Required
Pimcore 13 requires Symfony 7.3 or higher. All Symfony 6.x components are no longer supported.
- Update all Symfony components to version 7.3 or higher
- Test your application thoroughly with Symfony 7.x
- Remove any explicit Symfony 6.x version constraints from your
composer.json
Migrate Folder structure for email logs
The folder structure for email logs has changed to YYYY/MM/DD/<log filename>.
Please execute the command pimcore:migrate:mail-logs-folder-structure to move the files into the new folder structure or move the files manually.
Database Collation: utf8mb4_unicode_520_ci
Pimcore 2026.1 now explicitly uses utf8mb4_unicode_520_ci as the collation for all utf8mb4 tables and columns. Previous versions specified DEFAULT CHARSET=utf8mb4 without an explicit COLLATE clause in install.sql and Dao CREATE TABLE statements. Due to MySQL/MariaDB behavior, this caused the charset's built-in default collation (for example utf8mb4_general_ci on MySQL 5.7 / MariaDB or utf8mb4_0900_ai_ci on MySQL 8) to be used instead of the database-level default (utf8mb4_unicode_520_ci).
This mismatch can cause issues with foreign key constraints between tables that have different collations and may lead to unexpected sorting behavior.
Important: Some columns intentionally use a different collation (e.g. utf8mb4_bin for case-sensitive keys and JSON data). These columns must not be changed. The queries below only target columns that use the typical default, non-binary utf8mb4 collations (utf8mb4_general_ci and utf8mb4_0900_ai_ci); adjust this list if your server uses a different default.
Use the following SQL to identify all affected tables and columns in your database:
-- List all tables with default non-binary utf8mb4 table collations
SELECT TABLE_NAME, TABLE_COLLATION
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'your_database_name'
AND TABLE_TYPE = 'BASE TABLE'
AND TABLE_COLLATION IN ('utf8mb4_general_ci', 'utf8mb4_0900_ai_ci')
ORDER BY TABLE_NAME;
-- List all columns with default non-binary utf8mb4 collations
SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, COLLATION_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'your_database_name'
AND COLLATION_NAME IN ('utf8mb4_general_ci', 'utf8mb4_0900_ai_ci')
ORDER BY TABLE_NAME, COLUMN_NAME;
[General]
- Thumbnail generation adapters now implement
Video\AdapterInterfaceandDocument\AdapterInterfaceand improved adapters' initialization. - The abstract class
Pimcore\Video\Adapterhas been removed.Pimcore\Video\Adapter\Ffmpegnow directly implementsPimcore\Video\AdapterInterface. If you extendedPimcore\Video\Adapter, update your class to implementPimcore\Video\AdapterInterfacedirectly. - The hard-coded space between quantity value and unit has been removed for class definition quantity fields (e.g.
AbstractQuantityValue,QuantityValueRange). Spacing and formatting between value and unit is now controlled by locale- and translation-based formatting instead of being fixed in the code. - The
reset_passwordrate limiter configuration has been moved to the studio-backend bundle and is no longer part of Pimcore's core configuration. - Removed legacy Admin UI (Classic UI)
EditmodeListener. - Added support for PHP
8.5and bumped minimum requirement of Symfony to7.4. - Dropped support for PHP
8.3and Symfony6. - [QuantityValue] Introduced foreign key constraints on
__unitcolumns in object store, query, localized, objectbrick and fieldcollection tables forQuantityValue,InputQuantityValueandQuantityValueRangefields. These constraints referencequantityvalue_units(id)withON DELETE SET NULLandON UPDATE CASCADE, ensuring referential integrity. The migration automatically cleans up orphaned unit references (setting them toNULL) and changes the__unitcolumn type fromvarchar(64)tovarchar(50)to match the referencedquantityvalue_units.idcolumn. If you have custom unit IDs longer than 50 characters, they will be truncated.
Composer Dependency Majors: scheb/2fa 8.x and phpdocumentor/reflection-docblock 6.x
- [Composer] The constraint for
scheb/2fa-bundleandscheb/2fa-google-authenticatorhas been raised from^6.0 || ^7.5to^8.4. Projects still on scheb/2fa 6.x/7.x are forced onto 8.x when upgrading. Pimcore's own integration (Pimcore\Security\User\User, thescheb_two_factor.security.google_authenticatorservice and the admin 2FA flow) is fully compatible — no action is needed for standard setups. Review the following if your project extends 2FA (see also the official upgrade guide):- The priority of the two-factor authenticator has changed from
0to-100. If you register custom security authenticators on a 2FA-protected firewall and rely on their order relative to the two-factor authenticator, re-check and adjust their priority. - With Symfony 7.4, passing an associative options array to the
UserTotpCodeandUserGoogleTotpCodevalidator constraints is deprecated; Symfony 8 no longer accepts it. Pass named constructor arguments instead (e.g.new UserTotpCode(message: '...')). getGoogleAuthenticatorUsername()andgetTotpAuthenticationUsername()on the scheb model interfaces may now returnnull; code consuming these values should handle it.- scheb/2fa 8.x requires PHP >= 8.4, which matches Pimcore's platform requirement, and pulls in
spomky-labs/otphp11.x transitively.
- The priority of the two-factor authenticator has changed from
- [Composer] The constraint for
phpdocumentor/reflection-docblockhas been widened from^5.2to^5.6 || ^6.0, so acomposer updatemay resolve to 6.x. Pimcore does not use this library directly (it is a transitive dependency of the Symfony PropertyInfo/Serializer components, which support 6.x), so no action is needed unless your own code parses docblocks with it. In that case, note for 6.x (see the upgrade guide):- The static
::create()factory methods on tag classes (e.g.Param::create(),Method::create()) have been removed; create tags throughStandardTagFactory::createInstance()instead.StandardTagFactorycan no longer be instantiated directly withnew. Method::getArguments()has been removed.phpdocumentor/type-resolveris bumped to 2.0, which replaces the legacyCollectiontype handling with real generics support (e.g.Collection<MyClass>).- If your project is not ready for 6.x, you can keep the 5.x line by requiring
"phpdocumentor/reflection-docblock": "^5.6"in your project'scomposer.json.
- The static
Removed deprecated and discontinued bundles
The following bundles have been removed:
- GlossaryBundle
- SimpleBackendSearchBundle
- SeoBundle: dropped
http_error_logfeature and DB Table, and removed Document SEO Editor - StaticRouteBundle
- WordExportBundle
- XliffBundle
[DataObjects]
- Add a new optional
$parametersargument toConcrete::saveVersion()to allow passing of arguments to events. - Removed the following methods from
Pimcore\Model\DataObject\Serviceas part of the Admin UI removal:calculateCellValue()mapFieldname()getDataForEditmode()inManyToManyObjectRelationandAdvancedManyToManyObjectRelationnow usesElement\Service::gridElementData()instead of the removedService::gridObjectData(). As a result, the returned data for each related object no longer includes computed grid columns (e.g. brick fields, localized fields, classification store values, or helper columns). Only the base element data (id, type, path, etc.) is returned. If you rely on additional field data being present in the editmode payload of these relations, you need to fetch it separately.
[Database]
- All
utf8mb4tables now useutf8mb4_unicode_520_cias their default collation to match Doctrine'sdefault_table_optionsconfiguration. Columns inherit this collation unless a different one is explicitly defined (for exampleutf8mb4_binfor case-sensitive keys). Previously,install.sqland DaoCREATE TABLEstatements specifiedDEFAULT CHARSET=utf8mb4without an explicitCOLLATEclause, which caused MySQL/MariaDB to assign the charset's built-in default collation (utf8mb4_general_ci) instead of the intendedutf8mb4_unicode_520_ci. Existing installations need to update the collation of their tables and columns manually, details see 'Tasks to Do Prior the Update' chapter above.
[Models]
- Added a new optional
$parametersargument toAbstractUser::save()andAbstractUser::delete(), as well as their interface methods, to allow passing of arguments toUserRoleEvent.
[Generic Execution Engine]
- Added an
$offsetparameter to support paging ingetRunningJobsByUserId()inJobRunRepositoryInterface. - Added the possibility to pass an optional
$criteriaarray togetTotalCount(),getJobRunById(),getJobRunsByUserId()andgetRunningJobsByUserId()inJobRunRepositoryInterface. - Added the possibility to pass optional
$ownerIdand$executionContextparameters togetTotalCount().
[Installer]
The installer has been completely redesigned with a profile-based architecture. The old individual CLI options for pimcore:install have been removed, and the command now uses a profile-driven setup via --install-profile.
New Command Invocation
The installer is now invoked with:
vendor/bin/pimcore-install --install-profile=App\\Install\\MyProfile
If you have scripts or CI pipelines that invoke pimcore:install with the old options, update them to use --install-profile with a profile class. Create an install profile implementing InstallProfileInterface for your project.
Please see the documentation for further information: https://docs.pimcore.com/platform/
Removed CLI Options
The following CLI options have been removed:
| Old Option | Replacement |
|---|---|
--mysql-host-socket | Use DATABASE_URL env var (Doctrine DSN format) |
--mysql-username | Use DATABASE_URL env var |
--mysql-password | Use DATABASE_URL env var |
--mysql-database | Use DATABASE_URL env var |
--mysql-port | Use DATABASE_URL env var |
--mysql-ssl-cert-path | Use DATABASE_URL env var |
--encryption-secret | Use PIMCORE_ENCRYPTION_SECRET env var directly |
--instance-identifier | Use PIMCORE_INSTANCE_IDENTIFIER env var directly |
--product-key | Use PIMCORE_PRODUCT_KEY env var directly |
--install-bundles | Bundles are now defined by the install profile's getBundles() method |
--skip-database-structure | Use InstallStepFilterInterface in your profile to skip steps |
--skip-database-data | Use InstallStepFilterInterface in your profile to skip steps |
--skip-database-data-dump | Use InstallStepFilterInterface in your profile to skip steps |
--skip-database-config | Configuration is now written to .env.local; to avoid writing installer-generated config, use InstallStepFilterInterface to skip WriteEnv and, if needed, WriteDoctrineConfig |
--skip-product-registration-config | Configuration is now written to .env.local; to avoid writing installer-generated config, use InstallStepFilterInterface to skip WriteEnv |
--only-steps | Use InstallStepFilterInterface to control which InstallStep enum values are skipped |
The options --admin-username and --admin-password are retained but now also accept the env vars PIMCORE_ADMIN_USER and PIMCORE_ADMIN_PASSWORD respectively.
New CLI Options
| New Option | Description |
|---|---|
--install-profile | (Required) FQCN of the install profile class implementing InstallProfileInterface |
--env-definition | FQCN(s) of additional EnvVarDefinitionInterface implementations (repeatable) |
--post-install-commands | FQCN(s) of PostInstallCommandsProviderInterface implementations (repeatable) |
--skip-validation | Skip env var validation (no value = skip all; with value = skip by key/class/FQCN) |
Removed Environment Variables (PIMCORE_INSTALL_* Prefix)
All PIMCORE_INSTALL_* environment variables have been removed. The old installer derived env vars by prepending PIMCORE_INSTALL_ to the uppercased option name. These are replaced by standard env var names:
DATABASE_URL uses Doctrine DSN syntax and can describe either a TCP connection (for example mysql://user:pass@host:3306/dbname) or a unix socket connection.
| Old Env Var | New Env Var |
|---|---|
PIMCORE_INSTALL_ADMIN_USERNAME | PIMCORE_ADMIN_USER |
PIMCORE_INSTALL_ADMIN_PASSWORD | PIMCORE_ADMIN_PASSWORD |
PIMCORE_INSTALL_MYSQL_HOST_SOCKET | DATABASE_URL (Doctrine DSN format, e.g. mysql://user:pass@localhost/dbname?unix_socket=/var/run/mysqld/mysqld.sock) |
PIMCORE_INSTALL_MYSQL_USERNAME | DATABASE_URL |
PIMCORE_INSTALL_MYSQL_PASSWORD | DATABASE_URL |
PIMCORE_INSTALL_MYSQL_DATABASE | DATABASE_URL |
PIMCORE_INSTALL_MYSQL_PORT | DATABASE_URL |
PIMCORE_INSTALL_MYSQL_SSL_CERT_PATH | DATABASE_URL |
PIMCORE_INSTALL_ENCRYPTION_SECRET | PIMCORE_ENCRYPTION_SECRET |
PIMCORE_INSTALL_INSTANCE_IDENTIFIER | PIMCORE_INSTANCE_IDENTIFIER |
PIMCORE_INSTALL_PRODUCT_KEY | PIMCORE_PRODUCT_KEY |
PIMCORE_INSTALL_INSTALL_BUNDLES | Removed — bundles are defined by the profile |
Configuration Output Changes
The installer no longer writes the former local/user configuration YAML files for database and product registration settings. These values are now written to .env.local using Symfony Flex-style section markers (###> section-name ### / ###< section-name ###). The installer still generates config/packages/doctrine_mapping_types.yaml for Doctrine mapping types.
| Old Config File | New Location |
|---|---|
config/local/database.yaml | .env.local (DATABASE_URL) |
config/local/product_registration.yaml | .env.local (PIMCORE_ENCRYPTION_SECRET, PIMCORE_INSTANCE_IDENTIFIER, PIMCORE_PRODUCT_KEY) |
system.yml / system.template.yml | Legacy system config files are no longer written by the installer |
Removed Classes and Events
Pimcore\Bundle\InstallBundle\SystemConfig\ConfigWriter— removed (no more YAML config writing)Pimcore\Bundle\InstallBundle\Event\BundleSetupEvent— removed (bundles are now defined by the profile)Pimcore\Bundle\InstallBundle\DependencyInjection\Configuration— removed (no morepimcore_install.parameters.database_credentialsconfig tree)- The
config/installer.yamloptionpimcore_install.parameters.database_credentialsis no longer supported. UseDATABASE_URLenv var or the interactive installer prompts instead.
Two-Phase Architecture
The installer now runs in two phases:
- Phase 1 (lightweight
InstallerKernel): Collects and validates all env vars from the profile'sEnvVarDefinitionInterfaceimplementations, writes.env.local, writes Doctrine config. - Phase 2 (real
App\Kernel): Sets up the database, imports data sources, creates admin user, registers and installs bundles, runs post-install commands.
Profile Extensibility
Install profiles can implement additional interfaces for advanced control:
InstallStepFilterInterface— skip specific install steps (useful for PaaS environments)PostInstallHookInterface— run custom PHP code near the end of phase 2, before finalization steps such as cache clearing and install marker cleanupDataSourceInterface— import SQL dumps or other data during installation
Doctrine enum Mapping Type Removed
The enum: string Doctrine mapping type is no longer registered; only the bit: boolean mapping remains. The minimum doctrine/dbal requirement was raised to ^4.4, where the explicit enum mapping is no longer needed.
Action required for existing installations: Please check your Doctrine configuration for any registered mapping types and remove the enum: string mapping if it is present.
[OpenSearch / Elasticsearch DSN Configuration]
Search engine configuration now uses DSN-based env vars instead of separate host/port/authentication parameters:
- OpenSearch:
PIMCORE_OPENSEARCH_DSN=opensearch://admin:admin@localhost:9200?ssl=true - Elasticsearch:
PIMCORE_ELASTICSEARCH_DSN=elasticsearch://elastic:changeme@localhost:9200
The DSN is parsed at runtime in the client factory.
The old configuration approach with separate hosts arrays in YAML is replaced by a single dsn config option.
[Messenger Transport DSN Changes]
This is a breaking change for existing installations.
The PIMCORE_MESSENGER_TRANSPORT_DSN_PREFIX env var (previously named PIMCORE_MESSENGER_TRANSPORT_DSN) format has changed.
It must now include a trailing separator for queue name concatenation, because Pimcore bundle configs append queue names directly to this value.
Before (old format):
PIMCORE_MESSENGER_TRANSPORT_DSN=doctrine://default
After (new format):
# Doctrine
PIMCORE_MESSENGER_TRANSPORT_DSN_PREFIX=doctrine://default?queue_name=
# AMQP
PIMCORE_MESSENGER_TRANSPORT_DSN_PREFIX=amqp://guest:guest@rabbit:5672/%2f/
# Redis
PIMCORE_MESSENGER_TRANSPORT_DSN_PREFIX=redis://localhost:6379/
All Pimcore bundle transport configs now use the container parameter %pimcore.messenger.transport_dsn_prefix% with direct concatenation:
framework:
messenger:
transports:
pimcore_core: '%pimcore.messenger.transport_dsn_prefix%pimcore_core'
pimcore_maintenance: '%pimcore.messenger.transport_dsn_prefix%pimcore_maintenance'
A container-level default is provided in bundles/CoreBundle/config/pimcore/default.yaml:
parameters:
env(PIMCORE_MESSENGER_TRANSPORT_DSN_PREFIX): 'doctrine://default?queue_name='
If you have PIMCORE_MESSENGER_TRANSPORT_DSN_PREFIX (or the old PIMCORE_MESSENGER_TRANSPORT_DSN) explicitly set in your .env or environment, update its value to include the trailing separator and use the new name. For Doctrine, change doctrine://default to doctrine://default?queue_name=. Failure to do this will result in invalid transport DSNs like doctrine://defaultpimcore_core.
If you have custom transport definitions in your bundle or project YAML that hardcode doctrine://default?queue_name=, replace them with '%pimcore.messenger.transport_dsn_prefix%' concatenation to support backend-agnostic transport switching.
Symfony Templating Component Removed
The Symfony\Component\Templating\EngineInterface and related services have been completely removed.
What's Removed:
pimcore.templating.engine.delegatingserviceSymfony\Component\Templating\EngineInterfacesupportPimcore\Templating\TwigDefaultDelegatingEngineclass
Action Required:
Update your code to use Twig\Environment directly instead of EngineInterface.
QuantityValue Formatting Changes
- The space between QuantityValue value and unit is going to be removed. Please make sure any custom code that relies on the space is updated accordingly.
Deprecated Kernel Extension Hooks
Overriding Pimcore\Kernel::configureContainer() and Pimcore\Kernel::configureRoutes() is now deprecated and will be removed in Pimcore 2027.1.
These methods were exposed as protected on Pimcore\Kernel via trait aliases of Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait's private methods. Symfony does not consider them part of its public API and has changed their signatures between minor versions, which can break Pimcore subclasses on Symfony upgrades.
Migration:
Replace overrides of configureContainer() with one of the following stable, public extension points:
- Move container configuration to
config/packages/*.yaml(orconfig/packages/<env>/*.yaml). - Add a compiler pass for programmatic container manipulation.
- Override the public
Pimcore\Kernel::registerContainerConfiguration(LoaderInterface $loader)method directly. Callparent::registerContainerConfiguration($loader)first, then load additional configuration via$loader->load(...).
Replace overrides of configureRoutes() with one of the following:
- Move routes to
config/routes/*.yaml. - Register a custom routing loader as a service tagged
routing.loader.