| name | php-development |
| description | Modern PHP 8.x+ best practices, patterns, and standards for building robust applications. Use when the task involves `PHP project`, `composer.json`, `PHP development`, `Laravel`, or `Symfony`. |
| license | MIT |
| metadata | {"version":"1.0.0"} |
PHP Development
Production patterns for modern PHP (8.x+), covering Composer, PSR standards, type safety,
testing with PHPUnit, and architectural best practices.
When to Use This Skill
- Building or structuring a PHP application
- Working with Composer for dependency management
- Following PSR standards (autoloading, coding style)
- Using PHP 8.x features (attributes, enums, fibers, typed properties)
- Writing tests with PHPUnit
- Applying clean architecture in PHP projects
Core Concepts
1. Project Layout (PSR-4)
myapp/
├── src/
│ ├── Controller/
│ │ └── UserController.php
│ ├── Service/
│ │ └── UserService.php
│ ├── Repository/
│ │ └── UserRepository.php
│ ├── Entity/
│ │ └── User.php
│ ├── Exception/
│ │ ├── AppException.php
│ │ └── NotFoundException.php
│ └── ValueObject/
│ └── Email.php
├── tests/
│ ├── Unit/
│ │ └── Service/
│ │ └── UserServiceTest.php
│ └── Integration/
│ └── Repository/
│ └── UserRepositoryTest.php
├── config/
├── public/
│ └── index.php
├── composer.json
├── phpunit.xml
└── README.md
2. Key Standards
| Standard | Purpose |
|---|
| PSR-4 | Autoloading: namespace ↔ directory map |
| PSR-12 | Extended coding style (superseded by PER-CS) |
| PSR-7 | HTTP message interfaces |
| PSR-11 | Container interface (DI) |
| PSR-3 | Logger interface |
Quick Start
composer init
composer require guzzlehttp/guzzle
composer require --dev phpunit/phpunit phpstan/phpstan
composer dump-autoload
./vendor/bin/phpunit
./vendor/bin/phpstan analyse src --level=max
./vendor/bin/php-cs-fixer fix src
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
}
}
Patterns
Pattern 1: Type-Safe PHP 8.x
<?php
declare(strict_types=1);
namespace App\Entity;
enum UserRole: string
{
case Admin = 'admin';
case Editor = 'editor';
case Viewer = 'viewer';
public function canEdit(): bool
{
return match ($this) {
self::Admin, self::Editor => true,
self::Viewer => false,
};
}
}
readonly class User
{
public function __construct(
public string $id,
public string $name,
public Email $email,
public UserRole $role = UserRole::,
\DateTimeImmutable = \DateTimeImmutable(),
) {}
}
{
{
(!(, FILTER_VALIDATE_EMAIL)) {
();
}
}
{
->value;
}
}
Pattern 2: Error Handling
<?php
declare(strict_types=1);
namespace App\Exception;
class AppException extends \RuntimeException
{
public function __construct(
string $message,
public readonly array $context = [],
int $code = 0,
?\Throwable $previous = null,
) {
parent::__construct($message, $code, $previous);
}
}
class NotFoundException extends AppException
{
public static function forEntity(string $entity, string $id): self
{
return new self(
message: "{$entity} with ID {$id} not found",
context: [ => , => ],
code: ,
);
}
}
{
{
::(
: ,
: [ => ],
: ,
);
}
}
{
{}
{
{
= ->repository->();
} (\PDOException ) {
->logger->(, [ => , => ->()]);
(, previous: );
}
( === ) {
::(, );
}
;
}
}
Pattern 3: Interfaces and Dependency Injection
<?php
declare(strict_types=1);
namespace App\Repository;
interface UserRepositoryInterface
{
public function findById(string $id): ?User;
public function save(User $user): void;
public function delete(string $id): void;
public function findByRole(UserRole $role): array;
}
class PostgresUserRepository implements UserRepositoryInterface
{
public function __construct(
private readonly \PDO $connection,
) {}
public {
= ->connection->();
->([ => ]);
= ->(\PDO::);
? ->() : ;
}
{
= ->connection->(
);
->([
=> ->id,
=> ->name,
=> () ->email,
=> ->role->value,
]);
}
}
Pattern 4: Attributes (PHP 8.0+)
<?php
declare(strict_types=1);
namespace App\Attribute;
#[\Attribute(\Attribute::TARGET_METHOD)]
class Route
{
public function __construct(
public readonly string $path,
public readonly string $method = 'GET',
) {}
}
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Validate
{
public function __construct(
public readonly string $rule,
public readonly string $message = 'Validation failed',
) {}
}
class UserController
{
#[Route('/users/{id}', method: 'GET')]
{
}
(, : )
{
}
}
{
= [];
= ();
(->() ) {
= ->(::);
( ) {
= ->();
[] = [
=> ->path,
=> ->method,
=> [, ->()],
];
}
}
;
}
Pattern 5: Testing with PHPUnit
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Service;
use App\Entity\User;
use App\Entity\Email;
use App\Entity\UserRole;
use App\Exception\NotFoundException;
use App\Repository\UserRepositoryInterface;
use App\Service\UserService;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
class UserServiceTest extends TestCase
{
private UserRepositoryInterface $repository;
private UserService $service;
protected function setUp(): void
{
$this->repository = $this->(::);
->service = (->repository, ());
}
{
= (
id: ,
name: ,
email: (),
);
->repository
->()
->()
->();
= ->service->();
->(, );
}
{
->repository
->()
->()
->();
->(::);
->();
->service->();
}
{
->(::);
();
}
{
[
=> [],
=> [],
=> [],
=> [],
];
}
}
Best Practices
Do's
- Use
declare(strict_types=1) — In every PHP file for type safety
- Use readonly classes/properties — For value objects and DTOs
- Use enums over class constants — Type-safe, self-documenting
- Use constructor promotion — Reduces boilerplate
- Use named arguments — For readability:
new User(name: 'Alice', role: UserRole::Admin)
- Use
match over switch — Strict comparison, expression-based
- Run PHPStan at max level — Catch type errors before runtime
Don'ts
- Don't use
@ error suppression — Hides bugs
- Don't use
global variables — Use dependency injection
- Don't use
extract() — Creates variables from unknown keys
- Don't catch
\Exception without rethrowing — Swallows critical errors
- Don't use dynamic properties (deprecated in 8.2) — Declare all properties
- Don't use
mixed type when a specific type is known — Be precise
Resources