dotkernel-psr-standards
Use when writing or reviewing any PHP in a Dotkernel project — which PSR applies, how to type handlers and middleware (PSR-7/PSR-15), how to obtain services (PSR-11, no service locator), logging (PSR-
SKILL.md.claude/skills/dotkernel-psr-standards/SKILL.md
GitHubPSR compliance
Applies to
Projects: - all
Packages: - all
Program against PSR interfaces. Type-hint the interface, inject the implementation. This applies identically in every Dotkernel project.
PSR-4 — autoloading
Namespace mirrors directory, case-sensitive, one class per file, file named after the class.
<App>\User\Handler\User\GetUserResourceHandler → src/User/src/Handler/User/GetUserResourceHandler.php
Core\User\Entity\User → src/Core/src/User/src/Entity/User.php
Mappings live in composer.json under autoload.psr-4 (and autoload-dev for tests). Adding a
mapping edits composer.json → needs user approval, then composer dump-autoload.
PSR-7 — HTTP messages
Messages are immutable; with*() returns a new instance.
$response = $response->withHeader('sunset', $date); // correct
$response->withHeader('sunset', $date); // silently does nothing
Type-hint Psr\Http\Message\ServerRequestInterface / ResponseInterface, never a concrete
Diactoros class. Read the body once via (array) $request->getParsedBody() — the body-params
middleware has already parsed it; re-reading the raw stream needs a rewind().
PSR-15 — handlers and middleware
final class XMiddleware implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
// before…
$response = $handler->handle($request);
// after…
return $response;
}
}
- Handlers implement
RequestHandlerInterface; extend the project's abstract handler for its response helpers. - One responsibility per middleware. Order is declared in
config/pipeline.php, and changing it affects every request — justify any edit. - Middleware that only enriches the request passes data via
$request->withAttribute(...).
PSR-11 — container
Get services by constructor injection, resolved by the container:
#[Inject(BookServiceInterface::class, CreateBookInputFilter::class)]
public function __construct(
protected BookServiceInterface $bookService,
protected CreateBookInputFilter $inputFilter,
) {
}
dot-dependency-injection reads #[Inject]; register the class with
AttributedServiceFactory::class in the module ConfigProvider. The stock Light does not install
dot-dependency-injection and uses hand-written factories instead; match that there. #[Inject] can also pull
config values by dotted key.
Do not inject ContainerInterface and call get() inside a class — that is the
service-locator anti-pattern. The only legitimate $container->get() sites are factories and
delegators, where you also declare ContainerExceptionInterface and
NotFoundExceptionInterface.
PSR-3 — logging
Type-hint Psr\Log\LoggerInterface. Use levels deliberately: error for actionable failures,
warning for recoverable, info for lifecycle. Pass context as an array rather than
concatenating. Never log credentials, tokens, password hashes, auth request bodies, or PII
beyond an identifier.
PSR-6 / PSR-16 — caching
Type-hint Psr\Cache\CacheItemPoolInterface (PSR-6) or Psr\SimpleCache\CacheInterface
(PSR-16). Always set an explicit TTL, never cache per-user-authorized data under a global key,
and provide an invalidation path when the underlying entity changes.
PSR-17 — HTTP factories
Type-hint ResponseFactoryInterface / StreamFactoryInterface rather than instantiating a
concrete response — except inside the abstract handler, which already wraps the project's
response factory. Prefer its helpers over building responses yourself.
PSR-1 / PSR-12 — style, enforced by the Laminas Coding Standard
phpcs.xml applies LaminasCodingStandard, a PSR-12 superset. It will fail you on:
declare(strict_types=1);after<?phpand one blank line.- 4-space indent, LF endings, one trailing newline, no trailing whitespace, ~120-char lines.
- Sorted
usestatements: class imports first, blank line, thenuse function/use const— global functions must be imported, not called as\count(). - Short array syntax; short closures where applicable.
! $foo, not!$foo.- Aligned
=>in array literals and aligned=in consecutive assignments. - One class per file;
finalwhere the class is not designed for extension. - Type declarations on every parameter, property and return; nullable as
?Type.
Run the repo's cs-fix for the mechanical parts. Never add sniff exclusions to phpcs.xml or
sprinkle // phpcs:ignore to get green.
phpstan expectations
Dotkernel projects run a high level (commonly 8) with the Doctrine and PHPUnit extensions:
- No
mixedleaking — cast or narrow at the boundary. - Array shapes documented:
@param array<non-empty-string, mixed>,@return X[], and@phpstan-type/@phpstan-import-typefor shapes reused across classes. - Generics on collections:
@var Collection<int, UserRole>. instanceofchecks before using a nullable repository result — that is how the services here raise not-found errors.- Do not silence with
@phpstan-ignore-*. Fix the type.
Framework-specific but non-negotiable
- Status codes via
Fig\Http\Message\StatusCodeInterface::STATUS_*, not integer literals. - Throw the project's typed exceptions with their static
::create(...)constructors; messages from the shared message catalogue. - Where the project provides a resource-resolution attribute on
handle(), use it instead of fetching the entity manually.