backend-php/laravel-project-starter/SKILL.md
Scaffold and develop a Laravel 11.x application with PHP 8.3+, Eloquent ORM, queue workers, API resources, Sanctum auth, and Pest testing.
npx skillsauth add achreftlili/deep-dev-skills laravel-project-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Scaffold and develop a Laravel 11.x application with PHP 8.3+, Eloquent ORM, queue workers, API resources, Sanctum auth, and Pest testing.
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
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
app/Models/. One model per database table.app/Http/Requests/) handle all validation. Never validate inline in controllers.app/Services/, not in models or controllers.app/Http/Resources/) for consistent JSON shaping.database/. Every model should have a factory.app/Jobs/..env and .env.local. Never commit .env.bootstrap/app.php for middleware and route registration (no more app/Http/Kernel.php).<?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);
}
}
<?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');
}
};
<?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();
}
}
<?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'],
];
}
}
<?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(),
];
}
}
<?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);
}
}
<?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();
});
});
<?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();
<?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));
<?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']);
});
.env.example to .env (Laravel does this automatically during create-project) and fill in DB_* and REDIS_URLphp artisan key:generatecomposer install to ensure all dependencies are installedphp artisan migratephp artisan servecurl http://localhost:8000/up# 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
DB_CONNECTION, DB_HOST, DB_DATABASE, DB_USERNAME, DB_PASSWORD in .env. Supports PostgreSQL, MySQL, SQLite.QUEUE_CONNECTION=redis (or database, sqs). Run php artisan queue:work as a supervised process in production.$user->createToken('api') for token issuance. For SPA auth, use cookie-based sessions via statefulApi() middleware.MAIL_MAILER in .env. Use php artisan make:mail OrderConfirmation --markdown for templated emails.composer require laravel/sail --dev && php artisan sail:install) for a Docker dev environment.composer require livewire/livewire) for reactive server-rendered UI, or Inertia.js for Vue/React SPA.routes/console.php using Schedule::command(). Run via php artisan schedule:work locally or cron in production.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.