home / skills / personamanagmentlayer / pcl / php-expert

This skill delivers expert PHP development guidance for PHP 8+ and Laravel to improve code quality, architecture, and performance.

npx playbooks add skill personamanagmentlayer/pcl --skill php-expert

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
21.1 KB
---
name: php-expert
version: 1.0.0
description: Expert-level PHP development with PHP 8+, Laravel, Composer, and modern best practices
category: languages
tags: [php, laravel, composer, symfony, phpunit, psr]
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash(php:*, composer:*, artisan:*)
---

# 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
<?php

// Before PHP 8
class User {
    private string $name;
    private string $email;
    private int $age;

    public function __construct(string $name, string $email, int $age) {
        $this->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', '[email protected]', 30);
```

### Named Arguments
```php
<?php

function createUser(
    string $name,
    string $email,
    int $age = 18,
    bool $admin = false,
): User {
    return new User($name, $email, $age, $admin);
}

// Named arguments (PHP 8+)
$user = createUser(
    name: 'Alice',
    email: '[email protected]',
    age: 30,
    admin: true,
);

// Skip optional parameters
$user = createUser(
    name: 'Bob',
    email: '[email protected]',
);
```

### Union Types and Mixed
```php
<?php

// Union types (PHP 8+)
function processValue(int|float $number): int|float {
    return $number * 2;
}

function findUser(int|string $identifier): ?User {
    if (is_int($identifier)) {
        return User::find($identifier);
    }
    return User::where('email', $identifier)->first();
}

// Mixed type (accepts any type)
function debugValue(mixed $value): void {
    var_dump($value);
}
```

### Match Expression
```php
<?php

// Old switch
switch ($status) {
    case 'pending':
        $message = 'Order is pending';
        break;
    case 'processing':
        $message = 'Order is being processed';
        break;
    case 'completed':
        $message = 'Order completed';
        break;
    default:
        $message = 'Unknown status';
}

// Match expression (PHP 8+)
$message = match ($status) {
    'pending' => '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
<?php

// Before PHP 8
$country = null;
if ($user !== null) {
    $address = $user->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
<?php

// Define attribute
#[Attribute]
class Route {
    public function __construct(
        public string $path,
        public string $method = 'GET',
    ) {}
}

// Use attribute
class UserController {
    #[Route('/users', method: 'GET')]
    public function index(): array {
        return User::all();
    }

    #[Route('/users/{id}', method: 'GET')]
    public function show(int $id): User {
        return User::findOrFail($id);
    }

    #[Route('/users', method: 'POST')]
    public function store(Request $request): User {
        return User::create($request->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
<?php

// Basic enum
enum Status {
    case Pending;
    case Processing;
    case Completed;
    case Cancelled;
}

// Usage
$status = Status::Pending;

function updateOrder(Order $order, Status $status): void {
    $order->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
<?php

// Readonly property (PHP 8.1+)
class User {
    public function __construct(
        public readonly string $id,
        public readonly string $email,
        public string $name,
    ) {}
}

$user = new User('123', '[email protected]', 'Alice');
$user->name = 'Bob'; // OK
// $user->email = '[email protected]'; // Error: readonly property

// Readonly class (PHP 8.2+)
readonly class Point {
    public function __construct(
        public int $x,
        public int $y,
    ) {}
}
```

## Laravel Framework

### Models
```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => '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
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\StorePostRequest;
use App\Http\Requests\UpdatePostRequest;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class PostController extends Controller
{
    public function __construct()
    {
        $this->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
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StorePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->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
<?php

namespace App\Http\Resources;

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

class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $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
<?php

// Basic queries
$users = User::all();
$user = User::find(1);
$user = User::where('email', '[email protected]')->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
<?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) {
            $table->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
<?php

namespace App\Jobs;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

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

    public function __construct(
        public User $user,
    ) {}

    public function handle(): void
    {
        Mail::to($this->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
<?php

// Event
namespace App\Events;

use App\Models\Post;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class PostPublished
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public Post $post,
    ) {}
}

// Listener
namespace App\Listeners;

use App\Events\PostPublished;
use App\Notifications\NewPostNotification;

class NotifyFollowers
{
    public function handle(PostPublished $event): void
    {
        $followers = $event->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
<?php

namespace Tests\Feature;

use App\Models\User;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostControllerTest extends TestCase
{
    use RefreshDatabase;

    public function test_can_list_posts(): void
    {
        Post::factory()->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
<?php

namespace Tests\Unit;

use App\Models\User;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class UserTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_has_posts(): void
    {
        $user = User::factory()->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
<?php

namespace Database\Factories;

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

class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'user_id' => 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
<?php
declare(strict_types=1);

// Always use strict types
// Use type declarations for parameters and return types
// Use property types where possible
```

### Dependency Injection
```php
<?php

// Use constructor injection
class UserService
{
    public function __construct(
        private UserRepository $repository,
        private EventDispatcher $dispatcher,
    ) {}

    public function createUser(array $data): User
    {
        $user = $this->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/

Overview

This skill provides expert-level guidance for modern PHP development focused on PHP 8+ features, Laravel, Composer, and current best practices. It helps teams and developers write safer, more maintainable PHP using modern syntax, type safety, and framework patterns. The guidance is practical and example-driven to accelerate implementation in real projects.

How this skill works

I inspect code patterns, propose modern replacements (constructor property promotion, match, nullsafe, union types, attributes, enums, readonly), and map them to Laravel idioms such as Eloquent models, controllers, requests, resources, migrations, jobs, and queues. I recommend type-safe signatures, strict types, PSR-4 autoloading, Composer dependency strategies, and code organization to reduce bugs and improve developer experience. I provide concrete refactors, code snippets, and workflow suggestions tailored to PHP 8+ and Laravel versions.

When to use it

  • Upgrading a codebase to PHP 8+ or modern Laravel versions
  • Designing or refactoring Eloquent models, controllers, or API resources
  • Enforcing type safety, static analysis, and PSR standards
  • Optimizing database queries, eager loading, and avoiding N+1 problems
  • Implementing robust background jobs, queues, and transactional flows
  • Defining clean migrations, schema indexes, and data integrity rules

Best practices

  • Enable strict_types and use explicit return and property types everywhere possible
  • Prefer constructor property promotion and readonly properties for immutable value objects
  • Use attributes for metadata instead of docblock annotations where supported
  • Design Eloquent models with casts, fillable/hidden lists, and small single-responsibility methods
  • Use Form Requests for validation and authorization to keep controllers thin
  • Eager load relationships, paginate large queries, and process large datasets with chunking/transactions

Example use cases

  • Refactor legacy methods to use union types and match expressions for clearer control flow
  • Create API resources and controllers that return consistent JSON using Laravel Resource classes
  • Migrate validation and authorization into FormRequest classes and policies
  • Optimize database access by converting lazy loading into targeted eager loading and conditional selects
  • Implement background email sending or heavy processing via queued Jobs with failure handling

FAQ

Which PHP 8+ features deliver the biggest maintenance wins?

Union types, readonly properties, constructor property promotion, attributes, and match expressions reduce boilerplate and encode intent in types and signatures, improving readability and static analysis.

How should I approach upgrading a Laravel app to modern patterns?

Upgrade PHP first, add strict_types, run static analysis (phpstan/psalm), then incrementally refactor controllers to use FormRequests and Resources, migrate callbacks into jobs, and adopt attributes and typed models.