Table Of Contents
- What Are PSR Standards?
- PSR-4: Autoloading Standard
- PSR-12: Extended Coding Style
- PSR-3: Logger Interface
- PSR-7: HTTP Message Interface
- PSR-11: Container Interface
- PSR-15: HTTP Server Request Handlers
- PSR-18: HTTP Client
- PSR Compliance Tools
- Best Practices for PSR Compliance
- Integration with Modern PHP Frameworks
- Conclusion
PSR standards have transformed PHP development from a fragmented ecosystem into a unified community with shared conventions. After 10 years of PHP development, I've seen firsthand how adopting PSR standards makes code more maintainable, interoperable, and professional.
When I started working with larger development teams in San Francisco, I quickly realized that PSR standards weren't just suggestions – they were essential for collaborative development. Teams that follow PSR standards can easily integrate third-party libraries, onboard new developers faster, and maintain consistent code quality across projects.
What Are PSR Standards?
PSR (PHP Standards Recommendations) are standards developed by the PHP-FIG (PHP Framework Interop Group) to improve interoperability between PHP projects. They cover everything from coding style to interfaces for common functionality.
PSR-4: Autoloading Standard
PSR-4 defines how to autoload classes from file paths, making it the foundation of modern PHP dependency management.
// PSR-4 compliant directory structure
// src/
// ├── Controller/
// │ ├── UserController.php
// │ └── ProductController.php
// ├── Model/
// │ ├── User.php
// │ └── Product.php
// └── Service/
// ├── UserService.php
// └── ProductService.php
// composer.json
{
"autoload": {
"psr-4": {
"MyApp\\": "src/"
}
}
}
// src/Controller/UserController.php
<?php
namespace MyApp\Controller;
use MyApp\Model\User;
use MyApp\Service\UserService;
class UserController
{
private UserService $userService;
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
public function show(int $id): User
{
return $this->userService->findById($id);
}
}
// src/Model/User.php
<?php
namespace MyApp\Model;
class User
{
private int $id;
private string $name;
private string $email;
public function __construct(int $id, string $name, string $email)
{
$this->id = $id;
$this->name = $name;
$this->email = $email;
}
public function getId(): int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function getEmail(): string
{
return $this->email;
}
}
PSR-12: Extended Coding Style
PSR-12 extends PSR-2 with modern PHP features and consistent formatting rules.
<?php
declare(strict_types=1);
namespace MyApp\Service;
use MyApp\Model\User;
use MyApp\Repository\UserRepositoryInterface;
use MyApp\Exception\UserNotFoundException;
/**
* User service handling business logic for user operations.
*/
class UserService
{
private UserRepositoryInterface $userRepository;
public function __construct(UserRepositoryInterface $userRepository)
{
$this->userRepository = $userRepository;
}
/**
* Find user by ID.
*
* @param int $id User ID
* @return User
* @throws UserNotFoundException
*/
public function findById(int $id): User
{
$user = $this->userRepository->findById($id);
if ($user === null) {
throw new UserNotFoundException("User with ID {$id} not found");
}
return $user;
}
/**
* Create a new user.
*
* @param array $data User data
* @return User
*/
public function create(array $data): User
{
$this->validateUserData($data);
$user = new User(
id: $data['id'],
name: $data['name'],
email: $data['email']
);
return $this->userRepository->save($user);
}
/**
* Update user information.
*
* @param int $id User ID
* @param array $data Updated data
* @return User
*/
public function update(int $id, array $data): User
{
$user = $this->findById($id);
// Update logic here
match (true) {
isset($data['name']) => $user->setName($data['name']),
isset($data['email']) => $user->setEmail($data['email']),
default => null
};
return $this->userRepository->save($user);
}
/**
* Validate user data.
*
* @param array $data
* @throws InvalidArgumentException
*/
private function validateUserData(array $data): void
{
$required = ['id', 'name', 'email'];
foreach ($required as $field) {
if (!isset($data[$field]) || empty($data[$field])) {
throw new InvalidArgumentException("Field '{$field}' is required");
}
}
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email format');
}
}
}
PSR-3: Logger Interface
PSR-3 defines a common interface for logging libraries.
<?php
namespace MyApp\Logger;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Psr\Log\LoggerTrait;
/**
* PSR-3 compliant file logger.
*/
class FileLogger implements LoggerInterface
{
use LoggerTrait;
private string $logFile;
private int $maxFileSize;
public function __construct(string $logFile, int $maxFileSize = 10 * 1024 * 1024)
{
$this->logFile = $logFile;
$this->maxFileSize = $maxFileSize;
}
/**
* Log a message at the specified level.
*
* @param mixed $level
* @param string $message
* @param array $context
*/
public function log($level, $message, array $context = []): void
{
$this->rotateLogIfNeeded();
$timestamp = date('Y-m-d H:i:s');
$levelUpper = strtoupper($level);
$contextString = !empty($context) ? json_encode($context) : '';
$logEntry = "[{$timestamp}] {$levelUpper}: {$message} {$contextString}" . PHP_EOL;
file_put_contents($this->logFile, $logEntry, FILE_APPEND | LOCK_EX);
}
/**
* Rotate log file if it exceeds max size.
*/
private function rotateLogIfNeeded(): void
{
if (!file_exists($this->logFile)) {
return;
}
if (filesize($this->logFile) > $this->maxFileSize) {
$rotatedFile = $this->logFile . '.' . date('Y-m-d-H-i-s');
rename($this->logFile, $rotatedFile);
}
}
}
// Usage with PSR-3 interface
class UserService
{
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function createUser(array $data): User
{
$this->logger->info('Creating new user', ['email' => $data['email']]);
try {
$user = $this->performUserCreation($data);
$this->logger->info('User created successfully', ['user_id' => $user->getId()]);
return $user;
} catch (Exception $e) {
$this->logger->error('Failed to create user', [
'error' => $e->getMessage(),
'data' => $data
]);
throw $e;
}
}
private function performUserCreation(array $data): User
{
// User creation logic
return new User($data['id'], $data['name'], $data['email']);
}
}
PSR-7: HTTP Message Interface
PSR-7 defines interfaces for HTTP messages (requests and responses).
<?php
namespace MyApp\Http;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* PSR-7 compliant HTTP response implementation.
*/
class Response implements ResponseInterface
{
private int $statusCode;
private string $reasonPhrase;
private array $headers;
private StreamInterface $body;
private string $protocolVersion;
public function __construct(
int $statusCode = 200,
array $headers = [],
StreamInterface $body = null,
string $protocolVersion = '1.1'
) {
$this->statusCode = $statusCode;
$this->headers = $headers;
$this->body = $body ?? new Stream();
$this->protocolVersion = $protocolVersion;
$this->reasonPhrase = $this->getReasonPhraseFromStatusCode($statusCode);
}
public function getStatusCode(): int
{
return $this->statusCode;
}
public function withStatus($code, $reasonPhrase = ''): ResponseInterface
{
$new = clone $this;
$new->statusCode = $code;
$new->reasonPhrase = $reasonPhrase ?: $this->getReasonPhraseFromStatusCode($code);
return $new;
}
public function getReasonPhrase(): string
{
return $this->reasonPhrase;
}
public function getProtocolVersion(): string
{
return $this->protocolVersion;
}
public function withProtocolVersion($version): ResponseInterface
{
$new = clone $this;
$new->protocolVersion = $version;
return $new;
}
public function getHeaders(): array
{
return $this->headers;
}
public function hasHeader($name): bool
{
return isset($this->headers[strtolower($name)]);
}
public function getHeader($name): array
{
return $this->headers[strtolower($name)] ?? [];
}
public function getHeaderLine($name): string
{
return implode(', ', $this->getHeader($name));
}
public function withHeader($name, $value): ResponseInterface
{
$new = clone $this;
$new->headers[strtolower($name)] = is_array($value) ? $value : [$value];
return $new;
}
public function withAddedHeader($name, $value): ResponseInterface
{
$new = clone $this;
$new->headers[strtolower($name)] = array_merge(
$this->getHeader($name),
is_array($value) ? $value : [$value]
);
return $new;
}
public function withoutHeader($name): ResponseInterface
{
$new = clone $this;
unset($new->headers[strtolower($name)]);
return $new;
}
public function getBody(): StreamInterface
{
return $this->body;
}
public function withBody(StreamInterface $body): ResponseInterface
{
$new = clone $this;
$new->body = $body;
return $new;
}
private function getReasonPhraseFromStatusCode(int $statusCode): string
{
$reasonPhrases = [
200 => 'OK',
201 => 'Created',
204 => 'No Content',
400 => 'Bad Request',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Not Found',
500 => 'Internal Server Error'
];
return $reasonPhrases[$statusCode] ?? 'Unknown';
}
}
PSR-11: Container Interface
PSR-11 defines a common interface for dependency injection containers.
<?php
namespace MyApp\Container;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
/**
* PSR-11 compliant dependency injection container.
*/
class Container implements ContainerInterface
{
private array $services = [];
private array $factories = [];
private array $instances = [];
public function get(string $id)
{
if (!$this->has($id)) {
throw new NotFoundException("Service '{$id}' not found");
}
// Return cached instance if available
if (isset($this->instances[$id])) {
return $this->instances[$id];
}
// Create new instance
if (isset($this->factories[$id])) {
$this->instances[$id] = $this->factories[$id]($this);
} else {
$this->instances[$id] = $this->createInstance($id);
}
return $this->instances[$id];
}
public function has(string $id): bool
{
return isset($this->services[$id]) || isset($this->factories[$id]);
}
/**
* Register a service.
*
* @param string $id Service identifier
* @param string $className Class name
*/
public function set(string $id, string $className): void
{
$this->services[$id] = $className;
}
/**
* Register a factory.
*
* @param string $id Service identifier
* @param callable $factory Factory function
*/
public function setFactory(string $id, callable $factory): void
{
$this->factories[$id] = $factory;
}
/**
* Register a singleton instance.
*
* @param string $id Service identifier
* @param object $instance Instance to register
*/
public function setSingleton(string $id, object $instance): void
{
$this->instances[$id] = $instance;
}
/**
* Create instance with dependency injection.
*
* @param string $id Service identifier
* @return object
*/
private function createInstance(string $id): object
{
$className = $this->services[$id];
if (!class_exists($className)) {
throw new NotFoundException("Class '{$className}' not found");
}
$reflectionClass = new ReflectionClass($className);
$constructor = $reflectionClass->getConstructor();
if (!$constructor) {
return new $className();
}
$dependencies = [];
foreach ($constructor->getParameters() as $parameter) {
$type = $parameter->getType();
if ($type && !$type->isBuiltin()) {
$dependencyClass = $type->getName();
$dependencies[] = $this->get($dependencyClass);
} else {
$dependencies[] = null;
}
}
return $reflectionClass->newInstanceArgs($dependencies);
}
}
class NotFoundException extends Exception implements NotFoundExceptionInterface
{
}
// Usage example
class DatabaseConnection
{
private string $dsn;
public function __construct(string $dsn)
{
$this->dsn = $dsn;
}
}
class UserRepository
{
private DatabaseConnection $connection;
public function __construct(DatabaseConnection $connection)
{
$this->connection = $connection;
}
}
// Container configuration
$container = new Container();
$container->setFactory(DatabaseConnection::class, function() {
return new DatabaseConnection('mysql:host=localhost;dbname=app');
});
$container->set(UserRepository::class, UserRepository::class);
// Usage
$userRepository = $container->get(UserRepository::class);
PSR-15: HTTP Server Request Handlers
PSR-15 defines interfaces for HTTP server request handlers and middleware.
<?php
namespace MyApp\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Authentication middleware implementing PSR-15.
*/
class AuthenticationMiddleware implements MiddlewareInterface
{
private string $secretKey;
public function __construct(string $secretKey)
{
$this->secretKey = $secretKey;
}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
$token = $this->extractTokenFromRequest($request);
if (!$token) {
return $this->createUnauthorizedResponse();
}
if (!$this->validateToken($token)) {
return $this->createUnauthorizedResponse();
}
// Add user information to request
$userId = $this->getUserIdFromToken($token);
$request = $request->withAttribute('user_id', $userId);
return $handler->handle($request);
}
private function extractTokenFromRequest(ServerRequestInterface $request): ?string
{
$authorization = $request->getHeaderLine('Authorization');
if (strpos($authorization, 'Bearer ') === 0) {
return substr($authorization, 7);
}
return null;
}
private function validateToken(string $token): bool
{
// Implement token validation logic
return !empty($token) && strlen($token) > 10;
}
private function getUserIdFromToken(string $token): int
{
// Extract user ID from token
return 1; // Simplified
}
private function createUnauthorizedResponse(): ResponseInterface
{
$response = new Response(401);
$response->getBody()->write('Unauthorized');
return $response;
}
}
/**
* Logging middleware implementing PSR-15.
*/
class LoggingMiddleware implements MiddlewareInterface
{
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
$startTime = microtime(true);
$this->logger->info('Request started', [
'method' => $request->getMethod(),
'uri' => (string) $request->getUri(),
'user_agent' => $request->getHeaderLine('User-Agent')
]);
try {
$response = $handler->handle($request);
$duration = microtime(true) - $startTime;
$this->logger->info('Request completed', [
'status_code' => $response->getStatusCode(),
'duration' => $duration
]);
return $response;
} catch (Exception $e) {
$this->logger->error('Request failed', [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
throw $e;
}
}
}
PSR-18: HTTP Client
PSR-18 defines a common interface for HTTP clients.
<?php
namespace MyApp\Http;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* PSR-18 compliant HTTP client.
*/
class HttpClient implements ClientInterface
{
private array $options;
public function __construct(array $options = [])
{
$this->options = array_merge([
'timeout' => 30,
'connect_timeout' => 10,
'verify_ssl' => true,
'follow_redirects' => true,
'max_redirects' => 5
], $options);
}
public function sendRequest(RequestInterface $request): ResponseInterface
{
$ch = curl_init();
// Set basic options
curl_setopt_array($ch, [
CURLOPT_URL => (string) $request->getUri(),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->options['timeout'],
CURLOPT_CONNECTTIMEOUT => $this->options['connect_timeout'],
CURLOPT_SSL_VERIFYPEER => $this->options['verify_ssl'],
CURLOPT_FOLLOWLOCATION => $this->options['follow_redirects'],
CURLOPT_MAXREDIRS => $this->options['max_redirects'],
CURLOPT_HEADER => true
]);
// Set HTTP method
switch ($request->getMethod()) {
case 'POST':
curl_setopt($ch, CURLOPT_POST, true);
break;
case 'PUT':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
break;
case 'DELETE':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
break;
case 'PATCH':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
break;
}
// Set headers
$headers = [];
foreach ($request->getHeaders() as $name => $values) {
foreach ($values as $value) {
$headers[] = "$name: $value";
}
}
if (!empty($headers)) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
// Set body
$body = $request->getBody();
if ($body->getSize() > 0) {
curl_setopt($ch, CURLOPT_POSTFIELDS, (string) $body);
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
throw new NetworkException("cURL error: $error");
}
curl_close($ch);
// Parse response
$headerData = substr($response, 0, $headerSize);
$bodyData = substr($response, $headerSize);
return $this->createResponse($httpCode, $headerData, $bodyData);
}
private function createResponse(int $statusCode, string $headerData, string $bodyData): ResponseInterface
{
$headers = $this->parseHeaders($headerData);
$body = new Stream($bodyData);
return new Response($statusCode, $headers, $body);
}
private function parseHeaders(string $headerData): array
{
$headers = [];
$lines = explode("\r\n", $headerData);
foreach ($lines as $line) {
if (strpos($line, ':') !== false) {
[$name, $value] = explode(':', $line, 2);
$headers[strtolower(trim($name))][] = trim($value);
}
}
return $headers;
}
}
class NetworkException extends Exception
{
}
PSR Compliance Tools
Tools to help maintain PSR compliance:
<?php
namespace MyApp\Tools;
/**
* PSR compliance checker.
*/
class PSRComplianceChecker
{
public function checkPSR4Compliance(string $directory): array
{
$issues = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory)
);
foreach ($iterator as $file) {
if ($file->getExtension() === 'php') {
$issues = array_merge($issues, $this->checkFileCompliance($file));
}
}
return $issues;
}
private function checkFileCompliance(SplFileInfo $file): array
{
$issues = [];
$content = file_get_contents($file->getPathname());
// Check for PSR-12 compliance
if (!preg_match('/^<\?php\s*\n/', $content)) {
$issues[] = "File {$file->getPathname()}: Missing proper PHP opening tag";
}
if (preg_match('/\?>\s*$/', $content)) {
$issues[] = "File {$file->getPathname()}: Should not end with closing PHP tag";
}
// Check for declare(strict_types=1)
if (!preg_match('/declare\s*\(\s*strict_types\s*=\s*1\s*\)\s*;/', $content)) {
$issues[] = "File {$file->getPathname()}: Missing declare(strict_types=1)";
}
// Check namespace declaration
if (!preg_match('/namespace\s+[A-Za-z0-9\\\\]+\s*;/', $content)) {
$issues[] = "File {$file->getPathname()}: Missing or invalid namespace declaration";
}
return $issues;
}
public function generateComplianceReport(string $directory): string
{
$issues = $this->checkPSR4Compliance($directory);
if (empty($issues)) {
return "✅ All files are PSR compliant!\n";
}
$report = "PSR Compliance Issues Found:\n";
$report .= str_repeat("=", 50) . "\n";
foreach ($issues as $issue) {
$report .= "❌ $issue\n";
}
$report .= "\nTotal issues: " . count($issues) . "\n";
return $report;
}
}
// Usage
$checker = new PSRComplianceChecker();
echo $checker->generateComplianceReport('./src');
Best Practices for PSR Compliance
- Use strict types - Always declare
strict_types=1
- Follow PSR-4 autoloading - Organize files according to namespace structure
- Implement PSR interfaces - Use standard interfaces for interoperability
- Document with DocBlocks - Follow PSR-5 (when finalized) for documentation
- Use PSR-12 coding standards - Maintain consistent code formatting
- Test with compliance tools - Use tools like PHP-CS-Fixer and PHPStan
- Stay updated - Keep track of new PSR standards as they're released
Integration with Modern PHP Frameworks
// Laravel service provider using PSR compliance
class UserServiceProvider extends ServiceProvider
{
public function register(): void
{
// Register PSR-3 logger
$this->app->singleton(LoggerInterface::class, function ($app) {
return new FileLogger(storage_path('logs/app.log'));
});
// Register PSR-11 container
$this->app->bind(ContainerInterface::class, function ($app) {
return $app;
});
// Register PSR-18 HTTP client
$this->app->singleton(ClientInterface::class, function ($app) {
return new HttpClient([
'timeout' => 30,
'verify_ssl' => true
]);
});
}
}
Conclusion
PSR standards are the backbone of modern PHP development. They provide a common language and set of conventions that make PHP code more interoperable, maintainable, and professional.
Throughout my career building applications in San Francisco's tech industry, I've seen how teams that embrace PSR standards are more productive and write better code. The investment in learning and implementing PSR standards pays dividends in code quality, team collaboration, and long-term maintainability.
The key is to start with the fundamentals – PSR-4 autoloading and PSR-12 coding standards – then gradually adopt other PSRs as they become relevant to your projects. Modern PHP frameworks like Laravel, Symfony, and others already implement many PSR standards, making it easier to write compliant code.
Remember, PSR standards aren't just about following rules – they're about being part of a community that values quality, consistency, and interoperability. By writing PSR-compliant code, you're contributing to a healthier PHP ecosystem for everyone.
Add Comment
No comments yet. Be the first to comment!