Skip to content

Commit dabad4e

Browse files
committed
Merge branch '2025.4' into 2026.1
2 parents 9af1ce3 + 3f26a11 commit dabad4e

70 files changed

Lines changed: 3933 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

config/ownership_management.yaml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
services:
2+
_defaults:
3+
autowire: true
4+
autoconfigure: true
5+
public: false
6+
7+
# controllers are imported separately to make sure they're public
8+
# and have a tag that allows actions to type-hint services
9+
Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Controller\:
10+
resource: '../src/OwnershipManagement/Controller'
11+
public: true
12+
tags: [ 'controller.service_arguments' ]
13+
14+
#
15+
# Services
16+
#
17+
18+
Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Service\OwnershipManagementServiceInterface:
19+
class: Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Service\OwnershipManagementService
20+
21+
Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Service\ProviderLoaderInterface:
22+
class: Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Service\Loader\TaggedIteratorProviderLoader
23+
24+
Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Service\JobServiceInterface:
25+
class: Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Service\JobService
26+
27+
Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Service\Filter\InMemoryCollectionFilterInterface:
28+
class: Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Service\Filter\InMemoryCollectionFilter
29+
30+
Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Service\Filter\EntityCollectionFilterInterface:
31+
class: Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Service\Filter\EntityCollectionFilter
32+
33+
Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Service\SortOrderResolverInterface:
34+
class: Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Service\SortOrderResolver
35+
36+
#
37+
# Hydrators
38+
#
39+
40+
Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Hydrator\ConfigurationTypeHydratorInterface:
41+
class: Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Hydrator\ConfigurationTypeHydrator
42+
43+
Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Hydrator\OwnershipConfigurationHydratorInterface:
44+
class: Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Hydrator\OwnershipConfigurationHydrator
45+
46+
#
47+
# Providers
48+
#
49+
50+
Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Provider\GridConfigurationProvider:
51+
tags: [ 'pimcore.studio_backend.ownership_provider' ]
52+
53+
Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Provider\SavedSearchConfigurationProvider:
54+
tags: [ 'pimcore.studio_backend.ownership_provider' ]
55+
56+
#
57+
# Execution engine handlers
58+
#
59+
60+
Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\ExecutionEngine\Handler\ReassignOwnerHandler: ~
61+
62+
Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\ExecutionEngine\Handler\DeleteConfigurationsHandler: ~
63+
64+
#
65+
# Event subscribers
66+
#
67+
68+
Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\EventSubscriber\OwnershipManagementSubscriber: ~

