Labrodev infrastructure
Skill labrodev/laravel-playbook/skills/labrodev-infrastructure
An opinionated Laravel playbook for AI-assisted development. Architecture conventions, coding standards, and stubs for Cursor, Claude Code, Codex, and Junie — installed via Laravel Boost.
npx -y skills add labrodev/laravel-playbook --skill labrodev-infrastructureAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when integrating any external system in a Labrodev Laravel project — payment gateways, email/SMS/messenger providers, ERP or CRM APIs, webhooks, file storage, third-party SDKs — or when creating/reviewing anything under Core/Infrastructure: contracts, per-vendor adapters, resolvers, and external-data mapping.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
7.2 KB, ~1.5k tokens by cl100k_base, as published. Nobody here has run it
Infrastructure: adapters between the Domain and the outside world
Part of the Labrodev playbook skill set — assumes labrodev-core and labrodev-naming are installed. If absent, minimum global rules: declare(strict_types=1), final classes, App/Layer depends on Core (never the reverse), named-argument invocation.
Core/Infrastructure/{IntegrationName}/ holds everything that talks to the outside world: payment gateways, email/SMS/messenger providers, ERP and CRM integrations, marketplace APIs, webhook handling, file storage, third-party REST/GraphQL clients. Infrastructure is technical, not business — it is the adapter layer between the Domain and external systems.
Musts
- One module per integration:
Core/Infrastructure/Paddle/,Core/Infrastructure/Postmark/,Core/Infrastructure/Slack/— or one per capability with vendor adapters inside (Core/Infrastructure/Messaging/when several vendors serve the same purpose). - Domains reach Infrastructure only through contracts (interfaces) that live in the Infrastructure module's
Contracts/subfolder. The Domain never imports a vendor SDK or a concrete adapter. - Infrastructure contracts are the sanctioned interface use case: the contract, its implementation(s), and the provider wiring are all named explicitly (this satisfies labrodev-core's "no unplanned bindings" rule — plan them, name them, wire them).
- Adapters wrap the vendor SDK/HTTP client and own authentication, request formatting, retries, and rate limits.
- External payloads are mapped at the boundary into internal, domain-friendly structures: plain
final readonlyDTOs with camelCase properties (NOT Spatie Data — that is the user-input boundary → see the labrodev-data skill). - When several vendors implement one capability, add a Resolver that picks the adapter by a domain enum — the caller never switches on vendor names.
- Vendor credentials/config come from
config/services.php(env-backed) and are injected into the adapter — never readenv()outside config files, never hardcode.
Must-nots
- No business rules, no business state decisions, no workflows in Infrastructure. The moment an adapter decides whether something should happen, that logic belongs in a Domain Service or Action.
- Infrastructure must never access
App/Layerand must not depend on specific Domain business rules. - No raw vendor payloads leaking into the Domain: arrays and SDK response objects stop at the adapter; Domains see typed DTOs.
- No inline HTTP calls from Domain code (Actions, Services, Pipeline steps) — always through the contract.
- Slow or unreliable external calls inside request-bound flows: dispatch a queued Job that calls the contract instead of calling synchronously (→ see the labrodev-pipeline skill for step ordering).
The pattern: contract → adapters → resolver
Worked example — one messaging capability, several vendors:
Core/Infrastructure/Messaging/
├── Contracts/
│ └── MessageNotifier.php the capability contract
├── SlackNotifier.php per-vendor adapter
├── TelegramNotifier.php per-vendor adapter
├── MessageNotifierResolver.php picks the adapter by domain enum
└── OutboundMessage.php boundary DTO (final readonly)
Contract
<?php
declare(strict_types=1);
namespace Core\Infrastructure\Messaging\Contracts;
use Core\Infrastructure\Messaging\OutboundMessage;
interface MessageNotifier
{
public function send(OutboundMessage $outboundMessage): void;
}
Boundary DTO
<?php
declare(strict_types=1);
namespace Core\Infrastructure\Messaging;
final readonly class OutboundMessage
{
public function __construct(
public string $recipient,
public string $subject,
public string $body,
) {}
}
Adapter (one per vendor)
<?php
declare(strict_types=1);
namespace Core\Infrastructure\Messaging;
use Core\Infrastructure\Messaging\Contracts\MessageNotifier;
use Illuminate\Support\Facades\Http;
final readonly class SlackNotifier implements MessageNotifier
{
public function __construct(
private string $webhookUrl,
) {}
public function send(OutboundMessage $outboundMessage): void
{
Http::asJson()
->post($this->webhookUrl, [
'text' => sprintf('%s — %s', $outboundMessage->subject, $outboundMessage->body),
])
->throw();
}
}
Resolver (when the vendor is chosen at runtime)
<?php
declare(strict_types=1);
namespace Core\Infrastructure\Messaging;
use Core\Domain\Channel\Enums\ChannelType;
use Core\Infrastructure\Messaging\Contracts\MessageNotifier;
final readonly class MessageNotifierResolver
{
public function __invoke(ChannelType $channelType): MessageNotifier
{
return match ($channelType) {
ChannelType::Slack => resolve(SlackNotifier::class),
ChannelType::Telegram => resolve(TelegramNotifier::class),
};
}
}
Wiring (deliberate, in a provider)
$this->app->when(SlackNotifier::class)
->needs('$webhookUrl')
->giveConfig('services.slack.webhook_url');
Consuming from the Domain
A Pipeline step, Service, or Job injects the contract (single vendor) or the resolver (runtime vendor) — never a concrete adapter:
final readonly class PushBookingToCrm
{
public function __construct(
private CrmClient $crmClient, // Contracts\CrmClient
) {}
}
Placement decision
| Situation | Home |
|---|---|
| Talking to an external system (transport, auth, mapping) | Core/Infrastructure/{Integration} |
| Deciding whether/when to talk to it | Domain Action/Service/Rule |
| Multi-step flow that includes external calls | Pipeline with an Infrastructure-calling step → see the labrodev-pipeline skill |
| Async/retryable external work | Queued Job delegating to the contract |
| Generic technical helper with no external system | Core/Support → see the labrodev-core skill |
Review checklist
- Does the Domain import only the
Contracts/interface (and the resolver), never a vendor SDK or concrete adapter? - Are contract, implementation, and provider wiring all explicitly named (no ad-hoc bindings)?
- Do external payloads stop at the adapter, with the Domain receiving
final readonlyDTOs? - Is the adapter free of business decisions (it executes; the Domain decides)?
- Are credentials injected from
config/services.php— noenv()outside config, nothing hardcoded? - Is vendor selection centralized in a resolver keyed by a domain enum (no vendor
match/ifchains in Domain code)? - Are slow/unreliable calls pushed to queued Jobs rather than blocking request-bound flows?
- Does Infrastructure stay out of
App/Layerand free of Domain business rules?