# Pimcore 12 Upgrade

> Upgrade Pimcore 11 projects and bundles to Pimcore 12 - composer, config, PHP 8.3, Symfony 7, Docker, CI/CD, cors/dev, cors/saml

- **Type:** Skill
- **Install:** `agentstack add skill-cors-gmbh-pimcore-skills-pimcore-12-upgrade`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [cors-gmbh](https://agentstack.voostack.com/s/cors-gmbh)
- **Installs:** 0
- **Category:** [Cloud & Infrastructure](https://agentstack.voostack.com/c/cloud-infrastructure)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [cors-gmbh](https://github.com/cors-gmbh)
- **Source:** https://github.com/cors-gmbh/pimcore-skills/tree/main/skills/pimcore-12-upgrade

## Install

```sh
agentstack add skill-cors-gmbh-pimcore-skills-pimcore-12-upgrade
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Pimcore 11 to Pimcore 12 Upgrade

You are helping upgrade a Pimcore 11 codebase to Pimcore 12. First determine whether this is a **Project** or a **Bundle**, as the upgrade steps differ.

## Step 0: Determine Project vs Bundle

**Project** = A full Pimcore application (has `bin/console`, `config/`, `public/index.php`, `docker-compose.yaml`).
**Bundle** = A reusable Symfony/Pimcore bundle (has no `bin/console`, extends `AbstractPimcoreBundle`, published as composer package).

Check for indicators:

```bash
# Project indicators
ls bin/console config/bundles.php public/index.php docker-compose.yaml 2>/dev/null

# Bundle indicators
grep -r "AbstractPimcoreBundle\|AbstractBundle" src/ --include="*.php" -l
```

---

## Part A: Upgrade Steps for PROJECTS

### A1: Update PHP Requirement

Pimcore 12 requires PHP 8.3+.

**composer.json:**
```json
"php": ">=8.3"
```

**Docker:** Update PHP image to 8.3 or 8.4.

### A2: Update composer.json Dependencies

```bash
composer require pimcore/pimcore:^12.0
composer require pimcore/admin-ui-classic-bundle:^2.0
```

If using CORS packages:
```bash
composer require cors/dev:12.x-dev --dev
composer require cors/saml:12.x-dev  # if SAML is used
composer require cors/cors:^0.3.0    # if cors-bundle is used, check latest version
```

If using CoreShop packages (individual or full suite):
```bash
# CoreShop v4 → v5 (all coreshop/* packages)
# For x-dev packages: 4.1.x-dev → 5.0.x-dev
# For stable constraints: ^4.1 → ^5.0
composer require coreshop/theme-bundle:5.0.x-dev  # if used
composer require coreshop/menu-bundle:5.0.x-dev   # if used
composer require coreshop/messenger-bundle:5.0.x-dev  # if used
composer require coreshop/registry:^5.0            # if used
```

Symfony version support changes:
```json
"symfony/dotenv": "^6.4 | ^7.4",
"symfony/runtime": "^6.4 | ^7.4"
```

### A3: Pimcore License Configuration

Create `config/license.yaml`:
```yaml
pimcore:
    encryption:
        secret: '%env(PIMCORE_ENCRYPTION_SECRET)%'
    product_registration:
        instance_identifier: '%env(PIMCORE_INSTANCE_IDENTIFIER)%'
        product_key: '%env(PIMCORE_PRODUCT_KEY)%'
```

Import it in `config/config.yaml`:
```yaml
imports:
    - { resource: license.yaml }
```

Add to `.env` (or `.env.local`):
```
PIMCORE_ENCRYPTION_SECRET=your_secret_here
PIMCORE_INSTANCE_IDENTIFIER=your_instance_id_here
PIMCORE_PRODUCT_KEY=your_product_key_here
```

**Important:** `PIMCORE_ENCRYPTION_SECRET` must not be empty, otherwise you get:
```
`pimcore.encryption.secret` is not set.
Run `vendor/bin/generate-defuse-key` to generate a secret and set it as container parameter `pimcore.encryption.secret`.
```

Generate the secret with:
```bash
docker compose exec php vendor/bin/generate-defuse-key
```

Then set the output as `PIMCORE_ENCRYPTION_SECRET` in `.env.local`.

### A4: Docker Compose Updates

Pimcore 12 introduces new required services. Update `docker-compose.yaml`:

**Add these services:**
- `mercure` - Real-time notifications (required for Pimcore Studio)
- `opensearch:2.13.0` - Search engine (replaces Elasticsearch for Generic Data Index)
- `opensearch-dashboards:2.13.0` - OpenSearch UI (optional, for debugging)
- `rabbitmq:3-management` - Message queue

**Remove if present (enterprise-only):**
- `gotenberg`
- `pdfreactor`

If using `cors/dev`, update docker-compose include:
```yaml
include:
  - vendor/cors/dev/docker-compose-dev.yaml
```

### A4b: Update .docker/composer-setup.sh

If the project has a `.docker/composer-setup.sh`, update the Docker image registry from the old GitLab registry to GitHub Container Registry:

**Before (Pimcore 11):**
```bash
docker run --rm --env COMPOSER_AUTH="$COMPOSER_AUTH" --volume "$(pwd)":/var/www/html:cached git.e-conomix.at:5050/cors/docker/php-alpine-${DOCKER_ALPINE_VERSION}-cli:${DOCKER_PHP_VERSION}-${DOCKER_BASE_IMAGE} composer install --no-scripts --no-interaction
```

**After (Pimcore 12):**
```bash
docker run --rm --env COMPOSER_AUTH="$COMPOSER_AUTH" --volume "$(pwd)":/var/www/html:cached ghcr.io/cors-gmbh/pimcore-docker/php-fpm:${DOCKER_PHP_VERSION}-alpine${DOCKER_ALPINE_VERSION}-${DOCKER_BASE_IMAGE} composer install --no-scripts --no-interaction
```

Key changes:
- Registry: `git.e-conomix.at:5050/cors/docker/` → `ghcr.io/cors-gmbh/pimcore-docker/`
- Image name format: `php-alpine-{ALPINE}-cli:{PHP}-{BASE}` → `php-fpm:{PHP}-alpine{ALPINE}-{BASE}`

### A5: Update Static Analysis & Code Quality Configs

Replace local configs with shared `cors/dev` imports:

**ecs.php:**
```php
import('vendor/cors/dev/ecs.php');
    $ecsConfig->parallel();
    $ecsConfig->paths(['src']);
};
```

**phpstan.neon:**
```neon
includes:
    - vendor/cors/dev/phpstan.neon

parameters:
    paths:
        - src
```

**psalm.xml:**
```xml

    
    
        
        
            
        
    

```

### A6: Update CI/CD Pipeline

Update `.gitlab-ci.yml` to reference the correct `cors/docker` version:
```yaml
include:
  - project: 'cors/docker'
    ref: "8.0"
    file: '.project-gitlab-ci.yml'
```

### A7: Migrate Bundles from Kernel.php to config/bundles.php

Pimcore 11 projects often register bundles in `src/Kernel.php` via `registerBundlesToCollection()`. In Pimcore 12, migrate all bundles to `config/bundles.php` instead.

**Before (Pimcore 11 - src/Kernel.php):**
```php
class Kernel extends PimcoreKernel
{
    public function registerBundlesToCollection(BundleCollection $collection): void
    {
        $collection->addBundle(new SentryBundle(), 0, ['staging', 'prod']);
        $collection->addBundle(new CORSBundle());
        $collection->addBundle(new PimcoreAdminBundle(), 60);
        $collection->addBundle(new CoreShopCoreBundle(), 40);
    }
}
```

**After (Pimcore 12 - config/bundles.php):**
```php
 ['all' => true],
    Sentry\SentryBundle\SentryBundle::class => ['staging' => true, 'prod' => true],
    CORS\Bundle\CORSBundle\CORSBundle::class => ['all' => true],
    CoreShop\Bundle\CoreBundle\CoreShopCoreBundle::class => ['all' => true],
];
```

**After (Pimcore 12 - src/Kernel.php):**
```php
class Kernel extends PimcoreKernel
{
}
```

Migration rules:
- Bundles with no environment restriction → `['all' => true]`
- Bundles with `['staging', 'prod']` → `['staging' => true, 'prod' => true]`
- Priority parameter from `addBundle()` is no longer needed (Symfony handles order via bundles.php)
- Remove all `use` imports and the `registerBundlesToCollection` method from Kernel.php
- Remove bundles for packages that were removed during the upgrade

### A8: Migrate Annotations to Attributes

Symfony 7 removed support for `annotation` route loaders. Update `config/routes.yaml`:

**Before:**
```yaml
app:
    resource: "../src/Controller/"
    type: annotation
