|
| 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 | +``` |
0 commit comments