CtrlK
BlogDocsLog inGet started
Tessl Logo

laravel-project-starter

Scaffold and develop a Laravel 11.x application with PHP 8.3+, Eloquent ORM, queue workers, API resources, Sanctum auth, and Pest testing.

76

Quality

71%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./backend-php/laravel-project-starter/SKILL.md
SKILL.md
Quality
Evals
Security

Laravel Project Starter

Scaffold and develop a Laravel 11.x application with PHP 8.3+, Eloquent ORM, queue workers, API resources, Sanctum auth, and Pest testing.

Prerequisites

  • PHP 8.3+
  • Composer 2.x
  • Node.js 20+ (for Vite asset pipeline)
  • PostgreSQL or MySQL
  • Redis (recommended for queues and cache)

Scaffold Command

composer create-project laravel/laravel my-project
cd my-project
composer require laravel/sanctum
php artisan install:api
composer require --dev pestphp/pest pestphp/pest-plugin-laravel
vendor/bin/pest --init

Project Structure

my-project/
├── app/
│   ├── Http/
│   │   ├── Controllers/        # HTTP controllers
│   │   ├── Middleware/          # Custom middleware
│   │   └── Requests/           # Form Request validation classes
│   ├── Models/                 # Eloquent models
│   ├── Jobs/                   # Queueable jobs
│   ├── Policies/               # Authorization policies
│   ├── Providers/              # Service providers
│   └── Services/               # Business logic services
├── bootstrap/
│   └── app.php                 # Application bootstrap + middleware registration
├── config/                     # Configuration files
├── database/
│   ├── factories/              # Model factories for testing/seeding
│   ├── migrations/             # Database migrations
│   └── seeders/                # Database seeders
├── resources/
│   ├── views/                  # Blade templates
│   └── js/                     # Frontend JS (Vite)
├── routes/
│   ├── api.php                 # API routes
│   ├── web.php                 # Web routes
│   └── console.php             # Artisan console commands
├── storage/                    # Logs, cache, file uploads
├── tests/
│   ├── Feature/                # Feature/integration tests
│   └── Unit/                   # Unit tests
├── .env                        # Environment variables
├── .env.example                # Auto-generated by Laravel — commit this as the template for required env vars
├── artisan                     # CLI entry point
└── composer.json

Key Conventions

  • Models live in app/Models/. One model per database table.
  • Controllers are thin: validate via Form Requests, delegate to services, return responses.
  • Form Requests (app/Http/Requests/) handle all validation. Never validate inline in controllers.
  • Business logic goes in app/Services/, not in models or controllers.
  • API responses use API Resources (app/Http/Resources/) for consistent JSON shaping.
  • Migrations are timestamped and sequential. Never edit a migration that has been run in production; create a new one.
  • Factories and seeders live in database/. Every model should have a factory.
  • Queue jobs are self-contained units of work in app/Jobs/.
  • Environment config in .env and .env.local. Never commit .env.
  • Laravel 11 uses a streamlined bootstrap/app.php for middleware and route registration (no more app/Http/Kernel.php).

Essential Patterns

Eloquent Model

<?php
// app/Models/Product.php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Product extends Model
{
    use HasFactory;

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

    protected function casts(): array
    {
        return [
            'price' => 'decimal:2',
        ];
    }

    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    public function orderItems(): HasMany
    {
        return $this->hasMany(OrderItem::class);
    }

    public function scopeExpensive($query, float $threshold = 100.00)
    {
        return $query->where('price', '>=', $threshold);
    }
}

Migration

<?php
// database/migrations/2024_01_01_000001_create_products_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->decimal('price', 10, 2);
            $table->foreignId('category_id')->constrained()->cascadeOnDelete();
            $table->timestamps();

            $table->index('price');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};

Factory + Seeder

<?php
// database/factories/ProductFactory.php

namespace Database\Factories;

use App\Models\Category;
use Illuminate\Database\Eloquent\Factories\Factory;

class ProductFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name' => fake()->words(3, true),
            'description' => fake()->paragraph(),
            'price' => fake()->randomFloat(2, 5, 500),
            'category_id' => Category::factory(),
        ];
    }

    public function expensive(): static
    {
        return $this->state(fn () => [
            'price' => fake()->randomFloat(2, 500, 5000),
        ]);
    }
}
<?php
// database/seeders/ProductSeeder.php

namespace Database\Seeders;

use App\Models\Product;
use Illuminate\Database\Seeder;

class ProductSeeder extends Seeder
{
    public function run(): void
    {
        Product::factory()->count(50)->create();
        Product::factory()->expensive()->count(10)->create();
    }
}

Form Request Validation

<?php
// app/Http/Requests/StoreProductRequest.php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreProductRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'description' => ['nullable', 'string'],
            'price' => ['required', 'numeric', 'min:0.01'],
            'category_id' => ['required', 'exists:categories,id'],
        ];
    }
}

API Resource

<?php
// app/Http/Resources/ProductResource.php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

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

Controller (API)

<?php
// app/Http/Controllers/Api/ProductController.php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreProductRequest;
use App\Http\Requests\UpdateProductRequest;
use App\Http\Resources\ProductResource;
use App\Models\Product;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class ProductController extends Controller
{
    public function index(): AnonymousResourceCollection
    {
        $products = Product::with('category')
            ->latest()
            ->paginate(20);

        return ProductResource::collection($products);
    }

    public function store(StoreProductRequest $request): ProductResource
    {
        $product = Product::create($request->validated());

        return new ProductResource($product->load('category'));
    }

    public function show(Product $product): ProductResource
    {
        return new ProductResource($product->load('category'));
    }

    public function update(UpdateProductRequest $request, Product $product): ProductResource
    {
        $product->update($request->validated());

        return new ProductResource($product->load('category'));
    }

    public function destroy(Product $product): \Illuminate\Http\JsonResponse
    {
        $product->delete();

        return response()->json(null, 204);
    }
}