```

**After:**
```yaml
app:
    resource: "../src/Controller/"
    type: attribute
```

Also update `config/services.yaml` if it references the old `Symfony\Component\Security\Core\Security` class (removed in Symfony 7):

**Before:**
```yaml
- '@Symfony\Component\Security\Core\Security'
```

**After:**
```yaml
- '@Symfony\Bundle\SecurityBundle\Security'
```

### A8b: Remove Deprecated Security Config Options

Symfony 7 removed `enable_authenticator_manager` (it's now always enabled). Remove it from `config/packages/security.yaml`:

**Before:**
```yaml
security:
    enable_authenticator_manager: true
```

**After:**
```yaml
security:
```

### A8c: Fix Serializer Interface Changes

Symfony 7 enforces strict signatures on `NormalizerInterface`. Update all custom normalizers:

**`normalize()` method:**
```php
// Before (Symfony 6):
public function normalize($object, string $format = null, array $context = [])

// After (Symfony 7):
public function normalize(mixed $data, ?string $format = null, array $context = []): \ArrayObject|array|string|int|float|bool|null
```

**`supportsNormalization()` method:**
```php
// Before (Symfony 6):
public function supportsNormalization($data, $format = null)

// After (Symfony 7):
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
```

**New required method `getSupportedTypes()`:**
```php
public function getSupportedTypes(?string $format): array
{
    return [
        MyClass::class => true,
    ];
}
```

### A9: Pimcore Studio API Route Changes

If you have custom admin controllers, note the API prefix change:
- **Pimcore 11:** `/admin/...`
- **Pimcore 12:** `/pimcore-studio/api/...`

### A10: Deprecation Cleanup

Check for deprecations:
```bash
bin/console debug:container --deprecations
grep -rn "StaticRoutesBundle" config/ src/  # deprecated since 12.3
```

### A11: Clean Up and Run Migrations (LAST STEP)

**Important:** Only run migrations after everything else works (cache:clear, manual browser test).

1. **Delete all existing app migrations** in `src/Migrations/` - they reference removed bundles and use deprecated `ContainerAwareTrait` (removed in Symfony 7).

2. **Create a cleanup migration** that removes old entries from the `migration_versions` table:
```php
final class VersionXXXX extends AbstractMigration
{
    public function getDescription(): string
    {
        return 'Pimcore 12 upgrade: clean up old app migrations from migration_versions table';
    }

