Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3d5a277
refactor admin bundle: replace custom controller traits with dedicate…
solverat Jun 16, 2026
b6d40c9
part 1: remove admin bundle handler classes no longer in use (CQRS re…
solverat Jun 17, 2026
bf60deb
remove unused admin bundle handler classes and traits (CQRS refactor …
solverat Jun 18, 2026
f572e9c
replace inline logic with dedicated route actions for improved mainta…
solverat Jun 18, 2026
876067f
align admin bundle with CQRS principles by introducing payload classe…
solverat Jun 18, 2026
eff42e2
remove unused FileResult and StreamResult classes
solverat Jun 18, 2026
9cfdb9a
add cache clearing services, fix namespace alignment
solverat Jun 18, 2026
b6524c1
add workflow and translation resolver services
solverat Jun 18, 2026
6427294
remove unused ClearOpenDxpCacheHandler and ClearSymfonyCacheHandler c…
solverat Jun 18, 2026
b51ca03
add LoginCheckPayload
solverat Jun 18, 2026
c3f118d
introduce BadRequestHttpException, adjust routes, and improve error h…
solverat Jul 9, 2026
edca8a1
improve handler payloads, enhance error handling, and update controll…
solverat Jul 10, 2026
5976f8d
replace handler variables with generic $handler instances across cont…
solverat Jul 10, 2026
8d55228
simplify areabrick-render-index-editmode action
solverat Jul 10, 2026
b9d44bf
replace LoginPageFactory with LoginPageService, refactor controllers …
solverat Jul 10, 2026
06cea89
introduce NotificationFilterParser, refactor FindAllNotificationsHand…
solverat Jul 10, 2026
5d1c475
remove ElementEditLockHelperTrait and related exception handling acro…
solverat Jul 10, 2026
fb39465
add missing doc comments for ElementLockedException and AdminOperatio…
solverat Jul 10, 2026
01fa289
replace DashboardFactory with DashboardService, refactor handlers, re…
solverat Jul 10, 2026
8dec8a1
replace AdminSettingsAssembler with AdminSettingsService and AdminSta…
solverat Jul 10, 2026
a7639ae
replace Grid DTOs with centralized DTO namespace, refactor handlers, …
solverat Jul 10, 2026
929c9af
document Repository layer in README and expand exception handling det…
solverat Jul 10, 2026
2455a51
replace direct usage of Request with dedicated Payloads for asset upl…
solverat Jul 10, 2026
690315f
handle null values for `filename` and `data` in base64 asset uploads …
solverat Jul 10, 2026
8b7268a
handle potential null values and empty file contents across various h…
solverat Jul 10, 2026
7945c16
handle null and empty values across handlers, payloads, and services …
solverat Jul 11, 2026
96c0f0b
remove obsolete DTO classes and refactor remaining implementations to…
solverat Jul 13, 2026
d671206
rename SessionService to ElementDraftService and introduce session ga…
solverat Jul 13, 2026
ba07d31
refactor handlers and persistence logic to use centralized `Persisten…
solverat Jul 14, 2026
2ad27a9
remove obsolete handler and result classes, replace with shared abstr…
solverat Jul 15, 2026
c14a6f8
handle nullable stream in GetDocumentThumbnailResult, remove obsolete…
solverat Jul 15, 2026
167db29
remove `EmptyPayload`
solverat Jul 15, 2026
147f685
Merge branch '1.x' into controller-to-services
solverat Jul 15, 2026
f944bdd
Remove unused constructor dependencies in test controllers and associ…
solverat Jul 15, 2026
1787706
Refactor translation handlers to use `TranslationQueryService` for im…
solverat Jul 15, 2026
c6f83e7
Refactor "ShowEmailLog" to split functionality into dedicated handler…
solverat Jul 16, 2026
6dc5bdb
Refactor settings and statistics handlers to delegate array transform…
solverat Jul 16, 2026
df4db4f
Refactor image thumbnail handling: extract logic to `ImageThumbnailRe…
solverat Jul 16, 2026
f1946cf
re-align service structure (keep alias for bc reasons)
solverat Jul 16, 2026
4f14c6d
Refactor pre-send enrichment logic: introduce `PhpMetaEnricher` and `…
solverat Jul 16, 2026
6de0264
Introduce `opendxp.layout.viewport` for managing viewport regions and…
solverat Jul 20, 2026
ea6c444
Pass class id through grid filter params for relation field lookup
solverat Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions config/event_listeners.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ services:

OpenDxp\Bundle\AdminBundle\EventListener\AdminExceptionListener: ~

OpenDxp\Bundle\AdminBundle\EventListener\HtmlContentTypeResponseListener: ~

#
# GRID CONFIG CLEANUP
#
Expand Down
9 changes: 9 additions & 0 deletions config/legacy_services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Deprecated service id aliases kept for BC
services:
OpenDxp\Bundle\AdminBundle\Service\ElementServiceInterface:
alias: OpenDxp\Bundle\AdminBundle\Service\Element\ElementServiceInterface
deprecated:
package: 'open-dxp/admin-bundle'
version: '1.4'
message: 'The "%alias_id%" service id is deprecated, use "OpenDxp\Bundle\AdminBundle\Service\Element\ElementServiceInterface" instead. It will be removed in 2.0.'

2 changes: 1 addition & 1 deletion config/opendxp/routing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ opendxp_admin_page_display_preview_image:
# we need to have this outside of /admin scope, to be reachable publicly
opendxp_admin_document_document_diff_versions_html:
path: /__admin/document/diff-versions-html
defaults: { _controller: OpenDxp\Bundle\AdminBundle\Controller\Admin\Document\DocumentController::diffVersionsHtmlAction }
defaults: { _controller: OpenDxp\Bundle\AdminBundle\Controller\Admin\Document\DocumentVersionController::diffVersionsHtmlAction }
108 changes: 102 additions & 6 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ services:
public: true
tags: ['controller.service_arguments']

OpenDxp\Bundle\AdminBundle\Controller\Admin\IndexController:
public: true
OpenDxp\Maintenance\ExecutorInterface:
alias: OpenDxp\Maintenance\Executor

OpenDxp\Bundle\AdminBundle\Handler\Admin\Statistics\StatisticsHandler:
arguments:
$httpClient: '@opendxp.http_client'
tags: [ 'controller.service_arguments' ]

#
# COMMANDS
Expand Down Expand Up @@ -78,7 +79,7 @@ services:
#

OpenDxp\Model\DataObject\ClassDefinition\PreviewGeneratorInterface:
class: OpenDxp\Bundle\AdminBundle\Service\PreviewGenerator
class: OpenDxp\Bundle\AdminBundle\Generator\PreviewGenerator

#
# Twig Extensions
Expand All @@ -91,6 +92,7 @@ services:
#

OpenDxp\Bundle\AdminBundle\Service\Workflow\ActionsButtonService: ~
OpenDxp\Bundle\AdminBundle\Resolver\Workflow\WorkflowElementResolver: ~

#
# Admin Config Services
Expand All @@ -100,5 +102,99 @@ services:
#
# Element Services
#
OpenDxp\Bundle\AdminBundle\Service\ElementServiceInterface:
class: OpenDxp\Bundle\AdminBundle\Service\ElementService
OpenDxp\Bundle\AdminBundle\Service\Element\ElementServiceInterface:
class: OpenDxp\Bundle\AdminBundle\Service\Element\ElementService

OpenDxp\Bundle\AdminBundle\Generator\CustomLoginUrlGenerator: ~

OpenDxp\Bundle\AdminBundle\Service\Admin\AdminUserContextInterface:
class: OpenDxp\Bundle\AdminBundle\Service\Admin\AdminUserContext

OpenDxp\Bundle\AdminBundle\Session\SessionIdentityInterface:
class: OpenDxp\Bundle\AdminBundle\Session\SessionIdentity

OpenDxp\Bundle\AdminBundle\Session\Gateway\:
resource: '../src/Session/Gateway'

OpenDxp\Bundle\AdminBundle\Service\Element\ElementDraftService: ~
OpenDxp\Bundle\AdminBundle\Service\Element\EditLockService: ~

OpenDxp\Bundle\AdminBundle\Service\Login\LoginPageService: ~

OpenDxp\Bundle\AdminBundle\Service\Notification\NotificationFilterParser: ~

OpenDxp\Bundle\AdminBundle\Mapper\Document\DocumentPayloadMapper: ~
OpenDxp\Bundle\AdminBundle\Coordinator\Document\DocumentPersistenceCoordinator: ~

OpenDxp\Bundle\AdminBundle\Mapper\DataObject\DataObjectPayloadMapper: ~
OpenDxp\Bundle\AdminBundle\Coordinator\DataObject\DataObjectPersistenceCoordinator: ~
OpenDxp\Bundle\AdminBundle\Service\DataObject\DataObjectGridService: ~
OpenDxp\Bundle\AdminBundle\Mapper\DataObject\ClassificationstoreKeyConfigMapper: ~

OpenDxp\Bundle\AdminBundle\Mapper\Asset\AssetPayloadMapper: ~
OpenDxp\Bundle\AdminBundle\Coordinator\Asset\AssetPersistenceCoordinator: ~
OpenDxp\Bundle\AdminBundle\Service\Asset\AssetUploadService: ~
OpenDxp\Bundle\AdminBundle\Service\Asset\AssetGridService: ~
OpenDxp\Bundle\AdminBundle\Resolver\Asset\ImageThumbnailResolver: ~

OpenDxp\Bundle\AdminBundle\Service\Grid\GridColumnConfigService: ~
OpenDxp\Bundle\AdminBundle\Resolver\Grid\AssetGridColumnConfigResolver: ~
OpenDxp\Bundle\AdminBundle\Resolver\Grid\DataObjectGridColumnConfigResolver: ~
OpenDxp\Bundle\AdminBundle\Service\Grid\GridExportService: ~
OpenDxp\Bundle\AdminBundle\Service\Grid\GridBatchService: ~

OpenDxp\Bundle\AdminBundle\Service\Cache\OpenDxpCacheClearingService: ~
OpenDxp\Bundle\AdminBundle\Service\Cache\SymfonyCacheClearingService: ~

OpenDxp\Bundle\AdminBundle\Service\Translation\TranslationQueryService: ~
OpenDxp\Bundle\AdminBundle\Resolver\Translation\AdminSearchTermResolver: ~

OpenDxp\Bundle\AdminBundle\Service\Portal\DashboardService: ~

OpenDxp\Bundle\AdminBundle\Service\Admin\AdminSettingsService: ~
OpenDxp\Bundle\AdminBundle\Service\Admin\AdminStatisticsService: ~

#
# Repositories
#
OpenDxp\Bundle\AdminBundle\Repository\:
resource: '../src/Repository'

#
# Factories
#
OpenDxp\Bundle\AdminBundle\Factory\:
resource: '../src/Factory'

#
# HTTP / Value Resolvers
#
OpenDxp\Bundle\AdminBundle\Http\:
resource: '../src/Http'

#
# Enrichers
#
OpenDxp\Bundle\AdminBundle\Enricher\:
resource: '../src/Enricher'

#
# Handlers
#
OpenDxp\Bundle\AdminBundle\Handler\:
resource: '../src/Handler'
bind:
$documentNoteTypes: '%opendxp_admin.dataObjects.notes_events.types%'
$assetNoteTypes: '%opendxp_admin.assets.notes_events.types%'
$objectNoteTypes: '%opendxp_admin.documents.notes_events.types%'

OpenDxp\Bundle\AdminBundle\Handler\Document\AddDocument\AddDocumentHandler:
arguments:
$documentClassResolver: '@opendxp.class.resolver.document'
$defaultDocumentController: '%opendxp.documents.default_controller%'

#
# Security Voters
#
OpenDxp\Bundle\AdminBundle\Security\Voter\:
resource: '../src/Security/Voter'
120 changes: 101 additions & 19 deletions docs/00_Architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,37 +26,119 @@ events that are specific to the admin UI lifecycle.

```
src/
├── Command/ # CLI commands (e.g. cache warm-up, admin user management)
├── Attribute/ # PHP attributes driving centralized listener behavior (e.g. AsHtmlContentTypeResponse)
├── Command/ # CLI commands (e.g. cache warm-up, admin user management)
├── Controller/
│ ├── Admin/ # Admin controllers, one subfolder per element type
│ ├── Admin/ # Admin controllers, one subfolder per element type, thin pass-throughs to Handlers
│ │ ├── Asset/
│ │ ├── DataObject/
│ │ ├── Document/ # DocumentController tree, site panel, site custom settings
│ │ ├── Document/ # DocumentController: tree, site panel, site custom settings
│ │ └── ...
│ └── Traits/ # Shared controller traits
├── DependencyInjection/ # Bundle extension + compiler passes
├── Enum/ # PHP enums for typed config values
├── Event/ # Admin event classes + AdminEvents constants
├── EventListener/ # Symfony event subscribers (internal)
├── Helper/ # Stateless service helpers (e.g. GridHelperService)
├── Model/ # Admin-only models (GridConfig, GridConfigShare, etc.)
├── Perspective/ # Perspective resolution and serialization
├── Security/ # Admin authentication, authenticators, security tokens
├── Service/ # Application services (grid data, workflow)
├── System/ # System-level services
├── Translation/ # Admin translation handling
└── Twig/ # Twig extensions for admin templates
│ └── Traits/ # Shared controller traits
├── CustomView/ # Custom document/asset/object tree view configuration
├── DataObject/ # Grid column config element definitions shared by DataObject grid Handlers
├── DependencyInjection/ # Bundle extension + compiler passes
├── Dto/ # Data transfer objects (grid configs, site custom settings, admin bootstrap settings)
├── Enricher/ # Adds admin-only data (permissions, admin styles) to tree/editor payloads
├── Enum/ # PHP enums for typed config values
├── Event/ # Admin event classes + AdminEvents constants
├── EventListener/ # Symfony event subscribers (internal)
├── Exception/ # Admin-specific exceptions (e.g. AdminOperationFailedException)
├── Factory/ # Factories for services that can't be constructed via plain DI
├── GDPR/ # GDPR data provider integrations (export/anonymize document, asset, object, mail data)
├── Handler/ # Business logic, one invokable class per action, see below
├── Helper/ # Stateless service helpers (e.g. GridHelperService)
├── Http/ # ExtJsValueResolver: resolves Payload::fromRequest() as a controller argument
├── Model/ # Admin-only models (GridConfig, GridConfigShare, etc.)
├── Payload/ # Shared request DTOs consumed by Handlers; action-specific ones live next to their Handler
├── Perspective/ # Perspective resolution and serialization
├── Repository/ # Persistence for admin-only data (e.g. per-user dashboard config), keyed by explicit params, no bound state
├── Security/ # Admin authentication, authenticators, security tokens
├── Service/ # Application services shared across Handlers (grid data, workflow, element resolution)
├── Session/ # Session access boundary: SessionGatewayInterface/SessionIdentityInterface and their Gateway/ implementations, see below
├── System/ # System-level services
├── Translation/ # Admin translation handling
└── Twig/ # Twig extensions for admin templates
```

## Handler / Payload / Service Pattern

Admin controller actions contain no business logic. Each action is a thin pass-through that
resolves a typed request Payload, invokes a Handler, and maps the result to JSON:

```php
#[Route('/save', name: 'save', methods: ['PUT'])]
public function saveAction(
SaveClassDefinitionPayload $payload,
SaveClassDefinitionHandler $handler,
): JsonResponse {
return $this->apiJson($handler($payload));
}
```

`AdminAbstractController::apiJson()` serializes a Handler's Result into the wire envelope
`{success: true, ...resultProperties}`. Pass `rootProperty: 'nodes'` when the established
wire contract for an endpoint is the bare value of a single Result property instead of the
enveloped shape. For a void Handler with nothing to report, use `apiOk()` (`{success: true}`).
Never build the response body by hand with `adminJson()` in a controller action, if a
Result's shape does not match what an endpoint needs to return, change the Result and the
Handler, not the controller.

| Layer | Location | Responsibility |
|------------|---------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Payload | `src/Payload/Common/` or colocated with a Handler | Typed request DTO. `fromRequest()` is resolved automatically by `ExtJsValueResolver` before the controller action runs, the only place request data is read. |
| Handler | `src/Handler/**` | One invokable (`__invoke`) class per action. Holds all business logic. Transport-agnostic, never receives the HTTP `Request`. |
| Result | colocated with its Handler | Typed return value, mapped to a JSON response by the controller. |
| Service | `src/Service/**` | Logic shared across multiple Handlers (grid data assembly, workflow resolution, element resolution). |
| Repository | `src/Repository/**` | Persistence only. |
| Enricher | `src/Enricher/**` | Adds admin-only presentation data (permissions, admin styles) to tree/editor node data, shared across Handlers. |

**Error handling:**

Handlers always throw on failure, they never return a `success: false` or `errorMessage` field.

`AdminExceptionListener` converts any exception raised during an admin XHR request into `{success: false, message, ...}` JSON at the exception's real HTTP status.

### AdminOperationFailedException
For an expected, recoverable business-rule failure that the admin UI handles locally as `success: false` at HTTP 200, not a real HTTP error,
throw `OpenDxp\Bundle\AdminBundle\Exception\AdminOperationFailedException` instead of a generic exception; the listener maps it to 200 explicitly rather than a 4xx/5xx status.

### ElementLockedException
Some exceptions carry their own dedicated response shape instead of the generic `{success: false, message}` body: `OpenDxp\Bundle\AdminBundle\Exception\ElementLockedException` is mapped by the listener to a 200 response with an `editlock` payload,
which the admin UI uses to show the lock dialog.

## Session Access

Controllers and Payloads never touch Symfony's HTTP session (`SessionInterface`, `AttributeBagInterface`, `Request::getSession()`), not even to read it.
A Payload only carries values taken from the current `Request`; anything session-derived is looked up by the Handler itself, at the point it is actually needed. Only two kinds of classes may reference session storage:

1. **A Gateway** implementing `OpenDxp\Bundle\AdminBundle\Session\SessionGatewayInterface`, living in `src/Session/Gateway/`. Each Gateway wraps exactly one session bag (see the `BAG_*` constants on `SessionGatewayInterface` for the full list of bags in use) behind a small, domain-named API, e.g. `CopySessionGateway::rememberParentId()`, not a raw `get()`/`set()`. Handlers and Services inject the Gateway they need.
2. **`OpenDxp\Bundle\AdminBundle\Session\SessionIdentityInterface`**, for the narrower case of needing only the current session id as a correlation token (lock ownership, bootstrap settings), not a session value. Carries no mutable state, so it may be injected anywhere.

Controller actions never contain session code, not even a call into a Gateway. Since that means an endpoint's session footprint is otherwise invisible without opening its Handler, mark the action with an attribute instead:

```php
#[SessionGatewayAware(CopySessionGateway::class)]
#[Route('/copy', name: 'opendxp_admin_asset_copy', methods: ['POST'])]
public function copyAction(CopyAssetPayload $payload, CopyAssetHandler $handler): JsonResponse
{
$handler($payload);

return $this->apiOk();
}
```

Use `#[SessionIdentityAware]` (no argument) when only `SessionIdentityInterface` is involved.

## Frontend Assets

JavaScript and CSS live in `public/js/` and `public/css/`. The bundle uses
[Webpack Encore](https://symfony.com/doc/current/frontend/encore/simple-example.html)
(`webpack.config.js`) for asset compilation.

Key JS entry points:
- `public/js/opendxp/events.js` all frontend event constants
- `public/js/opendxp/document/tree.js` document tree rendering
- `public/js/opendxp/events.js`: all frontend event constants
- `public/js/opendxp/document/tree.js`: document tree rendering

## Configuration Namespace

Expand All @@ -68,4 +150,4 @@ opendxp_admin:
custom_admin_route_name: ~
user:
default_key_bindings: ~
```
```
33 changes: 18 additions & 15 deletions docs/10_Extension_Points/01_Events.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,30 @@ Symfony autoconfiguration registers the subscriber automatically when `EventSubs

### Document Events

| Constant | Event Class | Description |
|----------------------------------|-------------|----------------------------------------------|
| `DOCUMENT_GET_PRE_SEND_DATA` | — | Before document data is sent to the frontend |
| `DOCUMENT_LIST_BEFORE_LIST_LOAD` | — | Before document listing is loaded |
| `DOCUMENT_LIST_AFTER_LIST_LOAD` | — | After document listing is loaded |
| Constant | Event Class | Description |
|----------------------------------|-------------|------------------------------------------------|
| `DOCUMENT_GET_PRE_SEND_DATA` | — | Before document data is sent to the frontend |
| `DOCUMENT_LIST_BEFORE_LIST_LOAD` | — | Before document listing is loaded |
| `DOCUMENT_LIST_AFTER_LIST_LOAD` | — | After document listing is loaded |
| `DOCUMENT_GET_IS_LOCKED` | — | Before the edit lock is handled for a document |

### Asset Events

| Constant | Event Class | Description |
|-------------------------------|-------------|-------------------------------------------|
| `ASSET_GET_PRE_SEND_DATA` | — | Before asset data is sent to the frontend |
| `ASSET_LIST_BEFORE_LIST_LOAD` | — | Before asset listing is loaded |
| `ASSET_LIST_AFTER_LIST_LOAD` | — | After asset listing is loaded |
| Constant | Event Class | Description |
|-------------------------------|-------------|----------------------------------------------|
| `ASSET_GET_PRE_SEND_DATA` | — | Before asset data is sent to the frontend |
| `ASSET_LIST_BEFORE_LIST_LOAD` | — | Before asset listing is loaded |
| `ASSET_LIST_AFTER_LIST_LOAD` | — | After asset listing is loaded |
| `ASSET_GET_IS_LOCKED` | — | Before the edit lock is handled for an asset |

### Object Events

| Constant | Event Class | Description |
|--------------------------------|-------------|-------------------------------------------------|
| `OBJECT_GET_PRE_SEND_DATA` | — | Before data object data is sent to the frontend |
| `OBJECT_LIST_BEFORE_LIST_LOAD` | — | Before object listing is loaded |
| `OBJECT_LIST_AFTER_LIST_LOAD` | — | After object listing is loaded |
| Constant | Event Class | Description |
|--------------------------------|-------------|---------------------------------------------------|
| `OBJECT_GET_PRE_SEND_DATA` | — | Before data object data is sent to the frontend |
| `OBJECT_LIST_BEFORE_LIST_LOAD` | — | Before object listing is loaded |
| `OBJECT_LIST_AFTER_LIST_LOAD` | — | After object listing is loaded |
| `OBJECT_GET_IS_LOCKED` | — | Before the edit lock is handled for a data object |

### Element Style Events

Expand Down
Loading
Loading