Hyperf Backend Skill
Generic skill for generating Hyperf-based microservice backend code aligned with DDD + Clean Architecture, using menumbing/* packages and Kafka event streaming.
Placeholder Convention
When applying this skill, replace:
| Placeholder |
Format |
Example |
{ProjectName} |
PascalCase — used in PHP namespaces |
AcmeCorp, MyPlatform, SampleApp |
{project-name} |
kebab-case — used in DB names, Kafka group, Docker names |
acmecorp, myplatform, sampleapp |
{project_name} |
snake_case — used in DB schema, prefix, env vars |
acmecorp, myplatform |
{Domain} |
PascalCase — bounded context name |
Identity, Vacancy, Payment |
{service} |
kebab-case — service identifier |
catalog, auth, payment |
{Name} / {Entity} |
PascalCase — entity / aggregate name |
Program, User |
{Action} |
PascalCase verb — use case action |
Create, Update, Verify, Approve, Reject, Cancel, Search, Rotate, Submit |
{Suffix} |
PascalCase role for Service classes |
Creator, Updater, Verifier, Rotator, Checker, Manager, Resolver, Approver |
Project Init
Create new service using:
composer create-project hyperf/hyperf-skeleton:dev-master
Namespace convention: {ProjectName}\{Domain}\ (e.g., {ProjectName}\Identity, {ProjectName}\Vacancy, {ProjectName}\Common)
Folder Structure
config/
├── autoload/
│ ├── auth.php
│ ├── async_queue.php
│ ├── cache.php
│ ├── databases.php
│ ├── dependencies.php
│ ├── event_stream.php
│ ├── exceptions.php
│ ├── graceful_process.php
│ ├── http_client.php
│ ├── middlewares.php
│ ├── oauth2_resource_server.php
│ ├── opentracing.php
│ ├── orm.php
│ ├── redis.php
│ ├── resource.php
│ ├── {upstream}.php # One per outbound upstream (credentials/identifiers, env-backed)
│ └── ...
├── config.php
├── container.php
└── routes.php
migrations/
seeders/
src/
├── Action/ # Use Case Handlers (Application Layer) with routing annotations
├── Cache/ # Cache key generator classes
├── Command/ # CLI commands (sync, scheduler, batch)
├── Constant/ # Enums and constants
├── Controller/ # Rare, for complex auth flows
├── Console/ # Simple CLI command wrappers (thin over HyperfCommand)
├── DTO/ # Data Transfer Objects (per module)
├── Event/ # Domain Events: ProducedEvent (outbound) and ConsumedEvent (inbound) annotations
├── Exception/ # Custom exceptions + Handler/
├── Factory/ # DI factories (e.g., HttpClient construction)
├── Finder/ # Read-only model lookups (findOrFail pattern)
├── HttpClient/ # Outbound HTTP clients per upstream (one folder per service)
├── Job/ # Async queue jobs (when extending Hyperf\AsyncQueue\Job)
├── Listener/ # Event listeners (also async queue handlers via #[AsyncQueueMessage])
├── Middleware/ # HTTP middlewares (server-side and HttpClient/ middlewares)
├── Model/ # Eloquent-like models (Menumbing ORM)
├── Query/ # Query objects (rare)
├── Relation/ # Relation traits for models
├── Repository/ # Repository classes
├── Request/ # Form validation requests
├── Resource/ # API response resources
└── Service/ # Domain services (business logic)
Key Patterns — Quick Reference
For full code templates, see references/patterns.md.
| Pattern |
Naming |
Location |
| Action (Use Case Handler) |
{Action}{Name}Action — invoked via __invoke() |
src/Action/ |
| Model |
{Name} — extends Menumbing\Orm\Model, UUID v7 primary key |
src/Model/ |
| Repository |
{Name}Repository — annotated with #[AsRepository] |
src/Repository/ |
| Domain Service |
{Name}{Suffix} — final class, one method, #[Transactional] |
src/Service/{Name}/ |
| DTO |
{Action}{Name}DTO — readonly properties, one per use case |
src/DTO/ |
| Domain Event (Produced) |
{Entity}{Action} — #[ProducedEvent] annotation, Kafka stream |
src/Event/ |
| Consumed Event |
{Action}{Entity} — #[ConsumedEvent] annotation, inbound Kafka |
src/Event/{Domain}/ |
| Finder |
{Entity}Finder — readonly class, findOrFail() lookups |
src/Finder/ |
| Migration |
UUID primary key, datetimes(), softDeletes() |
migrations/ |
| Cache Key |
{Name}Key::generate() — prefixed with {project_name}: |
src/Cache/ |
Suffixes (Domain Service roles)
| Suffix |
Role |
Example |
Creator |
Creates a new aggregate |
UserCreator |
Updater |
Updates an existing aggregate |
ProgramUpdater |
Verifier |
Validates / verifies state |
OTPVerifier |
Approver / Rejecter |
Workflow transitions |
ProposalApprover, PositionRejecter |
Rotator |
Regenerates / rotates value |
ClientSecretRotator |
Checker |
Uniqueness / preconditions |
UserUniquenessChecker |
Resolver |
Read with caching / lookup |
PermissionResolver |
Manager |
Multi-purpose for one aggregate |
UserPinManager |
Invalidator / Requester |
Domain-specific verbs |
OTPInvalidator, OTPRequester |
Async Queue
- Prefer
#[AsyncQueueMessage] on ListenerInterface::process() (Pattern A)
- Use
Job class only when no triggering event (Pattern B)
- Tune
maxAttempts: 1 non-idempotent, 3 typical, 5 flaky external
HTTP Client
- Never instantiate Guzzle directly; declare upstreams in
http_client.php — auto-registered by listener
- Each upstream:
HttpClient/{Upstream}Client/ + Factory/{Upstream}HttpClientFactory.php + dependencies.php binding
- Credentials in
config/autoload/{upstream}.php (env-backed); Factory reads via ConfigInterface::get(), never env()
- Token caching with thundering-herd lock:
expire expires_in - 60s
Authentication
- Every public Action MUST declare
#[Auth(guards: ...)]
oauth2 for user tokens, client for service-to-service
- Stateless providers when service does NOT own user/client aggregate
- Per-endpoint scope via
options: ['scope' => '...'] in routing
- Inject principal with
#[AuthUser(for: 'user'|'client')]
Relation Trait
Each relation lives in its own trait under src/Relation/{Related}/:
BelongsTo{Related}Trait — singular: {related}()
HasOne{Related}Trait — singular: {related}()
HasMany{Related}sTrait — plural: {related}s()
BelongsToMany{Related}sTrait — plural: {related}s()
MorphTo — inline on polymorphic owner model (not a trait)
Finder
Read-only lookup layer between Service and Repository. Always readonly class, always findOrFail (never nullable).
- Naming:
{Entity}Finder
- Multiple lookup methods:
findOrFail($id), findBy{Field}OrFail($value)
- Throws
ModelNotFoundException → handled by exception pipeline
Consumed Event (Inbound Kafka)
Service consumes external Kafka events via #[ConsumedEvent]. Use abstract base classes for event families:
- Abstract base: shared constructor fields for a family of events
- Concrete event: adds
#[ConsumedEvent(stream: '...', name: '...')] annotation
- Listener:
#[Listener], resolves services via $this->container->get(), maps payload
ConsumedEventResolver utility reflects #[ConsumedEvent] metadata at runtime
Error Handling
Three-layer error system:
ErrorInterface — domain exceptions implement getError(): string|BackedEnum + getHint(): ?string
ErrorResource — renders RFC-compliant JSON: { error, error_description, code, hint, fails, debug }
AppExceptionHandler — catch-all handler wraps any Throwable in ErrorResource
- Exception handler chain (priority):
RespondTraceIdHandler → OAuth2ServerExceptionHandler → AppExceptionHandler
Custom Auth Guard
When the service needs a custom guard (e.g., external IdP like Authentik):
- Implement
GuardInterface with token validation logic
- Create a Factory class (
{Guard}Factory) registered in dependencies.php
- Config in
config/autoload/{guard_name}.php (env-backed)
- Factory reads config via
ConfigInterface::get(), never env()
Testing
- Framework: Pest 2.x on PHPUnit 10
- Base class:
Tests\TestCase with MakesHttpRequests, RunTestsInCoroutine, InteractsWithDatabase
- Database: SQLite in-memory, auto-migration at
test/bootstrap.php
- Directories:
test/Feature/ (HTTP/integration), test/Unit/
- Conventions:
test('description', function () { ... }), #[Group('name')] attributes
Specs (Optional)
For complex features, write an implementation spec in specs/ before coding:
{date}-{feature-slug}.md — Overview, Consumed/Produced Events, Payload, File Changes, Out of Scope
Config Templates
See references/config-templates.md for full templates:
databases.php, orm.php, cache.php
async_queue.php (AMQP/RabbitMQ)
event_stream.php (Kafka)
http_client.php (menumbing/http-client)
opentracing.php (menumbing/tracer)
auth.php, oauth2_resource_server.php
middlewares.php
health_check.php (menumbing/health-check)
exceptions.php (handler chain)
authentik.php (custom guard config)
signature.php (outbound request signing)
crontab.php (scheduled commands)
serializer.php (Symfony serializer)
cors.php (gokure/hyperf-cors)
server.php (Swoole server tuning)
See references/rules.md for the full rules and composer.json requirements.
Key rules:
- Always
declare(strict_types=1);
- UUID v7 via
Str::orderedUuid()->toString() for primary keys
- PHP 8.4 enums for status fields
readonly class for Actions and DTOs; final class for Services
- All API endpoints prefixed with
/v1
- Cross-service communication only via Kafka events or API calls
- MANDATORY
menumbing/graceful-process in every service
- MANDATORY
menumbing/health-check in every service for K8s probes
- Domain exceptions implement
ErrorInterface; use ErrorResource for JSON responses
- Use Finder for read-only lookups; never return nullable from lookup methods
1---2name: hyperf-backend3description: Generate Hyperf microservice backend code with DDD, Clean Architecture, menumbing packages, and Kafka event streaming. Use when creating services, Models, Repositories, Actions, or DTOs.4---56# Hyperf Backend Skill78Generic skill for generating Hyperf-based microservice backend code aligned with DDD + Clean Architecture, using `menumbing/*` packages and Kafka event streaming.910## Placeholder Convention1112When applying this skill, replace:1314| Placeholder | Format | Example |15|-------------|--------|---------|16| `{ProjectName}` | PascalCase — used in PHP namespaces | `AcmeCorp`, `MyPlatform`, `SampleApp` |17| `{project-name}` | kebab-case — used in DB names, Kafka group, Docker names | `acmecorp`, `myplatform`, `sampleapp` |18| `{project_name}` | snake_case — used in DB schema, prefix, env vars | `acmecorp`, `myplatform` |19| `{Domain}` | PascalCase — bounded context name | `Identity`, `Vacancy`, `Payment` |20| `{service}` | kebab-case — service identifier | `catalog`, `auth`, `payment` |21| `{Name}` / `{Entity}` | PascalCase — entity / aggregate name | `Program`, `User` |22| `{Action}` | PascalCase verb — use case action | `Create`, `Update`, `Verify`, `Approve`, `Reject`, `Cancel`, `Search`, `Rotate`, `Submit` |23| `{Suffix}` | PascalCase role for Service classes | `Creator`, `Updater`, `Verifier`, `Rotator`, `Checker`, `Manager`, `Resolver`, `Approver` |2425## Project Init2627Create new service using:2829```bash30composer create-project hyperf/hyperf-skeleton:dev-master31```3233Namespace convention: `{ProjectName}\{Domain}\` (e.g., `{ProjectName}\Identity`, `{ProjectName}\Vacancy`, `{ProjectName}\Common`)3435## Folder Structure3637```38config/39├── autoload/40│ ├── auth.php41│ ├── async_queue.php42│ ├── cache.php43│ ├── databases.php44│ ├── dependencies.php45│ ├── event_stream.php46│ ├── exceptions.php47│ ├── graceful_process.php48│ ├── http_client.php49│ ├── middlewares.php50│ ├── oauth2_resource_server.php51│ ├── opentracing.php52│ ├── orm.php53│ ├── redis.php54│ ├── resource.php55│ ├── {upstream}.php # One per outbound upstream (credentials/identifiers, env-backed)56│ └── ...57├── config.php58├── container.php59└── routes.php60migrations/61seeders/62src/63├── Action/ # Use Case Handlers (Application Layer) with routing annotations64├── Cache/ # Cache key generator classes65├── Command/ # CLI commands (sync, scheduler, batch)66├── Constant/ # Enums and constants67├── Controller/ # Rare, for complex auth flows68├── Console/ # Simple CLI command wrappers (thin over HyperfCommand)69├── DTO/ # Data Transfer Objects (per module)70├── Event/ # Domain Events: ProducedEvent (outbound) and ConsumedEvent (inbound) annotations71├── Exception/ # Custom exceptions + Handler/72├── Factory/ # DI factories (e.g., HttpClient construction)73├── Finder/ # Read-only model lookups (findOrFail pattern)74├── HttpClient/ # Outbound HTTP clients per upstream (one folder per service)75├── Job/ # Async queue jobs (when extending Hyperf\AsyncQueue\Job)76├── Listener/ # Event listeners (also async queue handlers via #[AsyncQueueMessage])77├── Middleware/ # HTTP middlewares (server-side and HttpClient/ middlewares)78├── Model/ # Eloquent-like models (Menumbing ORM)79├── Query/ # Query objects (rare)80├── Relation/ # Relation traits for models81├── Repository/ # Repository classes82├── Request/ # Form validation requests83├── Resource/ # API response resources84└── Service/ # Domain services (business logic)85```8687---8889## Key Patterns — Quick Reference9091For full code templates, see [references/patterns.md](references/patterns.md).9293| Pattern | Naming | Location |94|---------|--------|----------|95| Action (Use Case Handler) | `{Action}{Name}Action` — invoked via `__invoke()` | `src/Action/` |96| Model | `{Name}` — extends `Menumbing\Orm\Model`, UUID v7 primary key | `src/Model/` |97| Repository | `{Name}Repository` — annotated with `#[AsRepository]` | `src/Repository/` |98| Domain Service | `{Name}{Suffix}` — `final class`, one method, `#[Transactional]` | `src/Service/{Name}/` |99| DTO | `{Action}{Name}DTO` — `readonly` properties, one per use case | `src/DTO/` |100| Domain Event (Produced) | `{Entity}{Action}` — `#[ProducedEvent]` annotation, Kafka stream | `src/Event/` |101| Consumed Event | `{Action}{Entity}` — `#[ConsumedEvent]` annotation, inbound Kafka | `src/Event/{Domain}/` |102| Finder | `{Entity}Finder` — `readonly class`, `findOrFail()` lookups | `src/Finder/` |103| Migration | UUID primary key, `datetimes()`, `softDeletes()` | `migrations/` |104| Cache Key | `{Name}Key::generate()` — prefixed with `{project_name}:` | `src/Cache/` |105106### Suffixes (Domain Service roles)107108| Suffix | Role | Example |109|--------|------|---------|110| `Creator` | Creates a new aggregate | `UserCreator` |111| `Updater` | Updates an existing aggregate | `ProgramUpdater` |112| `Verifier` | Validates / verifies state | `OTPVerifier` |113| `Approver` / `Rejecter` | Workflow transitions | `ProposalApprover`, `PositionRejecter` |114| `Rotator` | Regenerates / rotates value | `ClientSecretRotator` |115| `Checker` | Uniqueness / preconditions | `UserUniquenessChecker` |116| `Resolver` | Read with caching / lookup | `PermissionResolver` |117| `Manager` | Multi-purpose for one aggregate | `UserPinManager` |118| `Invalidator` / `Requester` | Domain-specific verbs | `OTPInvalidator`, `OTPRequester` |119120### Async Queue121122- Prefer `#[AsyncQueueMessage]` on `ListenerInterface::process()` (Pattern A)123- Use `Job` class only when no triggering event (Pattern B)124- Tune `maxAttempts`: `1` non-idempotent, `3` typical, `5` flaky external125126### HTTP Client127128- Never instantiate Guzzle directly; declare upstreams in `http_client.php` — auto-registered by listener129- Each upstream: `HttpClient/{Upstream}Client/` + `Factory/{Upstream}HttpClientFactory.php` + `dependencies.php` binding130- Credentials in `config/autoload/{upstream}.php` (env-backed); Factory reads via `ConfigInterface::get()`, never `env()`131- Token caching with thundering-herd lock: `expire expires_in - 60s`132133### Authentication134135- Every public Action MUST declare `#[Auth(guards: ...)]`136- `oauth2` for user tokens, `client` for service-to-service137- Stateless providers when service does NOT own user/client aggregate138- Per-endpoint scope via `options: ['scope' => '...']` in routing139- Inject principal with `#[AuthUser(for: 'user'|'client')]`140141### Relation Trait142143Each relation lives in its own trait under `src/Relation/{Related}/`:144- `BelongsTo{Related}Trait` — singular: `{related}()`145- `HasOne{Related}Trait` — singular: `{related}()`146- `HasMany{Related}sTrait` — plural: `{related}s()`147- `BelongsToMany{Related}sTrait` — plural: `{related}s()`148- `MorphTo` — inline on polymorphic owner model (not a trait)149150### Finder151152Read-only lookup layer between Service and Repository. Always `readonly class`, always `findOrFail` (never nullable).153- Naming: `{Entity}Finder`154- Multiple lookup methods: `findOrFail($id)`, `findBy{Field}OrFail($value)`155- Throws `ModelNotFoundException` → handled by exception pipeline156157### Consumed Event (Inbound Kafka)158159Service consumes external Kafka events via `#[ConsumedEvent]`. Use abstract base classes for event families:160- Abstract base: shared constructor fields for a family of events161- Concrete event: adds `#[ConsumedEvent(stream: '...', name: '...')]` annotation162- Listener: `#[Listener]`, resolves services via `$this->container->get()`, maps payload163- `ConsumedEventResolver` utility reflects `#[ConsumedEvent]` metadata at runtime164165### Error Handling166167Three-layer error system:168- `ErrorInterface` — domain exceptions implement `getError(): string|BackedEnum` + `getHint(): ?string`169- `ErrorResource` — renders RFC-compliant JSON: `{ error, error_description, code, hint, fails, debug }`170- `AppExceptionHandler` — catch-all handler wraps any `Throwable` in `ErrorResource`171- Exception handler chain (priority): `RespondTraceIdHandler → OAuth2ServerExceptionHandler → AppExceptionHandler`172173### Custom Auth Guard174175When the service needs a custom guard (e.g., external IdP like Authentik):176- Implement `GuardInterface` with token validation logic177- Create a Factory class (`{Guard}Factory`) registered in `dependencies.php`178- Config in `config/autoload/{guard_name}.php` (env-backed)179- Factory reads config via `ConfigInterface::get()`, never `env()`180181### Testing182183- **Framework**: Pest 2.x on PHPUnit 10184- **Base class**: `Tests\TestCase` with `MakesHttpRequests`, `RunTestsInCoroutine`, `InteractsWithDatabase`185- **Database**: SQLite in-memory, auto-migration at `test/bootstrap.php`186- **Directories**: `test/Feature/` (HTTP/integration), `test/Unit/`187- **Conventions**: `test('description', function () { ... })`, `#[Group('name')]` attributes188189### Specs (Optional)190191For complex features, write an implementation spec in `specs/` before coding:192- `{date}-{feature-slug}.md` — Overview, Consumed/Produced Events, Payload, File Changes, Out of Scope193194195---196197## Config Templates198199See [references/config-templates.md](references/config-templates.md) for full templates:200- `databases.php`, `orm.php`, `cache.php`201- `async_queue.php` (AMQP/RabbitMQ)202- `event_stream.php` (Kafka)203- `http_client.php` (menumbing/http-client)204- `opentracing.php` (menumbing/tracer)205- `auth.php`, `oauth2_resource_server.php`206- `middlewares.php`207- `health_check.php` (menumbing/health-check)208- `exceptions.php` (handler chain)209- `authentik.php` (custom guard config)210- `signature.php` (outbound request signing)211- `crontab.php` (scheduled commands)212- `serializer.php` (Symfony serializer)213- `cors.php` (gokure/hyperf-cors)214- `server.php` (Swoole server tuning)215216---217See [references/rules.md](references/rules.md) for the full rules and `composer.json` requirements.218219Key rules:220- Always `declare(strict_types=1);`221- UUID v7 via `Str::orderedUuid()->toString()` for primary keys222- PHP 8.4 enums for status fields223- `readonly class` for Actions and DTOs; `final class` for Services224- All API endpoints prefixed with `/v1`225- Cross-service communication only via Kafka events or API calls226- **MANDATORY** `menumbing/graceful-process` in every service227- **MANDATORY** `menumbing/health-check` in every service for K8s probes228- Domain exceptions implement `ErrorInterface`; use `ErrorResource` for JSON responses229- Use Finder for read-only lookups; never return nullable from lookup methods