    public function up(Schema $schema): void
    {
        $this->addSql("DELETE FROM migration_versions WHERE version LIKE 'App\\\\Migrations\\\\%' AND version != 'App\\\\Migrations\\\\VersionXXXX'");
    }

    public function down(Schema $schema): void
    {
    }
}
```

3. **Run migrations** (Pimcore's own migrations handle data conversions like serialized PHP to JSON):
```bash
docker compose exec php bin/console doctrine:migrations:migrate --no-interaction
```

**Before running migrations, verify:**
- `docker compose exec php bin/console cache:clear` works without errors
- Manual test in browser shows no critical errors
- Ask the user for confirmation before executing

---

## Part B: Upgrade Steps for BUNDLES

Bundles follow a simpler process because they don't manage Docker, database, or application config directly.

### B1: Update composer.json

```json
{
  "require": {
    "php": ">=8.3",
    "pimcore/pimcore": "^11.0 | ^12.0"
  },
  "require-dev": {
    "cors/dev": "12.x-dev",
    "pimcore/admin-ui-classic-bundle": "^2.0"
  }
}
```

Key points:
- Support both `^11.0 | ^12.0` if possible for backwards compatibility
- `cors/dev` goes to `12.x-dev` (not `dev-main`)
- `pimcore/admin-ui-classic-bundle` bumps to `^2.0`
- Add Symfony 7 support: `"symfony/*": "^6.4 | ^7.4"`

### B2: Update CI/CD Pipeline

Bundle pipelines use a different CI template than projects:
```yaml
include:
  - project: 'cors/docker'
    ref: "8.0"
    file: '.bundle-gitlab-ci.yml'
```

Note: `.bundle-gitlab-ci.yml` (not `.project-gitlab-ci.yml`).

### B3: Update Static Analysis Configs

Same as project (Step A5) - use shared `cors/dev` imports for `ecs.php`, `phpstan.neon`, `psalm.xml`.

**Important for psalm.xml:** Make sure `` is defined locally (not just inherited via xi:include), otherwise `vendor/bin/psalm` without arguments won't analyze any files. See Step A5 for the correct config.

### B4: Add `#[\Override]` Attributes

PHP 8.3 introduces `#[\Override]`. Add it to all methods that override a parent class or implement an interface method:

```php
final class MyBundle extends AbstractPimcoreBundle
{
    #[\Override]
    public function getNiceName(): string { return 'My Bundle'; }

    #[\Override]
    public function getDescription(): string { return 'Description'; }
}
```

Common methods that need `#[\Override]`:
- `getNiceName()`, `getDescription()` on bundle classes
- `load()` on DependencyInjection Extension classes
- `getConfigTreeBuilder()` on Configuration classes
- `getSubscribedEvents()` on EventSubscriber classes
- `getType()` on Document Editable classes
- `compile()` on Twig Node classes
- `parse()`, `getTag()` on Twig TokenParser classes
- `getTokenParsers()` on Twig Extension classes

### B4b: Fix Symfony 7 Return Type Declarations

Symfony 7 enforces strict return type declarations. Methods that previously had no return type now require one:

**Command `execute()` method:**
```php
// Before (Pimcore 11 / Symfony 6):
protected function execute(InputInterface $input, OutputInterface $output)

// After (Pimcore 12 / Symfony 7):
protected function execute(InputInterface $input, OutputInterface $output): int
```

