--- id: personamanagmentlayer/pcl/php-expert version: "50bee515" license: Apache-2.0 install: manual updated: 2026-03-30 --- # php-expert — PHP Expert provides in-depth instruction on PHP 8+ language features including union types, named arguments, match expressions, attributes, and enums. Learn Laravel framework patterns, Composer dependency management, and object-oriented design principles for building robust applications. Publisher: personamanagmentlayer · Stars: 38 · Updated: 2026-03-30 Install (manual): `git clone https://github.com/personamanagmentlayer/pcl` ## SKILL.md # PHP Expert Expert guidance for modern PHP development including PHP 8+ features, Laravel framework, Composer dependency management, and PHP best practices. ## Core Concepts ### PHP 8+ Features - Union types and mixed type - Named arguments - Attributes (annotations) - Constructor property promotion - Match expressions - Nullsafe operator - JIT compiler - Fibers (PHP 8.1+) - Readonly properties and classes ### Object-Oriented PHP - Classes and objects - Interfaces and abstract classes - Traits - Namespaces - Autoloading (PSR-4) - Type declarations - Visibility modifiers ### Modern PHP - Strict types - Return type declarations - Property type declarations - Enums (PHP 8.1+) - First-class callable syntax ## Modern PHP 8+ Syntax ### Constructor Property Promotion ```php name = $name; $this->email = $email; $this->age = $age; } } // PHP 8+ (constructor property promotion) class User { public function __construct( private string $name, private string $email, private int $age, ) {} public function getName(): string { return $this->name; } } $user = new User('Alice', 'alice@example.com', 30); ``` ### Named Arguments ```php first(); } // Mixed type (accepts any type) function debugValue(mixed $value): void { var_dump($value); } ``` ### Match Expression ```php 'Order is pending', 'processing' => 'Order is being processed', 'completed' => 'Order completed', default => 'Unknown status', }; // Multiple conditions $result = match ($value) { 0, 1, 2 => 'Small', 3, 4, 5 => 'Medium', default => 'Large', }; // With expressions $discount = match (true) { $customer->isPremium() && $order->total() > 1000 => 0.20, $customer->isPremium() => 0.15, $order->total() > 500 => 0.10, default => 0, }; ``` ### Nullsafe Operator ```php getAddress(); if ($address !== null) { $country = $address->getCountry(); } } // PHP 8+ nullsafe operator $country = $user?->getAddress()?->getCountry(); // With default $country = $user?->getAddress()?->getCountry() ?? 'Unknown'; ``` ### Attributes (Annotations) ```php all()); } } // Read attributes $reflection = new ReflectionClass(UserController::class); foreach ($reflection->getMethods() as $method) { $attributes = $method->getAttributes(Route::class); foreach ($attributes as $attribute) { $route = $attribute->newInstance(); echo "{$route->method} {$route->path}\n"; } } ``` ### Enums (PHP 8.1+) ```php status = $status; $order->save(); } // Backed enums enum OrderStatus: string { case Pending = 'pending'; case Processing = 'processing'; case Completed = 'completed'; case Cancelled = 'cancelled'; public function label(): string { return match($this) { self::Pending => 'Pending', self::Processing => 'Being Processed', self::Completed => 'Completed', self::Cancelled => 'Cancelled', }; } public function color(): string { return match($this) { self::Pending => 'yellow', self::Processing => 'blue', self::Completed => 'green', self::Cancelled => 'red', }; } } $status = OrderStatus::from('pending'); echo $status->label(); // 'Pending' echo $status->value; // 'pending' ``` ### Readonly Properties and Classes ```php name = 'Bob'; // OK // $user->email = 'new@example.com'; // Error: readonly property // Readonly class (PHP 8.2+) readonly class Point { public function __construct( public int $x, public int $y, ) {} } ``` ## Laravel Framework ### Models ```php 'datetime', 'is_admin' => 'boolean', 'settings' => 'array', ]; // Relationships public function posts(): HasMany { return $this->hasMany(Post::class); } public function roles(): BelongsToMany { return $this->belongsToMany(Role::class); } // Accessors (Laravel 9+) protected function name(): Attribute { return Attribute::make( get: fn (string $value) => ucfirst($value), set: fn (string $value) => strtolower($value), ); } // Scopes public function scopeActive($query) { return $query->where('active', true); } public function scopeAdmins($query) { return $query->where('is_admin', true); } // Methods public function isAdmin(): bool { return $this->is_admin; } } // Usage $users = User::active()->get(); $admins = User::admins()->get(); $user = User::with('posts', 'roles')->find(1); ``` ### Controllers ```php middleware('auth:api')->except(['index', 'show']); } public function index(): AnonymousResourceCollection { $posts = Post::with('user') ->published() ->latest() ->paginate(20); return PostResource::collection($posts); } public function show(Post $post): PostResource { $post->load(['user', 'comments.user']); return new PostResource($post); } public function store(StorePostRequest $request): JsonResponse { $post = $request->user()->posts()->create( $request->validated() ); return response()->json( new PostResource($post), 201 ); } public function update(UpdatePostRequest $request, Post $post): PostResource { $this->authorize('update', $post); $post->update($request->validated()); return new PostResource($post); } public function destroy(Post $post): JsonResponse { $this->authorize('delete', $post); $post->delete(); return response()->json(null, 204); } } ``` ### Form Requests ```php user() !== null; } public function rules(): array { return [ 'title' => ['required', 'string', 'max:200'], 'content' => ['required', 'string', 'min:100'], 'published' => ['sometimes', 'boolean'], 'tags' => ['sometimes', 'array'], 'tags.*' => ['string', 'max:50'], ]; } public function messages(): array { return [ 'title.required' => 'Please provide a title for your post', 'content.min' => 'Post content must be at least 100 characters', ]; } } ``` ### API Resources ```php $this->id, 'title' => $this->title, 'slug' => $this->slug, 'content' => $this->when( $request->routeIs('posts.show'), $this->content ), 'excerpt' => $this->excerpt, 'published' => $this->published, 'published_at' => $this->published_at?->toIso8601String(), 'author' => new UserResource($this->whenLoaded('user')), 'comments' => CommentResource::collection( $this->whenLoaded('comments') ), 'tags' => $this->tags, 'created_at' => $this->created_at->toIso8601String(), 'updated_at' => $this->updated_at->toIso8601String(), ]; } } ``` ### Eloquent Queries ```php first(); // Complex queries $posts = Post::where('published', true) ->where('created_at', '>', now()->subWeek()) ->orderBy('created_at', 'desc') ->limit(10) ->get(); // Eager loading (avoid N+1) $posts = Post::with(['user', 'comments.user'])->get(); // Lazy eager loading $posts = Post::all(); $posts->load('user'); // Conditional loading $posts = Post::with([ 'user' => function ($query) { $query->select('id', 'name', 'email'); }, 'comments' => function ($query) { $query->latest()->limit(5); }, ])->get(); // Aggregates $count = Post::where('published', true)->count(); $sum = Order::where('status', 'completed')->sum('total'); $avg = Product::avg('price'); // Chunk processing Post::chunk(100, function ($posts) { foreach ($posts as $post) { // Process post } }); // Transactions DB::transaction(function () use ($data) { $user = User::create($data['user']); $profile = $user->profile()->create($data['profile']); $user->roles()->attach($data['roles']); }); ``` ### Migrations ```php id(); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->string('title'); $table->string('slug')->unique(); $table->text('content'); $table->text('excerpt')->nullable(); $table->boolean('published')->default(false); $table->timestamp('published_at')->nullable(); $table->json('tags')->nullable(); $table->timestamps(); $table->softDeletes(); $table->index('published'); $table->index('published_at'); $table->index(['user_id', 'published']); }); } public function down(): void { Schema::dropIfExists('posts'); } }; ``` ### Jobs (Queues) ```php user->email) ->send(new WelcomeEmail($this->user)); } public function failed(\Throwable $exception): void { // Handle job failure Log::error('Failed to send welcome email', [ 'user_id' => $this->user->id, 'error' => $exception->getMessage(), ]); } } // Dispatch job SendWelcomeEmail::dispatch($user); SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(10)); SendWelcomeEmail::dispatch($user)->onQueue('emails'); ``` ### Events and Listeners ```php post->user->followers; foreach ($followers as $follower) { $follower->notify(new NewPostNotification($event->post)); } } } // Register in EventServiceProvider protected $listen = [ PostPublished::class => [ NotifyFollowers::class, ], ]; // Dispatch event PostPublished::dispatch($post); ``` ## Testing with PHPUnit ### Feature Tests ```php count(3)->create(['published' => true]); Post::factory()->create(['published' => false]); $response = $this->getJson('/api/posts'); $response->assertOk() ->assertJsonCount(3, 'data'); } public function test_can_create_post_when_authenticated(): void { $user = User::factory()->create(); $response = $this->actingAs($user, 'api') ->postJson('/api/posts', [ 'title' => 'Test Post', 'content' => 'Test content with enough characters to pass validation.', ]); $response->assertCreated() ->assertJsonPath('data.title', 'Test Post'); $this->assertDatabaseHas('posts', [ 'title' => 'Test Post', 'user_id' => $user->id, ]); } public function test_cannot_create_post_when_not_authenticated(): void { $response = $this->postJson('/api/posts', [ 'title' => 'Test Post', 'content' => 'Test content', ]); $response->assertUnauthorized(); } public function test_validates_post_creation(): void { $user = User::factory()->create(); $response = $this->actingAs($user, 'api') ->postJson('/api/posts', [ 'title' => '', // Invalid 'content' => 'Short', // Too short ]); $response->assertUnprocessable() ->assertJsonValidationErrors(['title', 'content']); } public function test_can_update_own_post(): void { $user = User::factory()->create(); $post = Post::factory()->create(['user_id' => $user->id]); $response = $this->actingAs($user, 'api') ->putJson("/api/posts/{$post->id}", [ 'title' => 'Updated Title', 'content' => 'Updated content with enough characters.', ]); $response->assertOk(); $this->assertDatabaseHas('posts', [ 'id' => $post->id, 'title' => 'Updated Title', ]); } public function test_cannot_update_other_user_post(): void { $user = User::factory()->create(); $otherUser = User::factory()->create(); $post = Post::factory()->create(['user_id' => $otherUser->id]); $response = $this->actingAs($user, 'api') ->putJson("/api/posts/{$post->id}", [ 'title' => 'Updated Title', ]); $response->assertForbidden(); } } ``` ### Unit Tests ```php create(); $posts = Post::factory()->count(3)->create(['user_id' => $user->id]); $this->assertCount(3, $user->posts); $this->assertTrue($user->posts->contains($posts->first())); } public function test_is_admin_returns_true_for_admin_users(): void { $admin = User::factory()->create(['is_admin' => true]); $user = User::factory()->create(['is_admin' => false]); $this->assertTrue($admin->isAdmin()); $this->assertFalse($user->isAdmin()); } } ``` ### Factories ```php User::factory(), 'title' => fake()->sentence(), 'slug' => fake()->slug(), 'content' => fake()->paragraphs(5, true), 'excerpt' => fake()->paragraph(), 'published' => false, 'published_at' => null, 'tags' => fake()->words(3), ]; } public function published(): static { return $this->state(fn (array $attributes) => [ 'published' => true, 'published_at' => now(), ]); } public function withUser(User $user): static { return $this->state(fn (array $attributes) => [ 'user_id' => $user->id, ]); } } ``` ## Best Practices ### Type Safety ```php repository->create($data); $this->dispatcher->dispatch(new UserCreated($user)); return $user; } } ``` ### PSR Standards - PSR-1: Basic Coding Standard - PSR-4: Autoloading Standard - PSR-12: Extended Coding Style - PSR-7: HTTP Message Interface ## Anti-Patterns to Avoid ❌ **Not using strict types**: Always declare(strict_types=1) ❌ **Fat controllers**: Extract logic to services ❌ **N+1 queries**: Use eager loading ❌ **No type declarations**: Use types everywhere ❌ **Ignoring PSR standards**: Follow PSR-4, PSR-12 ❌ **Direct DB queries in controllers**: Use repositories ❌ **Missing validation**: Always validate input ❌ **No tests**: Write tests for critical code ## Resources - PHP Documentation: https://www.php.net/docs.php - Laravel Documentation: https://laravel.com/docs - Composer: https://getcomposer.org/ - PSR Standards: https://www.php-fig.org/psr/ - PHPUnit: https://phpunit.de/ [View on SkillFed](https://skillfed.io/personamanagmentlayer/pcl/php-expert) · [View on GitHub](https://github.com/personamanagmentlayer/pcl)