config/pimcore/execution_engine.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,6 @@ framework:
3434
Pimcore\Bundle\StudioBackendBundle\RecycleBin\ExecutionEngine\Messages\RestoreItemsMessage: pimcore_generic_execution_engine
3535
Pimcore\Bundle\StudioBackendBundle\Element\ExecutionEngine\AutomationAction\Messenger\Messages\ElementUsageReplaceMessage: pimcore_generic_execution_engine
3636
Pimcore\Bundle\StudioBackendBundle\Class\ExecutionEngine\AutomationAction\Messenger\Messages\BulkImportMessage: pimcore_generic_execution_engine
37-
Pimcore\Bundle\StudioBackendBundle\Class\ExecutionEngine\AutomationAction\Messenger\Messages\BulkImportCleanupMessage: pimcore_generic_execution_engine
37+
Pimcore\Bundle\StudioBackendBundle\Class\ExecutionEngine\AutomationAction\Messenger\Messages\BulkImportCleanupMessage: pimcore_generic_execution_engine
38+
Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\ExecutionEngine\Messages\ReassignOwnerMessage: pimcore_generic_execution_engine
39+
Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\ExecutionEngine\Messages\DeleteConfigurationsMessage: pimcore_generic_execution_engine
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
---
2+
title: Ownership Management Providers
3+
description: Register custom configuration types that can be administered in the Ownership Management area.
4+
---
5+
6+
# Extending Ownership Management
7+
8+
The Ownership Management area is an **admin-only** screen that lets administrators list, reassign the
9+
owner of (this is especially useful when the current owner does not exist in the system anymore),
10+
and delete *user-owned configurations* across all users (for example, Grid Configurations or
11+
Dashboards).
12+
13+
Add a new type by implementing `OwnershipProviderInterface` and tagging the service with
14+
`pimcore.studio_backend.ownership_provider`. The implementation may live in **any bundle** that depends
15+
on the Studio Backend Bundle. The Studio Backend auto-discovers tagged providers and exposes them through
16+
the generic ownership-management endpoints — no new controllers or routes are required.
17+
18+
## How It Works
19+
20+
The generic endpoints resolve the provider by its `type` (taken from the request path) and delegate to subsequent endpoints.
21+
22+
Implement these methods on your custom provider:
23+
24+
- `getType()`: Unique, machine-readable identifier used in the route (e.g. `grid_configuration`).
25+
- `getLabel()`: Translation key (in the `studio` domain) for the tab label.
26+
- `getIcon()`: Icon identifier for the tab.
27+
- `getSortPriority()`: Higher values are listed first.
28+
- `listConfigurations(OwnershipListQuery $query)`: Returns a `Collection` of `OwnershipConfiguration`.
29+
- `reassignOwner(array $ids, int $newOwnerId)`: Reassigns the owner of the given configurations.
30+
- `delete(array $ids)`: Deletes the given configurations.
31+
32+
### Listing
33+
34+
`listConfigurations()` receives an `OwnershipListQuery` and must return a
35+
`Collection<OwnershipConfiguration>` (items for the current page plus the total count). The query exposes
36+
everything a provider needs, already extracted from the request:
37+
38+
- `getOffset()` / `getLimit()`: pagination window.
39+
- `getSearchTerm()`: a single free-text term to match against the configuration name, its id and the
40+
owner's username (`null` when no search is active).
41+
- `includeDeletedOwners()`: when `false`, configurations whose owner no longer exists must be hidden
42+
(defaults to `true`).
43+
- `getSortField()` / `getSortDirection()`: sorting.
44+
45+
Build each row with the `OwnershipConfigurationHydrator`.
46+
47+
### Reusing the in-memory filter
48+
49+
If your configurations reuse the [LocationAwareConfigRepository](https://docs.pimcore.com/platform/Pimcore/Development_Details/Configuration/Configuration_Environments/#configuration-storage-locations-and-fallbacks-locationawareconfigrepository), do **not** re-implement search, the deleted-owner filter, sorting, and pagination.
50+
Hydrate all of your items and delegate to `InMemoryCollectionFilterInterface::apply()`, which performs the
51+
whole pipeline for you.
52+
53+
### Owner reassignment and deletion (async)
54+
55+
`reassignOwner()` and `delete()` are reused for both the synchronous and asynchronous paths — you do not
56+
implement anything extra for batching. The service runs a single id synchronously; for multiple ids it
57+
creates a Generic Execution Engine job whose handlers call your provider per batch and report progress.
58+
Both methods receive the configuration ids as `string[]`; cast them to your storage's id type internally.
59+
60+
## Example Provider
61+
62+
The example below is an in-memory provider that delegates listing to the shared filter. A real,
63+
database-backed example is the `GridConfigurationProvider` in this bundle, and a configuration-file based
64+
example is the `DashboardOwnershipProvider` in the `pimcore/studio-dashboards-bundle`.
65+
66+
```php
67+
use Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Hydrator\OwnershipConfigurationHydratorInterface;
68+
use Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Provider\OwnershipProviderInterface;
69+
use Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Query\OwnershipListQuery;
70+
use Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Service\Filter\InMemoryCollectionFilterInterface;
71+
use Pimcore\Bundle\StudioBackendBundle\Response\Collection;
72+
73+
final readonly class MyConfigurationProvider implements OwnershipProviderInterface
74+
{
75+
private const string TYPE = 'my_configuration';
76+
77+
public function __construct(
78+
private MyConfigurationRepository $repository,
79+
private OwnershipConfigurationHydratorInterface $hydrator,
80+
private InMemoryCollectionFilterInterface $collectionFilter,
81+
) {
82+
}
83+
84+
public function getType(): string
85+
{
86+
return self::TYPE;
87+
}
88+
89+
public function getLabel(): string
90+
{
91+
return 'ownership_management_type_my_configuration';
92+
}
93+
94+
public function getIcon(): string
95+
{
96+
return 'settings';
97+
}
98+
99+
public function getSortPriority(): int
100+
{
101+
return 10;
102+
}
103+
104+
public function listConfigurations(OwnershipListQuery $query): Collection
105+
{
106+
$items = [];
107+
$listItems = $this->repository->findAll($query->getSearchTerm(), $query->getLimit(), $query->getOffset();
108+
foreach ($listItems) as $config) {
109+
$items[] = $this->hydrator->hydrate(
110+
(string) $config->getId(),
111+
self::TYPE,
112+
$config->getName(),
113+
$config->getOwner(),
114+
);
115+
}
116+
117+
return $this->collectionFilter->apply($items, $query);
118+
}
119+
120+
public function reassignOwner(array $ids, int $newOwnerId): void
121+
{
122+
foreach ($ids as $id) {
123+
$config = $this->repository->getById($id);
124+
$config->setOwner($newOwnerId);
125+
$this->repository->save($config);
126+
}
127+
}
128+
129+
public function delete(array $ids): void
130+
{
131+
foreach ($ids as $id) {
132+
$this->repository->delete($this->repository->getById($id));
133+
}
134+
}
135+
}
136+
```
137+
138+
Register the provider and tag it:
139+
140+
```yaml
141+
services:
142+
App\OwnershipManagement\MyConfigurationProvider:
143+
tags: [ 'pimcore.studio_backend.ownership_provider' ]
144+
```
145+
146+
Finally, add the tab label translation key to the `studio` domain:
147+
148+
```yaml
149+
# translations/studio.en.yaml
150+
ownership_management_type_my_configuration: My Configurations
151+
```

doc/03_Extending/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ pimcore_studio_backend:
4848
- [Perspectives and Widgets](./11_Perspectives/README.md)
4949
- [Custom Widget Types](./11_Perspectives/01_Extending_Widgets.md)
5050
- [GDPR Data Providers](./12_Extending_GDPR_Data_Providers.md)
51+
- [Ownership Management Providers](./13_Extending_Ownership_Management.md)

src/ExecutionEngine/Util/Config.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,6 @@ enum Config: string
4545
case RECYCLE_BIN_RESTORE_FAILED = 'studio_ee_recycle_bin_restore_failed';
4646
case ELEMENT_REPLACE_ASSIGNMENT_FAILED = 'studio_ee_element_replace_assignment_failed';
4747
case BULK_IMPORT_FAILED_MESSAGE = 'studio_ee_bulk_import_failed';
48+
case OWNERSHIP_MANAGEMENT_REASSIGN_FAILED = 'studio_ee_ownership_management_reassign_failed';
49+
case OWNERSHIP_MANAGEMENT_DELETE_FAILED = 'studio_ee_ownership_management_delete_failed';
4850
}

