php-expert

Expert guidance for modern PHP development including PHP 8+ features, Laravel framework, Composer dependency management, and PHP best practices.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "php-expert" with this command: npx skills add personamanagmentlayer/pcl/personamanagmentlayer-pcl-php-expert

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

// 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', 'alice@example.com', 30);

Named Arguments

<?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: 'alice@example.com', age: 30, admin: true, );

// Skip optional parameters $user = createUser( name: 'Bob', email: 'bob@example.com', );

Union Types and Mixed

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

// 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

// 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

// 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

// 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

// 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', 'user@example.com', 'Alice'); $user->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

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

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

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

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

// Basic queries $users = User::all(); $user = User::find(1); $user = User::where('email', 'user@example.com')->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

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

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

// 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

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

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

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 declare(strict_types=1);

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

Dependency Injection

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

python-expert

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

devops-expert

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

code-review-expert

No summary provided by upstream source.

Repository SourceNeeds Review