A collection of modern PHP design patterns implemented using PHP 8.2+ features. Sexier than older implementations and more readable than ever.
- PHP 8.2 or higher
- Composer
composer require cmatosbc/desired-patterns
Quick Links:
- 1. Singleton Pattern
- 2. Multiton Pattern
- 3. Command Pattern
- 4. Chain of Responsibility Pattern
- 5. Registry Pattern
- 6. Service Locator Pattern
- 7. Specification Pattern
- 8. Strategy Pattern
- 9. State Pattern
- 10. Pipeline Pattern
The Singleton pattern ensures a class has only one instance and provides a global point of access to it. Our implementation uses a trait to make it reusable.
use DesiredPatterns\Traits\Singleton;
class Database
{
use Singleton;
private function __construct()
{
// Initialize database connection
}
public function query(string $sql): array
{
// Execute query
}
}
// Usage
$db = Database::getInstance();
The Multiton pattern is similar to Singleton but maintains a map of named instances. This is useful when you need multiple named instances of a class.
use DesiredPatterns\Traits\Multiton;
class Configuration
{
use Multiton;
private string $environment;
private function __construct(string $environment)
{
$this->environment = $environment;
}
public function getEnvironment(): string
{
return $this->environment;
}
}
// Usage
$devConfig = Configuration::getInstance('development');
$prodConfig = Configuration::getInstance('production');
// Check if instance exists
if (Configuration::hasInstance('testing')) {
// ...
}
The Command pattern encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
use DesiredPatterns\Commands\AbstractCommand;
use DesiredPatterns\Commands\AbstractCommandHandler;
// Command
class CreateUserCommand extends AbstractCommand
{
public function __construct(
public readonly string $name,
public readonly string $email
) {}
}
// Handler
class CreateUserHandler extends AbstractCommandHandler
{
public function handle(CreateUserCommand $command): void
{
// Create user logic here
}
public function supports(object $command): bool
{
return $command instanceof CreateUserCommand;
}
}
// Usage
$command = new CreateUserCommand('John Doe', '[email protected]');
$handler = new CreateUserHandler();
$handler->handle($command);
The Chain of Responsibility pattern lets you pass requests along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.
use DesiredPatterns\Chain\AbstractHandler;
// Create concrete handlers
class PayPalHandler extends AbstractHandler
{
public function handle($request)
{
if ($request['type'] === 'paypal') {
return [
'status' => 'success',
'message' => 'Payment processed via PayPal'
];
}
return parent::handle($request);
}
}
class CreditCardHandler extends AbstractHandler
{
public function handle($request)
{
if ($request['type'] === 'credit_card') {
return [
'status' => 'success',
'message' => 'Payment processed via credit card'
];
}
return parent::handle($request);
}
}
// Usage
$paypalHandler = new PayPalHandler();
$creditCardHandler = new CreditCardHandler();
// Build the chain
$paypalHandler->setNext($creditCardHandler);
// Process payment
$result = $paypalHandler->handle([
'type' => 'credit_card',
'amount' => 100.00
]);
The Registry pattern provides a global point of access to objects or services throughout an application.
use DesiredPatterns\Registry\Registry;
// Store a value
Registry::set('config.database', [
'host' => 'localhost',
'name' => 'myapp'
]);
// Retrieve a value
$dbConfig = Registry::get('config.database');
// Check if key exists
if (Registry::has('config.database')) {
// ...
}
// Remove a key
Registry::remove('config.database');
// Get all keys
$keys = Registry::keys();
The Service Locator pattern is a design pattern used to encapsulate the processes involved in obtaining a service with a strong abstraction layer.
use DesiredPatterns\ServiceLocator\ServiceLocator;
class DatabaseService
{
public function connect(): void
{
// Connection logic
}
}
// Create a service locator
$locator = new ServiceLocator();
// Register a service
$locator->register('database', fn() => new DatabaseService());
// Resolve the service
$db = $locator->resolve('database');
// Check if service exists
if ($locator->has('database')) {
// ...
}
// Extend an existing service
$locator->extend('database', function($service) {
// Modify or decorate the service
return $service;
});
The Specification pattern is used to create business rules that can be combined using boolean logic.
use DesiredPatterns\Specifications\AbstractSpecification;
use DesiredPatterns\Specifications\Composite\{AndSpecification, OrSpecification, NotSpecification};
// Create specifications
class AgeSpecification extends AbstractSpecification
{
public function __construct(private int $minAge) {}
public function isSatisfiedBy(mixed $candidate): bool
{
return $candidate->age >= $this->minAge;
}
}
class VerifiedSpecification extends AbstractSpecification
{
public function isSatisfiedBy(mixed $candidate): bool
{
return $candidate->isVerified;
}
}
// Usage
$isAdult = new AgeSpecification(18);
$isVerified = new VerifiedSpecification();
// Combine specifications
$canAccessContent = $isAdult->and($isVerified);
// Check if specifications are met
$user = new stdClass();
$user->age = 25;
$user->isVerified = true;
if ($canAccessContent->isSatisfiedBy($user)) {
// Allow access
}
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.
-
Payment Processing
- Different payment methods (Credit Card, PayPal, Cryptocurrency)
- Each payment method has its own validation and processing logic
- System can switch between payment strategies based on user selection
-
Data Export
- Multiple export formats (CSV, JSON, XML, PDF)
- Each format has specific formatting requirements
- Choose export strategy based on user preference or file type
-
Shipping Calculation
- Various shipping providers (FedEx, UPS, DHL)
- Each provider has unique rate calculation algorithms
- Select provider based on destination, weight, or cost
namespace Examples\Strategy\Payment;
use DesiredPatterns\Strategy\AbstractStrategy;
use DesiredPatterns\Traits\ConfigurableStrategyTrait;
/**
* Credit Card Payment Strategy
*/
class CreditCardStrategy extends AbstractStrategy
{
use ConfigurableStrategyTrait;
protected array $requiredOptions = ['api_key'];
public function supports(array $data): bool
{
return isset($data['payment_method'])
&& $data['payment_method'] === 'credit_card';
}
public function validate(array $data): bool
{
return isset($data['card_number'])
&& isset($data['expiry'])
&& isset($data['cvv']);
}
public function execute(array $data): array
{
// Process credit card payment
return [
'status' => 'success',
'transaction_id' => uniqid('cc_'),
'method' => 'credit_card',
'amount' => $data['amount']
];
}
}
/**
* PayPal Payment Strategy
*/
class PayPalStrategy extends AbstractStrategy
{
use ConfigurableStrategyTrait;
protected array $requiredOptions = ['client_id', 'client_secret'];
public function supports(array $data): bool
{
return isset($data['payment_method'])
&& $data['payment_method'] === 'paypal';
}
public function validate(array $data): bool
{
return isset($data['paypal_email'])
&& isset($data['amount']);
}
public function execute(array $data): array
{
// Process PayPal payment
return [
'status' => 'success',
'transaction_id' => uniqid('pp_'),
'method' => 'paypal',
'amount' => $data['amount']
];
}
}
/**
* Cryptocurrency Payment Strategy
*/
class CryptoStrategy extends AbstractStrategy
{
use ConfigurableStrategyTrait;
protected array $requiredOptions = ['wallet_address'];
public function supports(array $data): bool
{
return isset($data['payment_method'])
&& $data['payment_method'] === 'crypto';
}
public function validate(array $data): bool
{
return isset($data['crypto_address'])
&& isset($data['crypto_currency']);
}
public function execute(array $data): array
{
// Process crypto payment
return [
'status' => 'success',
'transaction_id' => uniqid('crypto_'),
'method' => 'crypto',
'amount' => $data['amount'],
'currency' => $data['crypto_currency']
];
}
}
// Usage Example
$context = new StrategyContext();
// Configure payment strategies
$context->addStrategy(
new CreditCardStrategy(),
['api_key' => 'sk_test_123']
)
->addStrategy(
new PayPalStrategy(),
[
'client_id' => 'client_123',
'client_secret' => 'secret_456'
]
)
->addStrategy(
new CryptoStrategy(),
['wallet_address' => '0x123...']
);
// Process a credit card payment
$ccPayment = $context->executeStrategy([
'payment_method' => 'credit_card',
'amount' => 99.99,
'card_number' => '4242424242424242',
'expiry' => '12/25',
'cvv' => '123'
]);
// Process a PayPal payment
$ppPayment = $context->executeStrategy([
'payment_method' => 'paypal',
'amount' => 149.99,
'paypal_email' => '[email protected]'
]);
// Process a crypto payment
$cryptoPayment = $context->executeStrategy([
'payment_method' => 'crypto',
'amount' => 199.99,
'crypto_address' => '0x456...',
'crypto_currency' => 'ETH'
]);
The State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class. Our implementation provides a flexible and type-safe way to handle state transitions with context validation.
use DesiredPatterns\State\StateMachineTrait;
use DesiredPatterns\State\AbstractState;
// Define your states
class PendingState extends AbstractState
{
public function getName(): string
{
return 'pending';
}
protected array $allowedTransitions = ['processing', 'cancelled'];
protected array $validationRules = [
'order_id' => 'required',
'amount' => 'type:double'
];
public function handle(array $context): array
{
return [
'status' => 'pending',
'message' => 'Order is being validated',
'order_id' => $context['order_id']
];
}
}
// Create your state machine
class Order
{
use StateMachineTrait;
public function __construct(string $orderId)
{
// Initialize states
$this->addState(new PendingState(), true)
->addState(new ProcessingState())
->addState(new ShippedState());
// Set initial context
$this->updateContext([
'order_id' => $orderId,
'created_at' => date('Y-m-d H:i:s')
]);
}
public function process(array $paymentDetails): array
{
$this->transitionTo('processing', $paymentDetails);
return $this->getCurrentState()->handle($this->getContext());
}
}
// Usage
$order = new Order('ORD-123');
try {
$result = $order->process([
'payment_id' => 'PAY-456',
'amount' => 99.99
]);
echo $result['message']; // "Payment verified, preparing shipment"
} catch (StateException $e) {
echo "Error: " . $e->getMessage();
}
The State pattern is perfect for managing complex workflows like order processing. Each state encapsulates its own rules and behaviors:
-
States:
PendingState
: Initial state, validates order detailsProcessingState
: Handles payment verificationShippedState
: Manages shipping detailsDeliveredState
: Handles delivery confirmationCancelledState
: Manages order cancellation
-
Features:
- Context validation per state
- Type-safe state transitions
- State history tracking
- Fluent interface for state machine setup
-
Benefits:
- Clean separation of concerns
- Easy to add new states
- Type-safe state transitions
- Automatic context validation
- Comprehensive state history
-
Use Cases:
- Order Processing Systems
- Document Workflow Management
- Game State Management
- Payment Processing
- Task Management Systems
The Pipeline pattern allows you to process data through a series of operations, where each operation takes input from the previous operation and produces output for the next one. This pattern is particularly useful for data transformation, validation, and processing workflows.
- Fluent interface for operation chaining
- Built-in error handling
- Input validation
- Type-safe operations with PHP 8.2+ generics
- Side effect management
- Conditional processing
- Operation composition
use DesiredPatterns\Pipeline\Pipeline;
// Basic pipeline
$result = Pipeline::of(5)
->pipe(fn($x) => $x * 2) // 10
->pipe(fn($x) => $x + 1) // 11
->get(); // Returns: 11
// Pipeline with error handling
$result = Pipeline::of($value)
->try(
fn($x) => processData($x),
fn(\Throwable $e) => handleError($e)
)
->get();
// Pipeline with validation
$result = Pipeline::of($data)
->when(
fn($x) => $x > 0,
fn($x) => sqrt($x)
)
->get();
The PipelineBuilder provides a more structured way to create complex pipelines with validation and error handling:
use DesiredPatterns\Pipeline\PipelineBuilder;
$builder = new PipelineBuilder();
$result = $builder
->withValidation(fn($x) => $x > 0, 'Value must be positive')
->withValidation(fn($x) => $x < 100, 'Value must be less than 100')
->withErrorHandling(fn(\Throwable $e) => handleValidationError($e))
->add(fn($x) => $x * 2)
->add(fn($x) => "Result: $x")
->build(50)
->get();
Here's a real-world example of using the Pipeline pattern for processing user data:
class UserDataProcessor
{
private PipelineBuilder $pipeline;
public function __construct()
{
$this->pipeline = new PipelineBuilder();
$this->pipeline
->withValidation(
fn($data) => isset($data['email']),
'Email is required'
)
->withValidation(
fn($data) => filter_var($data['email'], FILTER_VALIDATE_EMAIL),
'Invalid email format'
)
->withErrorHandling(fn(\Throwable $e) => [
'success' => false,
'error' => $e->getMessage()
])
->add(function($data) {
// Normalize email
$data['email'] = strtolower($data['email']);
return $data;
})
->add(function($data) {
// Hash password if present
if (isset($data['password'])) {
$data['password'] = password_hash(
$data['password'],
PASSWORD_DEFAULT
);
}
return $data;
})
->add(function($data) {
// Add metadata
$data['created_at'] = new DateTime();
$data['status'] = 'active';
return $data;
});
}
public function process(array $userData): array
{
return $this->pipeline
->build($userData)
->get();
}
}
// Usage
$processor = new UserDataProcessor();
// Successful case
$result = $processor->process([
'email' => '[email protected]',
'password' => 'secret123'
]);
// Returns: [
// 'email' => '[email protected]',
// 'password' => '$2y$10$...',
// 'created_at' => DateTime,
// 'status' => 'active'
// ]
// Error case
$result = $processor->process([
'email' => 'invalid-email'
]);
// Returns: [
// 'success' => false,
// 'error' => 'Invalid email format'
// ]
- Separation of Concerns: Each operation in the pipeline has a single responsibility.
- Maintainability: Easy to add, remove, or modify processing steps without affecting other parts.
- Reusability: Pipeline operations can be reused across different contexts.
- Error Handling: Built-in error handling makes it easy to manage failures.
- Validation: Input validation can be added at any point in the pipeline.
- Type Safety: PHP 8.2+ generics provide type safety throughout the pipeline.
- Testability: Each operation can be tested in isolation.
- Data transformation and normalization
- Form validation and processing
- API request/response handling
- Image processing workflows
- ETL (Extract, Transform, Load) operations
- Document processing pipelines
- Multi-step validation processes
Run the test suite using PHPUnit :
vendor/bin/phpunit
Or run it updating the coverage report:
vendor/bin/phpunit --coverage-html coverage
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
This library is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.