src/ExecutionEngine/Util/Jobs.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,6 @@ enum Jobs: string
3838
case RECYCLE_BIN_RESTORE = 'studio_ee_job_recycle_bin_restore';
3939
case ELEMENT_USAGE_REPLACE = 'studio_ee_job_element_usage_replace';
4040
case BULK_IMPORT_CLASS_DEFINITIONS = 'studio_ee_job_bulk_import_class_definitions';
41+
case OWNERSHIP_MANAGEMENT_REASSIGN_OWNER = 'studio_ee_job_ownership_management_reassign_owner';
42+
case OWNERSHIP_MANAGEMENT_DELETE = 'studio_ee_job_ownership_management_delete';
4143
}

src/ExecutionEngine/Util/StepConfig.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,7 @@ enum StepConfig: string
5050
case ITEMS_TO_DELETE = 'items_to_delete';
5151
case ELEMENT_TYPE_TO_BATCH_DELETE = 'element_type_to_batch_delete';
5252
case ITEMS_TO_RESTORE = 'items_to_restore';
53+
case CONFIGURATION_IDS = 'configuration_ids';
54+
case CONFIGURATION_TYPE = 'configuration_type';
55+
case NEW_OWNER_ID = 'new_owner_id';
5356
}

src/Grid/Repository/ConfigurationRepository.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Doctrine\ORM\EntityManagerInterface;
1717
use Pimcore\Bundle\StudioBackendBundle\Entity\Grid\GridConfiguration;
1818
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException;
19+
use Pimcore\Bundle\StudioBackendBundle\OwnershipManagement\Service\Filter\EntityCollectionFilterInterface;
1920

