skills/pimcore-studio-backend-service/SKILL.md
Pimcore Studio service layer patterns — permission checks, current user resolution, StaticResolverBundle usage, trait patterns, event dispatching, and cleanup with try/finally
npx skillsauth add pimcore/skills pimcore-studio-backend-serviceInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The StudioBackendBundle uses a layered architecture: Controllers (HTTP only) -> Services (business logic) -> Hydrators (DTO creation). Events provide extension points. All APIs are OpenAPI-documented.
Services contain ALL business logic: permission checks, orchestration of vendor calls, exception conversion. Controllers delegate entirely to services.
final readonly class ConfigurationService implements ConfigurationServiceInterface
{
public function __construct(
private ConfigurationDetailHydratorInterface $configurationDetailHydrator
) {
}
public function getConfiguration(string $name): ConfigurationDetail
{
$config = $this->loadConfigurationWithPermission(
$name,
PermissionConstants::PLUGIN_DATA_IMPORTER_PERMISSION_READ
);
return $this->configurationDetailHydrator->hydrate($config);
}
}
final readonly class with @internal.{Domain}ServiceInterface / {Domain}ServiceService\Studio\ServiceInterface / Service) instead of repeating the domain. For example, prefer Pimcore\Bundle\CopilotBundle\Service\Studio\Configuration\ServiceInterface over ConfigurationServiceInterface when the Configuration namespace already identifies the domain.loadConfigurationWithPermission() private method.loadConfigurationWithPermission, resolveCurrentUser) into a trait or helper service rather than duplicating them across services.new SomeResponse(...) directly in services (except simple delegation).$response = $this->hydrator->hydrateSomething(...);
$this->eventDispatcher->dispatch(new SomeEvent($response), SomeEvent::EVENT_NAME);
return $response;
Use SecurityServiceInterface::getCurrentUser() for resolving the current user. Never use UserLoader::getUser().
Always add instanceof User check after getCurrentUser():
$user = $this->securityService->getCurrentUser();
if (!$user instanceof User) {
throw new EnvironmentException('Could not resolve current user');
}
private function loadConfigurationWithPermission(string $name, string $permission): Configuration
{
$config = Configuration::getByName($name);
if (!$config) {
throw new NotFoundHttpException(
sprintf('Configuration with name "%s" not found', $name)
);
}
if (!$config->isAllowed($permission)) {
throw new ForbiddenException(
sprintf('Access denied to configuration "%s"', $name)
);
}
return $config;
}
Keep returning the Configuration object even when callers discard it -- consistency over micro-optimization.
Use try/finally for cleanup of temp files/resources:
try {
// ... use file ...
} finally {
@unlink($file->getPathname());
}
When building sort arrays from optional query parameters, always null-check before constructing the array. Passing [null => null] to Doctrine/repository methods causes unexpected behavior or errors:
// BAD -- if sortBy/sortOrder are null, creates [null => null]
$sortInformation = [$parameters->getSortBy() => $parameters->getSortOrder()];
// GOOD -- guard against null
$sortInformation = [];
$sortBy = $parameters->getSortBy();
$sortOrder = $parameters->getSortOrder();
if ($sortBy !== null && $sortOrder !== null) {
$sortInformation[$sortBy] = $sortOrder;
}
Never make static calls to Pimcore classes that live outside the current bundle. This includes all static calls to Pimcore core classes (e.g., Pimcore\Tool, Pimcore\Model\Asset, Pimcore\Logger). Third-party library static calls (e.g., PHPUnit, Symfony helpers, utility libraries) are not affected by this rule.
Static calls to Pimcore classes couple the code to a concrete implementation, making it untestable and impossible to override. The correct replacement is Pimcore\Bundle\StaticResolverBundle, which provides injectable interface wrappers for all common Pimcore static calls.
Pimcore\Bundle\StaticResolverBundle (located at dev/pimcore/static-resolver-bundle/src/).Pimcore\Tool::getValidLanguages()// BAD -- static call to a class outside the bundle
use Pimcore\Tool;
foreach (Tool::getValidLanguages() as $lang) { ... }
// GOOD -- inject ToolResolverInterface and call the instance method
use Pimcore\Bundle\StaticResolverBundle\Lib\ToolResolverInterface;
public function __construct(
private ToolResolverInterface $toolResolver,
) {}
foreach ($this->toolResolver->getValidLanguages() as $lang) { ... }
Browse dev/pimcore/static-resolver-bundle/src/ to find the resolver for the static class you need to replace:
| Static Class | Resolver Interface |
|---|---|
| Pimcore\Tool | Pimcore\Bundle\StaticResolverBundle\Lib\ToolResolverInterface |
| Pimcore\Model\Asset | Pimcore\Bundle\StaticResolverBundle\Models\Asset\AssetServiceInterface |
| Pimcore\Model\DataObject\* | Pimcore\Bundle\StaticResolverBundle\Models\DataObject\* |
| Pimcore\Model\Document\* | Pimcore\Bundle\StaticResolverBundle\Models\Document\* |
If no resolver exists yet for the static class you need, check with the team before creating a new one.
This rule applies to all layers: services, hydrators, event listeners. It does not apply to legacy controllers (which are not to be modified during migration).
Shared traits live in Service\Studio\Traits\.
/**
* Requires the using class to have a property: SecurityServiceInterface $securityService
*
* @internal
*
* @property SecurityServiceInterface $securityService
*/
trait CurrentUserResolverTrait
{
/**
* @throws EnvironmentException if the current user cannot be resolved
*/
private function resolveCurrentUser(): User
{
$user = $this->securityService->getCurrentUser();
if (!$user instanceof User) {
throw new EnvironmentException('Could not resolve current user');
}
return $user;
}
}
@internal -- traits are internal implementation details.@property PHPDoc -- when a trait accesses $this->someProperty from the using class, document it with @property Type $propertyName. Also add a human-readable sentence: "Requires the using class to have a property: ...". If the trait makes no $this->... calls (e.g., only static calls), omit @property.@throws goes on trait methods directly -- this is the exception to the "interfaces only" rule. Traits have no interfaces, so @throws must be documented on the trait methods themselves.private -- trait methods like loadConfigurationWithPermission() and resolveCurrentUser() are private helpers, not part of any public contract.tools
UX and UI design conventions for Pimcore Studio - layout, spacing, action labels, writing style, and design principles for consistent extensions
tools
Widget system in Pimcore Studio UI - registering widgets, opening them in layout areas, WidgetManagerTabConfig, and connecting widgets to navigation
tools
How bundles consume the Pimcore Studio UI SDK - plugins, modules, DI, registries, and imports
development
TypeScript coding standards and best practices for Pimcore Studio UI - type safety, null checks, and code quality