From drupal-workflow
Provides Drupal 10/11 service definitions, constructor injection, interfaces, and plugin factories. Use when creating services, registering dependencies, or service container patterns.
How this skill is triggered — by the user, by Claude, or both
Slash command
/drupal-workflow:drupal-service-diThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Always use constructor injection — never use `\Drupal::` static calls in classes. Always check the structural index before creating new services to avoid duplicates. Prefer interface type-hints over concrete classes.
Always use constructor injection — never use \Drupal:: static calls in classes. Always check the structural index before creating new services to avoid duplicates. Prefer interface type-hints over concrete classes.
# my_module.services.yml
services:
my_module.content_manager:
class: Drupal\my_module\ContentManager
arguments:
- '@entity_type.manager'
- '@current_user'
- '@logger.channel.my_module'
my_module.event_subscriber:
class: Drupal\my_module\EventSubscriber\MyEventSubscriber
arguments:
- '@my_module.content_manager'
tags:
- { name: event_subscriber }
declare(strict_types=1);
namespace Drupal\my_module;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Psr\Log\LoggerInterface;
final class ContentManager implements ContentManagerInterface {
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly AccountProxyInterface $currentUser,
private readonly LoggerInterface $logger,
) {}
}
declare(strict_types=1);
namespace Drupal\my_module;
interface ContentManagerInterface {
/**
* Loads content items for the current user.
*
* @return \Drupal\node\NodeInterface[]
* Array of node entities.
*/
public function loadUserContent(): array;
}
declare(strict_types=1);
namespace Drupal\my_module\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\my_module\ContentManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a content list block.
*
* @Block(
* id = "my_module_content_list",
* admin_label = @Translation("Content List"),
* )
*/
final class ContentListBlock extends BlockBase implements ContainerFactoryPluginInterface {
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
private readonly ContentManagerInterface $contentManager,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
public static function create(
ContainerInterface $container,
array $configuration,
$plugin_id,
$plugin_definition,
): static {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('my_module.content_manager'),
);
}
public function build(): array {
return $this->contentManager->loadUserContent();
}
}
// Bad: static service call.
$node = \Drupal::entityTypeManager()->getStorage('node')->load($nid);
// Good: injected dependency.
$node = $this->entityTypeManager->getStorage('node')->load($nid);
// Bad: no interface type-hint.
class MyService {
public function __construct(private readonly ContentManager $manager) {}
}
// Good: interface type-hint.
class MyService {
public function __construct(private readonly ContentManagerInterface $manager) {}
}
Service names follow no universal convention. Don't guess -- verify.
# Quick check: does a service exist?
ddev drush eval 'print json_encode(["exists" => Drupal::hasService("module_name.service_name")]);'
When a service name fails: Read the module's *.services.yml directly rather than guessing variations. The module prefix may be singular (group_permission.checker) when you expect plural (group_permissions.checker).
When a module exists in both vendor/drupal/ and web/modules/contrib/, only one is actually loaded by PHP.
// Find which file is running at runtime
$ref = new \ReflectionMethod($service, 'methodName');
echo $ref->getFileName();
// Edit the file that ReflectionMethod reports, not the one you assume.
Common scenario: A composer-patched module. The original sits in vendor/, the patched version in web/modules/contrib/. Editing the vendor copy has no effect because the autoloader loads from contrib.
| Service ID | Interface | Purpose |
|---|---|---|
entity_type.manager | EntityTypeManagerInterface | Entity CRUD |
current_user | AccountProxyInterface | Current user |
config.factory | ConfigFactoryInterface | Configuration |
logger.factory | LoggerChannelFactoryInterface | Logging |
messenger | MessengerInterface | User messages |
module_handler | ModuleHandlerInterface | Module operations |
state | StateInterface | Key-value state |
cache.default | CacheBackendInterface | Default cache |
npx claudepluginhub gkastanis/drupal-workflow --plugin drupal-workflowEnforces core Drupal 10+ rules for services, dependency injection, security including sanitization and access control, code quality, and testing verification. Always use when writing Drupal code.
Searches Drupal core modules for patterns like forms, entities, services, plugins, controllers; returns file paths, quick references, and grep/glob strategies.
Guides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.