2021
/**
2122
* @internal
@@ -24,6 +25,7 @@
2425
{
2526
public function __construct(
2627
private EntityManagerInterface $entityManager,
28+
private EntityCollectionFilterInterface $entityCollectionFilter,
2729
) {
2830
}
2931

@@ -99,6 +101,40 @@ public function getByClassId(string $classId): array
99101
return $this->entityManager->getRepository(GridConfiguration::class)->findBy(['classId' => $classId]);
100102
}
101103

104+
public function findAllPaginated(
105+
int $offset,
106+
int $limit,
107+
?string $searchTerm = null,
108+
array $ownerIds = [],
109+
array $excludeOwnerIds = [],
110+
array $sortBy = [],
111+
): array {
112+
return $this->entityCollectionFilter->findAllPaginated(
113+
GridConfiguration::class,
114+
$offset,
115+
$limit,
116+
$searchTerm,
117+
$ownerIds,
118+
$excludeOwnerIds,
119+
$sortBy,
120+
);
121+
}
122+
123+
public function countAll(?string $searchTerm = null, array $ownerIds = [], array $excludeOwnerIds = []): int
124+
{
125+
return $this->entityCollectionFilter->countAll(
126+
GridConfiguration::class,
127+
$searchTerm,
128+
$ownerIds,
129+
$excludeOwnerIds,
130+
);
131+
}
132+
133+
public function getDistinctOwnerIds(): array
134+
{
135+
return $this->entityCollectionFilter->getDistinctOwnerIds(GridConfiguration::class);
136+
}
137+
102138
public function delete(GridConfiguration $configuration): void
103139
{
104140
$this->entityManager->remove($configuration);

src/Grid/Repository/ConfigurationRepositoryInterface.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,34 @@ public function getForAsset(): array;
4747
*/
4848
public function getByClassId(string $classId): array;
4949

50+
/**
51+
* Returns all grid configurations (asset and data object) across all users, paginated.
52+
*
53+
* @param int[] $ownerIds
54+
* @param int[] $excludeOwnerIds
55+
* @param array<array{field?: string, direction?: string}> $sortBy ordered sort instructions
56+
*
57+
* @return GridConfiguration[]
58+
*/
59+
public function findAllPaginated(
60+
int $offset,
61+
int $limit,
62+
?string $searchTerm = null,
63+
array $ownerIds = [],
64+
array $excludeOwnerIds = [],
65+
array $sortBy = [],
66+
): array;
67+
68+
/**
69+
* @param int[] $ownerIds
70+
* @param int[] $excludeOwnerIds
71+
*/
72+
public function countAll(?string $searchTerm = null, array $ownerIds = [], array $excludeOwnerIds = []): int;
73+
74+
/**
75+
* @return int[]
76+
*/
77+
public function getDistinctOwnerIds(): array;
78+
5079
public function delete(GridConfiguration $configuration): void;
5180
}

src/OpenApi/Config/Tags.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@
9898
name: Tags::Notifications->value,
9999
description: 'tag_notifications_description'
100100
)]
101+
#[Tag(
102+
name: Tags::OwnershipManagement->value,
103+
description: 'tag_ownership_management_description'
104+
)]
101105
#[Tag(
102106
name: Tags::Perspectives->value,
103107
description: 'tag_perspectives_description'
@@ -195,6 +199,7 @@ enum Tags: string
195199
case Metadata = 'Metadata';
196200
case Notes = 'Notes';
197201
case Notifications = 'Notifications';
202+
case OwnershipManagement = 'Ownership Management';
198203
case Perspectives = 'Perspectives';
199204
case Properties = 'Properties';
200205
case RecycleBin = 'Recycle Bin';

0 commit comments

Comments
 (0)