From qe-framework
Build and configure Laravel 10+ applications: Eloquent models, Sanctum auth, Horizon queues, RESTful APIs with API resources, Livewire components, and Pest/PHPUnit tests.
How this skill is triggered — by the user, by Claude, or both
Slash command
/qe-framework:Qlaravel-specialistThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Senior Laravel specialist — Laravel 10+, Eloquent ORM, PHP 8.2+.
Senior Laravel specialist — Laravel 10+, Eloquent ORM, PHP 8.2+.
php artisan migrate:statusphp artisan route:listphp artisan test before any step is complete (>85% coverage)| Topic | Reference | Load When |
|---|---|---|
| Eloquent ORM | references/eloquent.md | Models, relationships, scopes, query optimization |
| Routing & APIs | references/routing.md | Routes, controllers, middleware, API resources |
| Queue System | references/queues.md | Jobs, workers, Horizon, failed jobs, batching |
| Livewire | references/livewire.md | Components, wire:model, actions, real-time |
| Testing | references/testing.md | Feature tests, factories, mocking, Pest PHP |
MUST DO:
MUST NOT DO:
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\{Factories\HasFactory, Model, SoftDeletes};
use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany};
final class Post extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = ['title', 'body', 'status', 'user_id'];
protected $casts = ['status' => PostStatus::class, 'published_at' => 'immutable_datetime'];
public function author(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); }
public function comments(): HasMany { return $this->hasMany(Comment::class); }
public function scopePublished(Builder $query): Builder { return $query->where('status', PostStatus::Published); }
}
<?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('posts', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->text('body');
$table->string('status')->default('draft');
$table->timestamp('published_at')->nullable();
$table->softDeletes();
$table->timestamps();
});
}
public function down(): void { Schema::dropIfExists('posts'); }
};
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\{Request, Resources\Json\JsonResource};
final class PostResource extends JsonResource {
public function toArray(Request $request): array {
return [
'id' => $this->id, 'title' => $this->title, 'body' => $this->body,
'status' => $this->status->value,
'published_at' => $this->published_at?->toIso8601String(),
'author' => new UserResource($this->whenLoaded('author')),
'comments' => CommentResource::collection($this->whenLoaded('comments')),
];
}
}
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\Post;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\{InteractsWithQueue, SerializesModels};
final class PublishPost implements ShouldQueue {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = 60;
public function __construct(private readonly Post $post) {}
public function handle(): void {
$this->post->update(['status' => PostStatus::Published, 'published_at' => now()]);
}
public function failed(\Throwable $e): void {
logger()->error('PublishPost failed', ['post' => $this->post->id, 'error' => $e->getMessage()]);
}
}
<?php
use App\Models\{Post, User};
it('returns a published post for authenticated users', function (): void {
$user = User::factory()->create();
$post = Post::factory()->published()->for($user, 'author')->create();
$this->actingAs($user)->getJson("/api/posts/{$post->id}")
->assertOk()->assertJsonPath('data.status', 'published');
});
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Requests\StorePostRequest;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\JsonResponse;
final class PostController extends Controller {
/**
* Store a newly created post.
* @param StorePostRequest $request Validated request containing post data
* @return JsonResponse The created post resource
*/
public function store(StorePostRequest $request): JsonResponse {
$post = Post::create($request->validated());
return response()->json(new PostResource($post), 201);
}
}
// FormRequest with validation rules and authorization
final class StorePostRequest extends FormRequest {
/** @return bool User authorization check */
public function authorize(): bool { return auth()->check(); }
/** @return array Validation rules */
public function rules(): array {
return ['title' => 'required|string|max:255', 'body' => 'required|string'];
}
}
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Http\JsonResponse;
final class InvalidPostException extends Exception {
public static function unpublishable(int $postId): self {
return new self("Post {$postId} cannot be published");
}
public function render(): JsonResponse {
return response()->json(['error' => $this->message], 422);
}
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\{Builder, Relations\BelongsTo};
use App\Events\PostPublished;
final class Post extends Model {
protected $dispatchesEvents = ['updated' => PostPublished::class];
/** Scope: filter published posts with author eagerly loaded */
public function scopePublishedWithAuthor(Builder $query): Builder {
return $query->where('status', 'published')->with('author');
}
public function author(): BelongsTo { return $this->belongsTo(User::class); }
}
// Listener fires after post is updated
final class PostPublished {
public function __construct(public Post $post) {}
}
/**
* Brief description (imperative, one line).
*
* Detailed explanation if needed. Reference related classes/methods.
*
* @param string $title Post title, max 255 chars
* @param int $userId Foreign key to users table
* @return array Status with 'published_at' timestamp
* @throws InvalidPostException If post cannot transition to published state
* @see PostPublished Event triggered on publish
*/
public function publish(string $title, int $userId): array { }
PHPCodeSniffer (phpcs/phpcbf) — PSR-12 code style PHPStan — Static analysis, strict types, null safety Config files:
// phpcs.xml: PSR-12 + no unused imports
<rule ref="PSR12"/>
<rule ref="PSR1.Files.SideEffects.FoundWithSymbols">
<exclude-pattern>*/config/*</exclude-pattern>
</rule>
// phpstan.neon: Level 9, strict mode
includes:
- phpstan-strict-rules.neon
parameters:
level: 9
checkMissingIterableValueType: true
{{ $var }}. Use {!! $html !!} only for trusted HTML; sanitize with Purify.@csrf in all POST/PUT/DELETE forms; middleware checks X-CSRF-TOKEN header.$fillable or $guarded on models. Use request()->validate() before Model::create().mimetypes:image/jpeg), store in /storage/, never in web root.auth()->check() in controllers/middleware; Sanctum for APIs with token rotation.1. Fat Controllers
// WRONG: Business logic in controller
public function store(Request $request) {
$validated = $request->validate([...]);
$post = Post::create($validated);
// Payment, notifications, cache invalidation HERE
}
// CORRECT: Delegate to service
public function store(StorePostRequest $request, PostService $service) {
return response()->json($service->create($request->validated()), 201);
}
2. N+1 Query Problem
// WRONG: Loop loads author for each post
foreach(Post::all() as $post) { echo $post->author->name; }
// CORRECT: Eager load author
foreach(Post::with('author')->get() as $post) { echo $post->author->name; }
3. Raw Queries
// WRONG: SQL injection risk
$posts = DB::select("SELECT * FROM posts WHERE user_id = {$userId}");
// CORRECT: Use parameterized query or Eloquent
$posts = Post::where('user_id', $userId)->get();
4. No Input Validation
// WRONG: Unvalidated input
public function store(Request $request) { Post::create($request->all()); }
// CORRECT: Validate before store
public function store(StorePostRequest $request) { Post::create($request->validated()); }
5. Business Logic in Routes
// WRONG: Logic in route closure
Route::post('/publish', fn() => Post::find(request('id'))->update(['status' => 'published']));
// CORRECT: Use controller method
Route::post('/posts/{post}/publish', [PostController::class, 'publish']);
| Stage | Command | Expected |
|---|---|---|
| After migration | php artisan migrate:status | All Ran |
| After routing | php artisan route:list --path=api | Routes with correct verbs |
| After job dispatch | php artisan queue:work --once | No exception |
| After implementation | php artisan test --coverage | >85%, 0 failures |
| Before PR | ./vendor/bin/pint --test | PSR-12 passes |
npx claudepluginhub inho-team/qe-framework --plugin qe-frameworkCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.