AgentStack
SKILL verified MIT Self-run

Laravel Architecture

skill-ahmmedrasel-dev-agent-skills-laravel-architecture · by ahmmedrasel-dev

Guide Laravel 11 REST API projects using Service + Repository + DTO pattern, module-based structure, migration conventions, model relationships, API Resources, and route organization. Use when asked about Laravel project structure, where to put code, how to write migrations, how to design controllers, how to implement a new module, or how to review Laravel code for convention compliance.

No reviews yet
0 installs
2 views
0.0% view→install

Install

$ agentstack add skill-ahmmedrasel-dev-agent-skills-laravel-architecture

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Laravel Architecture? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Laravel Architecture

When to use

Use this skill when working on a Laravel 11 REST API project that follows the Service + Repository + DTO pattern. Most useful when:

  • Starting a new module (e.g. Product, Order, Category)
  • Writing or reviewing migrations
  • Deciding where business logic, DB queries, or validation belongs
  • Reviewing controller, service, or repository code for convention compliance
  • Setting up route structure with auth and role-based access

Input parameters

  • Module name or feature being built (e.g. Product, Order)
  • Whether it is a new module or an existing one being modified
  • Any specific layer in question (migration, model, repository, service, controller, resource)
  • Auth requirements (public, authenticated, admin-only)

Procedure

1. Project Structure

Every feature follows this vertical slice layout:

app/
├── Http/
│   ├── Controllers/Api/V1//
│   │   └── Controller.php
│   ├── Requests//
│   │   ├── StoreRequest.php
│   │   └── UpdateRequest.php
│   └── Resources//
│       ├── Resource.php
│       └── Collection.php
├── Services/
│   └── Service.php
├── Repositories/
│   ├── Interfaces/RepositoryInterface.php
│   └── Repository.php
├── DTOs/
│   └── DTO.php
└── Models/
    └── .php

Register all repository bindings in app/Providers/RepositoryServiceProvider.php.

2. Request Lifecycle

HTTP Request
  → FormRequest        (validate input)
  → Controller         (receive, delegate, respond — no logic)
  → Service            (business logic, orchestration)
  → Repository         (DB queries only)
  → Model              (Eloquent — no logic, only relationships + scopes)
  → API Resource       (format JSON response)
  → HTTP Response

3. Migration Conventions

Naming: YYYY_MM_DD_HHMMSS_verb_noun_table.php

Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->text('description')->nullable();
    $table->unsignedBigInteger('category_id');
    $table->decimal('price', 10, 2);          // money: always decimal(10,2)
    $table->unsignedInteger('stock')->default(0);
    $table->enum('status', ['active', 'inactive', 'draft'])->default('active');
    $table->json('meta')->nullable();
    $table->timestamps();
    $table->softDeletes();

    $table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade');
    $table->index(['status', 'created_at']);
});

Column type rules:

  • Money → decimal(10, 2)
  • Status/type → enum([...]) with default
  • Images/files → string (path only)
  • Flexible data → json
  • Foreign key → unsignedBigInteger + foreign()
  • Flags → boolean()->default(false)
  • Pivot table name → alphabetical order (e.g. category_product)

4. Model

class Product extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = ['name', 'slug', 'description', 'category_id', 'price', 'stock', 'status'];

    protected $casts = [
        'price'  => 'decimal:2',
        'stock'  => 'integer',
        'meta'   => 'array',
    ];

    // Relationships only — no business logic
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    // Query scopes for common filters
    public function scopeActive(Builder $query): Builder
    {
        return $query->where('status', 'active');
    }
}

5. DTO

readonly class ProductDTO
{
    public function __construct(
        public string  $name,
        public string  $slug,
        public ?string $description,
        public int     $categoryId,
        public float   $price,
        public int     $stock,
        public string  $status = 'active',
    ) {}

    public static function fromRequest(array $data): self
    {
        return new self(
            name:        $data['name'],
            slug:        $data['slug'] ?? '',
            description: $data['description'] ?? null,
            categoryId:  $data['category_id'],
            price:       $data['price'],
            stock:       $data['stock'],
            status:      $data['status'] ?? 'active',
        );
    }
}

6. Repository

Interface in app/Repositories/Interfaces/:

