layered-architecture
Skill35 starsChanged 5 months agoMITView on GitHub
Use when generating controllers, services, repositories, or DTOs in a Symfony app. Enforces layer boundaries: HTTP in controllers, business logic in services, data access in repositories, and DTO mapping between them. Forbids entities in responses and repository access from controllers.
5 other projects use a version of this.
Adopted by
Most recently taken up by
- troskater/flowdeck-claudetheir own version, first seen Oct 25, 2026
- code-dimension/code-dimension-skillstheir own version, first seen Oct 17, 2026
- VeryGoodOpenSource/vgv-ai-flutter-plugintheir own version, first seen Aug 20, 2026
- rrezartprebreza/spring-boot-skillstheir own version, first seen Aug 7, 2026
- dkprog/claude-skillstheir own version, first seen Feb 18, 2026
Other versions5 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 5
- rrezartprebreza/spring-boot-skills★ 344same name
- VeryGoodOpenSource/vgv-ai-flutter-plugin★ 176same name
- code-dimension/code-dimension-skills★ 3same name
- dkprog/claude-skills★ 0same name
- troskater/flowdeck-claude★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository16 other files it gives its agents.
- api-platform-resourcesSkill
- bundle-organizationSkill
- console-commandsSkill
- doctrine-migrationsSkill
- doctrine-ormSkill
- doctrine-query-optimizationSkill
- domain-driven-designSkill
- dto-and-validationSkill
- hexagonal-architectureSkill
- jwt-authenticationSkill
- messenger-asyncSkill
- problem-details-rfc9457Skill
- rest-controller-conventionsSkill
- security-authenticationSkill
- testing-pyramidSkill
- transactional-patternsSkill
The file
We split this skill into the rules it gives an agent
5 rules. 0 are common, 4 are distinctive, and 1 is very specific to this project.
0
Common
Most of the 5 edited versions we read keep these.
4
Distinctive
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 30
Never returns a Doctrine entity directly — map to a response DTO (or use serialization groups).
- Distinctive: line 31
Never injects a repository — always go through a service.
- Distinctive: line 105
No business logic — pure data access.
- Very specific: line 136
Use readonly classes / promoted readonly properties — DTOs are immutable.
- Distinctive: line 162
Keep mapping out of controllers and services — static factory on the DTO, or a dedicated mapper.
“Common” is measured: kept by at least 6 in 10 of the 5 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Layered Architecture (Symfony)
Layer rules
Controller (#[Route]) ← HTTP only. No business logic. No entities in responses.
↓ DTOs
Service (application) ← All business logic. Orchestrates repositories. Owns the transaction.
↓ Entities / domain objects
Repository (Doctrine) ← Data access only. No business logic. Returns entities or read models.
↓ DQL / DBAL
DatabaseDependencies point downward only. A repository never calls a service; a service never sees an Request.
Controller layer
- Handles HTTP: route matching, deserializing the request, returning a
Response. - Calls one service method per endpoint — no orchestration in the controller.
- Never returns a Doctrine entity directly — map to a response DTO (or use serialization groups).
- Never injects a repository — always go through a service.
- No
try/catchfor domain errors — let an exception listener turn them into HTTP responses.
// ✅ GOOD
#[Route('/orders', methods: ['POST'])]
public function create(#[MapRequestPayload] CreateOrderRequest $request): JsonResponse
{
$order = $this->orderService->createOrder($request);
return $this->json(OrderResponse::fromEntity($order), Response::HTTP_CREATED);
}
// ❌ BAD — business logic + direct repository + entity returned
#[Route('/orders', methods: ['POST'])]
public function create(Request $request, OrderRepository $orders): JsonResponse
{
$data = json_decode($request->getContent(), true);
if (empty($data['items'])) {
throw new \RuntimeException('No items'); // domain rule in controller
}
$order = new Order($data);
$orders->save($order); // controller talks to repository
return $this->json($order); // entity leaks out
}Service layer
- Contains all business logic, validation rules beyond the DTO, and orchestration.
- Owns the transaction boundary (
EntityManagerInterface::flush()/wrapInTransaction()). - Constructor injection only — never
ContainerInterface/ service locator pulls. - One service per aggregate (
OrderService, notOrderAndPaymentService). - Returns domain objects or DTOs — never an HTTP
Request/Response.
// ✅ GOOD
final class OrderService
{
public function __construct(
private readonly OrderRepository $orders,
private readonly InventoryService $inventory,
private readonly EntityManagerInterface $em,
) {}
public function createOrder(CreateOrderRequest $request): Order
{
$this->inventory->reserve($request->items);
$order = Order::create($request->customerEmail);
foreach ($request->items as $item) {
$order->addItem($item->productId, $item->quantity);
}
$this->em->persist($order);
$this->em->flush();
return $order;
}
}
// ❌ BAD — service container pull, HTTP type in service
final class OrderService
{
public function __construct(private ContainerInterface $container) {}
public function createOrder(Request $request): Response { /* ... */ }
}Repository layer
- Extends
ServiceEntityRepository<Entity>. - Custom queries via the query builder or DQL — raw SQL only when unavoidable.
- Returns entities or read-model DTOs — never associative arrays leaking column names.
- No business logic — pure data access.
// ✅ GOOD
/** @extends ServiceEntityRepository<Order> */
final class OrderRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Order::class);
}
/** @return Order[] */
public function findActiveByCustomer(string $email): array
{
return $this->createQueryBuilder('o')
->andWhere('o.customerEmail = :email')
->andWhere('o.status = :status')
->setParameter('email', $email)
->setParameter('status', OrderStatus::Active)
->getQuery()
->getResult();
}
}DTOs
- Separate request and response DTOs — never reuse one class for both.
- Validation constraints live on the request DTO only (see
dto-and-validation). - Static factory for mapping:
OrderResponse::fromEntity($order). - Use
readonlyclasses / promoted readonly properties — DTOs are immutable.
// ✅ GOOD
final readonly class OrderResponse
{
/** @param LineItemResponse[] $items */
public function __construct(
public string $id,
public string $status,
public array $items,
) {}
public static function fromEntity(Order $order): self
{
return new self(
$order->getId()->toRfc4122(),
$order->getStatus()->value,
array_map(LineItemResponse::fromEntity(...), $order->getItems()->toArray()),
);
}
}Mapping
- Keep mapping out of controllers and services — static factory on the DTO, or a dedicated mapper.
- Entity → response DTO:
OrderResponse::fromEntity($order). - Request DTO → entity: static factory on the entity (
Order::create(...)) or a mapper service. - Collection mapping:
array_map(OrderResponse::fromEntity(...), $orders)— first-class callable syntax.
Configuration
- App wiring lives in
config/services.yaml; rely on autowiring + autoconfiguration. - Group related settings with
#[Autowire]bound parameters or a typed config object — not scattered%env()%reads inside services. - Infrastructure-only bean definitions (HTTP clients, factories) go in
config/services.yaml, never inside a service class.
Cross-cutting
- Logging: inject
Psr\Log\LoggerInterface— nevererror_log()/dump()in committed code. - Validation:
#[MapRequestPayload]validates the DTO automatically; custom rules via constraint validators. - Exception handling: one
#[AsEventListener(KernelEvents::EXCEPTION)]listener — nevertry/catchin controllers for domain errors.
Gotchas
- Agent injects
ContainerInterface/ uses$this->get()— use constructor injection of the concrete dependency. - Agent returns a Doctrine entity from a controller — map to a response DTO (or apply serialization groups).
- Agent injects a repository into a controller — go through a service.
- Agent puts
flush()and transaction logic in the controller — move it to the service. - Agent builds a god service (
OrderAndInventoryService) — split by aggregate. - Agent decodes JSON manually with
json_decode($request->getContent())— use#[MapRequestPayload]with a DTO. - Agent reuses one DTO for request and response — keep them separate.
- Agent
try/catches domain exceptions in the controller — let the exception listener map them.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