Search all Command classes and add `: int` return type to `execute()`:
```bash
grep -rl "protected function execute(InputInterface \$input, OutputInterface \$output)$" src/ --include="*.php"
```

### B5: Add `final` to Classes

Make classes `final` unless they are explicitly designed for extension:

```php
final class MyListener implements EventSubscriberInterface { ... }
final class MyExtension extends Extension { ... }
final class MyController extends UserAwareController { ... }
```

### B6: Add `declare(strict_types=1)`

Every PHP file must have `declare(strict_types=1);` after the opening ` 0`)
- `UnusedClass` / `PossiblyUnusedMethod` → suppress in psalm.xml (false positives for DI-based bundles)
- `MissingConstructor` from vendor code → suppress in psalm.xml

Suppress bundle-typical false positives in `psalm.xml`:
```xml

    
    
    
    

```

---

## Quick Reference: Version Matrix

| Dependency | Pimcore 11 | Pimcore 12 |
|---|---|---|
| PHP | >=8.1 | >=8.3 |
| Symfony | ^6.4 | ^6.4 \| ^7.4 |
| pimcore/pimcore | ^11.0 | ^12.0 |
| pimcore/admin-ui-classic-bundle | ^1.x | ^2.0 |
| pimcore optional bundles (file-explorer, google-marketing, newsletter, system-info, web-to-print) | ^1.x | ^2.0 |
| pimcore/ecommerce-framework-bundle | ^1.0 | removed (no P12 version) |
| pimcore/personalization-bundle | ^1.0 | removed (no P12 version) |
| coreshop/* (core-shop, theme-bundle, menu-bundle, messenger-bundle, registry, etc.) | 4.x / 4.1.x-dev | ^5.0 / 5.0.x-dev |
| cors/dev | dev-main / ^1.0@dev | 12.x-dev |
| cors/cors | ^0.2 | ^0.3.0 |
| cors/saml | ^11.0 | 12.x-dev |
| cors/web-care | ^11.0 | ^2.0 |
| cors/prometheus | ^11.0 | ^2.0 |
| cors/docker CI ref | 7.0 | 8.0 |
| phpstan/phpstan | ^1.10 | ^1.10 \|\| ^2.0 |
| vimeo/psalm | ^5.0 | ^5.0 \|\| ^6.0 |
| symfony/webpack-encore-bundle | ^1.17 | ^1.17 \| ^2.0 |

## Packages with NO Pimcore 12 Support (as of Feb 2026)

These packages must be removed when upgrading to Pimcore 12:

| Package | Notes |
|---|---|
| `dachcom-digital/dynamic-search` | Requires pimcore ^11.0 |
| `dachcom-digital/dynamic-search-data-provider-trinity` | Requires pimcore ^11.0 |
| `dachcom-digital/dynamic-search-index-provider-elasticsearch` | Requires pimcore ^11.0 |
| `dachcom-digital/emailizr` | Requires pimcore ^11.0 |
| `dachcom-digital/seo` | Requires pimcore ^11.0 |
| `cors/kubernetes-console` | Requires admin-ui-classic-bundle ^1.0 |

## Project require-dev Cleanup

When using `cors/dev:12.x-dev`, the following packages are bundled and should be **removed** from project-level `require-dev`:
- `phpstan/phpstan`
- `phpstan/phpstan-symfony`
- `symplify/easy-coding-standard`
- `vimeo/psalm`

## Replacing Removed Dachcom Bundles

### dachcom-digital/emailizr

The emailizr bundle provides CSS inlining for email templates via custom Twig tags (`{% emailizr_inline_style %}`, `{% end_emailizr_inline_style %}`), a `emailizr_style_collector` global, and an `emailizr_inline_style()` Twig function.

**Steps to replace:**

1. Install required dependencies:
```bash
composer require pelago/emogrifier twig/inky-extra
```

2. Create the following classes in `src/Emailizr/`:

**`src/Emailizr/Collector/CssCollector.php`** - Collects CSS file paths for inlining:
```php

 */
class CssCollector implements \IteratorAggregate
{
    /** @var list */
    protected array $cssFiles = [];

    public function add(string $file): void
    {
        $this->cssFiles[] = $file;
    }

    public function removeAll(): void
    {
        $this->cssFiles = [];
    }

    /**
     * @return \ArrayIterator, string>
     */
    #[\Override]
    public function getIterator(): \Arr

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [cors-gmbh](https://github.com/cors-gmbh)
- **Source:** [cors-gmbh/pimcore-skills](https://github.com/cors-gmbh/pimcore-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** yes

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-cors-gmbh-pimcore-skills-pimcore-12-upgrade
- Seller: https://agentstack.voostack.com/s/cors-gmbh
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
