Scaffold and develop a Laravel 11.x application with PHP 8.3+, Eloquent ORM, queue workers, API resources, Sanctum auth, and Pest testing.
76
71%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./backend-php/laravel-project-starter/SKILL.mdScaffold 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 --initmy-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.jsonapp/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=8DB_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.181fcbc
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.