A comprehensive PHP tutorial demonstrating the Strategy Pattern with a "first-match" approach for data transformation operations.
- What is the Strategy Pattern?
- Why is it Powerful?
- When to Use It
- When NOT to Use It
- Project Overview
- Architecture
- Quick Start
- Usage Examples
- Running Tests
- Contributing
The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
β Context βββββΆβ Strategy ββββββ ConcreteStrategyβ
β (Transformer) β β (Interface) β β (JsonTransformerβ
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
β² βββββββββββββββββββ
β β ConcreteStrategyβ
βββββββββββββββββ (XmlTransformer β
βββββββββββββββββββ
βββββββββββββββββββ
β ConcreteStrategyβ
β (CsvTransformer β
βββββββββββββββββββ
Switch between different algorithms at runtime without changing the client code:
$transformer = new TransformerProvider($strategies);
// Same interface, different behaviors
$result = $transformer->transform($data, Type::JSON); // Uses JsonTransformer
$result = $transformer->transform($data, Type::XML); // Uses XmlTransformer
$result = $transformer->transform($data, Type::CSV); // Uses CsvTransformerAdd new transformation formats without modifying existing code:
// Add new strategy without touching existing code
class PdfTransformer implements TransformerInterface {
public function supports(Type $type): bool {
return $type === Type::PDF;
}
public function transform(array $data): string {
// PDF transformation logic
}
}- Single Responsibility: Each strategy handles one transformation format
- Open/Closed: Open for extension, closed for modification
- Dependency Inversion: Depends on abstractions, not concretions
Easy to unit test each strategy independently:
public function testJsonTransformer(): void
{
$transformer = new JsonTransformer();
$result = $transformer->transform(['name' => 'John']);
$this->assertJson($result);
}When you need to support multiple output formats:
// Transform user data in different formats
$userData = ['id' => 1, 'name' => 'John', 'email' => 'john@example.com'];
$transformer->transform($userData, Type::JSON); // For APIs
$transformer->transform($userData, Type::XML); // For legacy systems
$transformer->transform($userData, Type::CSV); // For spreadsheetsDifferent payment gateways with same interface:
$processor = new PaymentProcessor([
new StripePayment(),
new PayPalPayment(),
new SquarePayment()
]);
$processor->process($amount, PaymentType::CREDIT_CARD);Multiple notification channels:
$notifier = new NotificationService([
new EmailNotification(),
new SmsNotification(),
new PushNotification()
]);
$notifier->send($message, NotificationType::EMAIL);Different storage backends:
$storage = new FileStorage([
new LocalStorage(),
new S3Storage(),
new GoogleCloudStorage()
]);
$storage->store($file, StorageType::CLOUD);Don't use Strategy Pattern if you only have one way to do something:
// β Overkill - just use a simple method
class Calculator {
public function add(int $a, int $b): int {
return $a + $b; // Only one way to add numbers
}
}For simple if/else scenarios:
// β Strategy Pattern is overkill here
public function getDiscount(string $customerType): float {
return match($customerType) {
'premium' => 0.20,
'regular' => 0.10,
default => 0.0
};
}When the algorithm is stable and unlikely to change:
// β String length calculation doesn't need strategies
public function getStringLength(string $text): int {
return strlen($text); // This won't change
}When every nanosecond matters:
// β Strategy overhead might be too much for tight loops
for ($i = 0; $i < 1000000; $i++) {
$result = $strategy->calculate($data[$i]); // Overhead adds up
}This project demonstrates a "first-match" Strategy Pattern implementation for data transformation operations. The system automatically selects the first strategy that supports the requested transformation type.
- π― First-match selection algorithm
- π Runtime strategy switching
- π¦ Easy strategy extension
- π§ͺ Comprehensive test coverage
- π Detailed documentation
- π³ Docker development environment
// Strategy Interface
interface TransformerInterface {
public function transform(array $data): string;
public function supports(Type $type): bool;
}
// Context (Strategy Manager)
class TransformerProvider implements TransformerProviderInterface {
public function transform(array $data, Type $type): string {
foreach ($this->strategies as $strategy) {
if ($strategy->supports($type)) {
return $strategy->transform($data); // First match wins!
}
}
throw new InvalidArgumentException("No strategy found for type: {$type->value}");
}
}
// Concrete Strategies
class JsonTransformer implements TransformerInterface { /* ... */ }
class XmlTransformer implements TransformerInterface { /* ... */ }
class CsvTransformer implements TransformerInterface { /* ... */ }1. Client requests transformation with Type::JSON
2. TransformerProvider iterates through strategies
3. First strategy that supports JSON is selected
4. Selected strategy performs the transformation
5. Result is returned to client
- PHP 8.1+
- Docker & Docker Compose
- Make
# Clone the repository
git clone https://github.com/nullodyssey/first-match-strategy-pattern.git
cd first-match-strategy-pattern
# Start the development environment
make start
# Install dependencies
make composer c="install"
# Run tests to verify everything works
make testuse NullOdyssey\FirstMatchStrategy\TransformerProvider;
use NullOdyssey\FirstMatchStrategy\Strategy\{JsonTransformer, XmlTransformer, CsvTransformer};
use NullOdyssey\FirstMatchStrategy\Type;
// Create transformer with strategies
$transformer = new TransformerProvider([
new JsonTransformer(),
new XmlTransformer(),
new CsvTransformer()
]);
// Sample data
$userData = [
'id' => 123,
'name' => 'John Doe',
'email' => 'john@example.com',
'active' => true
];
// Transform in different formats
$jsonResult = $transformer->transform($userData, Type::JSON);
// Output: {"id":123,"name":"John Doe","email":"john@example.com","active":true}
$xmlResult = $transformer->transform($userData, Type::XML);
// Output: <root><id>123</id><name>John Doe</name>...</root>
$csvResult = $transformer->transform($userData, Type::CSV);
// Output: id,name,email,active\n123,"John Doe","john@example.com",1use NullOdyssey\FirstMatchStrategy\DataTransformer;
use NullOdyssey\FirstMatchStrategy\Type;
$transformerService = new DataTransformer();
$data = ['name' => 'Jane Smith', 'role' => 'Developer'];
// Simple one-liner transformations
$transformerService($data, Type::JSON); // Triggers JSON transformation
$transformerService($data, Type::XML); // Triggers XML transformation
$transformerService($data, Type::CSV); // Triggers CSV transformationuse NullOdyssey\FirstMatchStrategy\TransformerInterface;
use NullOdyssey\FirstMatchStrategy\Type;
// Extend the Type enum first
enum Type: string {
case JSON = 'json';
case XML = 'xml';
case CSV = 'csv';
case YAML = 'yaml'; // New type
}
// Create custom strategy
class YamlTransformer implements TransformerInterface {
public function transform(array $data): string {
return yaml_emit($data);
}
public function supports(Type $type): bool {
return $type === Type::YAML;
}
}
// Use with existing transformer
$transformer = new TransformerProvider([
new JsonTransformer(),
new XmlTransformer(),
new CsvTransformer(),
new YamlTransformer() // Add your custom strategy
]);
$result = $transformer->transform($data, Type::YAML);# Run all tests
make test
# Run specific test file
make sh
./vendor/bin/phpunit tests/Unit/TransformerProviderTest.php
# Run with coverage
make sh
./vendor/bin/phpunit --coverage-html coverage// Order transformation in multiple formats for different systems
$orderTransformer = new TransformerProvider([
new JsonTransformer(), // For REST APIs
new XmlTransformer(), // For SOAP services
new CsvTransformer(), // For accounting software
new PdfTransformer() // For customer invoices
]);
$order = ['id' => 'ORD-123', 'total' => 99.99, 'items' => [...]];
$receipt = $orderTransformer->transform($order, Type::PDF);// Report generation in multiple formats
$reportTransformer = new TransformerProvider([
new ExcelTransformer(), // For business users
new CsvTransformer(), // For data analysis
new JsonTransformer(), // For API consumption
new ChartTransformer() // For visualizations
]);
$analyticsData = ['revenue' => 10000, 'users' => 500, 'conversion' => 2.5];
$report = $reportTransformer->transform($analyticsData, Type::EXCEL);# Docker & Environment
make build # Build Docker images
make start # Build and start containers
make down # Stop containers
make sh # Connect to PHP container
# Dependencies
make composer # Run composer commands
make vendor # Install production dependencies
# Code Quality
make test # Run PHPUnit tests
make phpstan # Run static analysis
make phpcs # Run code style fixer
make quality # Run both phpstan and phpcsAfter exploring this project, you'll understand:
- β Strategy Pattern fundamentals
- β When and why to use it
- β First-match selection algorithm
- β SOLID principles in practice
- β PHP 8.1+ features (enums, readonly classes)
- β Test-driven development
- β Design pattern trade-offs
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
nullodyssey
- Website: nullodyssey.dev
- Email: community@nullodyssey.dev
Happy coding! π