API Routes with Sanctum Auth

<?php
// routes/api.php

use App\Http\Controllers\Api\ProductController;
use Illuminate\Support\Facades\Route;

Route::middleware('auth:sanctum')->group(function () {
    Route::apiResource('products', ProductController::class);

    Route::get('/user', function (\Illuminate\Http\Request $request) {
        return $request->user();
    });
});

Middleware Registration (Laravel 11)

<?php
// bootstrap/app.php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->statefulApi();
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })
    ->create();

Queue Job

<?php
// app/Jobs/ProcessOrderPayment.php

namespace App\Jobs;

use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;

class ProcessOrderPayment implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 60;

    public function __construct(
        public readonly Order $order,
    ) {}

    public function handle(): void
    {
        // Payment processing logic here
        Log::info("Processing payment for order #{$this->order->id}");

        $this->order->update(['status' => 'paid']);
    }

    public function failed(\Throwable $exception): void
    {
        Log::error("Payment failed for order #{$this->order->id}", [
            'error' => $exception->getMessage(),
        ]);
    }
}

Dispatch a job:

ProcessOrderPayment::dispatch($order);

// Delayed dispatch
ProcessOrderPayment::dispatch($order)->delay(now()->addMinutes(5));

Pest Feature Test

<?php
// tests/Feature/ProductApiTest.php

use App\Models\Product;
use App\Models\User;

test('authenticated user can list products', function () {
    $user = User::factory()->create();
    Product::factory()->count(5)->create();

    $response = $this->actingAs($user)
        ->getJson('/api/products');

    $response->assertOk()
        ->assertJsonCount(5, 'data')
        ->assertJsonStructure([
            'data' => [
                '*' => ['id', 'name', 'price', 'created_at'],
            ],
        ]);
});

test('unauthenticated user cannot access products', function () {
    $this->getJson('/api/products')
        ->assertUnauthorized();
});

test('can create a product with valid data', function () {
    $user = User::factory()->create();
    $category = \App\Models\Category::factory()->create();

    $response = $this->actingAs($user)
        ->postJson('/api/products', [
            'name' => 'Test Product',
            'description' => 'A test product',
            'price' => 29.99,
            'category_id' => $category->id,
        ]);

    $response->assertCreated()
        ->assertJsonPath('data.name', 'Test Product');

    $this->assertDatabaseHas('products', ['name' => 'Test Product']);
});

test('validation rejects invalid product data', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->postJson('/api/products', [
            'name' => '',
            'price' => -5,
        ])
        ->assertUnprocessable()
        ->assertJsonValidationErrors(['name', 'price', 'category_id']);
});

First Steps After Scaffold

  1. Copy .env.example to .env (Laravel does this automatically during create-project) and fill in DB_* and REDIS_URL
  2. Generate the app key: php artisan key:generate
  3. Run composer install to ensure all dependencies are installed
  4. Create the database and run migrations: php artisan migrate
  5. Start the dev server: php artisan serve
  6. Verify the health endpoint: curl http://localhost:8000/up

Common Commands

# Start dev server
php artisan serve

# Database
php artisan migrate
php artisan migrate:fresh --seed
php artisan make:model Product -mfsc    # model + migration + seeder + controller
php artisan make:migration add_status_to_orders_table

# Code generation
php artisan make:controller Api/ProductController --api
php artisan make:request StoreProductRequest
php artisan make:resource ProductResource
php artisan make:job ProcessOrderPayment
php artisan make:policy ProductPolicy --model=Product
php artisan make:middleware EnsureIsAdmin

# Queues
php artisan queue:work --tries=3
php artisan queue:listen redis
php artisan queue:failed
php artisan queue:retry all

# Cache
php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan optimize

# Tests
vendor/bin/pest
vendor/bin/pest --filter=ProductApiTest
vendor/bin/pest --parallel
php artisan test

# Linting
composer require --dev laravel/pint
vendor/bin/pint
vendor/bin/pint --test

# Static analysis
composer require --dev larastan/larastan
vendor/bin/phpstan analyse --level=8

Integration Notes

  • Database: Set DB_CONNECTION, DB_HOST, DB_DATABASE, DB_USERNAME, DB_PASSWORD in .env. Supports PostgreSQL, MySQL, SQLite.
  • Queues: Set QUEUE_CONNECTION=redis (or database, sqs). Run php artisan queue:work as a supervised process in production.
  • Sanctum: Provides token-based API auth. Use $user->createToken('api') for token issuance. For SPA auth, use cookie-based sessions via statefulApi() middleware.
  • Mail: Configure MAIL_MAILER in .env. Use php artisan make:mail OrderConfirmation --markdown for templated emails.
  • Docker: Use Laravel Sail (composer require laravel/sail --dev && php artisan sail:install) for a Docker dev environment.
  • Frontend: Vite is the default bundler. Pair with Livewire (composer require livewire/livewire) for reactive server-rendered UI, or Inertia.js for Vue/React SPA.
  • Scheduling: Define scheduled tasks in routes/console.php using Schedule::command(). Run via php artisan schedule:work locally or cron in production.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.