interface ProductRepositoryInterface
{
    public function paginate(array $filters, int $perPage = 20): LengthAwarePaginator;
    public function findById(int $id): ?Product;
    public function findBySlug(string $slug): ?Product;
    public function create(ProductDTO $dto): Product;
    public function update(Product $product, ProductDTO $dto): Product;
    public function delete(Product $product): bool;
}

Concrete in app/Repositories/:

class ProductRepository implements ProductRepositoryInterface
{
    public function paginate(array $filters, int $perPage = 20): LengthAwarePaginator
    {
        return Product::query()
            ->with(['category', 'variants'])
            ->when(isset($filters['category_id']), fn($q) =>
                $q->where('category_id', $filters['category_id'])
            )
            ->when(isset($filters['search']), fn($q) =>
                $q->where('name', 'like', "%{$filters['search']}%")
            )
            ->latest()
            ->paginate($perPage);
    }

    public function create(ProductDTO $dto): Product
    {
        return Product::create([
            'name'        => $dto->name,
            'slug'        => $dto->slug,
            'category_id' => $dto->categoryId,
            'price'       => $dto->price,
            'stock'       => $dto->stock,
            'status'      => $dto->status,
        ]);
    }
    // findById, findBySlug, update, delete follow same pattern
}

Bind in RepositoryServiceProvider:

$this->app->bind(ProductRepositoryInterface::class, ProductRepository::class);

7. Service

class ProductService
{
    public function __construct(
        private readonly ProductRepositoryInterface $productRepository,
    ) {}

    public function createProduct(ProductDTO $dto): Product
    {
        // Business logic here — auto-generate slug, validate stock rules, fire events
        $dto = new ProductDTO(...$dto, slug: Str::slug($dto->name));
        return $this->productRepository->create($dto);
    }
}

8. Controller (thin)

class ProductController extends Controller
{
    public function __construct(private readonly ProductService $productService) {}

    public function index(Request $request): ProductCollection
    {
        $products = $this->productService->getPaginatedProducts(
            filters: $request->only(['category_id', 'status', 'search']),
            perPage: $request->integer('per_page', 20),
        );
        return new ProductCollection($products);
    }

    public function store(StoreProductRequest $request): JsonResponse
    {
        $product = $this->productService->createProduct(
            ProductDTO::fromRequest($request->validated())
        );
        return (new ProductResource($product))->response()->setStatusCode(201);
    }
}

9. API Resource

class ProductResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'         => $this->id,
            'name'       => $this->name,
            'slug'       => $this->slug,
            'price'      => (float) $this->price,
            'stock'      => $this->stock,
            'status'     => $this->status,
            'category'   => new CategoryResource($this->whenLoaded('category')),
            'created_at' => $this->created_at->toISOString(),
        ];
    }
}

Standard response shapes:

  • Single: { "data": { ... } }
  • Collection: { "data": [...], "links": {...}, "meta": { "total": 100, ... } }
  • Error: { "message": "...", "errors": { "field": ["..."] } }

10. Route Organization

Route::prefix('v1')->group(function () {
    // Public
    Route::post('/auth/login', [AuthController::class, 'login']);
    Route::apiResource('products', ProductController::class)->only(['index', 'show']);

    // Authenticated
    Route::middleware('auth:sanctum')->group(function () {
        Route::post('/auth/logout', [AuthController::class, 'logout']);
        Route::apiResource('orders', OrderController::class);

        // Admin only
        Route::middleware('role:admin|super_admin')->prefix('admin')->group(function () {
            Route::apiResource('products', Admin\ProductController::class)->except(['index', 'show']);
        });
    });
});

Examples

Prompt 1:

Use laravel-architecture to create a new Category module with CRUD.

Prompt 2:

Review this ProductController — does it follow the laravel-architecture conventions?

Prompt 3:

Write the migration for an orders table using laravel-architecture conventions.
The table needs: user_id, total_amount, delivery_charge, status, paid_at, and delivery address fields.

Smoke test

Ask the agent to scaffold a new Order module. Verify the response includes:

  • Correct folder paths for Controller, Service, Repository, DTO, Resource
  • Migration with proper column types (decimal for money, enum for status)
  • Thin controller delegating to Service
  • Repository with paginate() using ->when() for filters
  • DTO with fromRequest() factory
  • Route group with auth middleware

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.