DEV Community

Helmar
Helmar

Posted on • Edited on

From Chaos to Clean: Implementing Hexagonal Architecture in PHP

Introduction to Hexagonal Architecture

Have you ever inherited a PHP project where changing one small feature breaks three other seemingly unrelated parts? Or struggled to write tests because everything is tightly coupled to the database? Welcome to the world of architectural chaos!

This guide will show you how to escape that nightmare by implementing Hexagonal Architecture in PHP. We'll use a real-world news aggregator project to demonstrate how this pattern can transform your codebase from a tangled mess into a well-organized, testable, and maintainable application.

No theoretical fluff here – just practical, actionable insights that you can apply immediately. Ready to turn your chaotic codebase into clean architecture? Let's begin!

What is Hexagonal Architecture?

Think of Hexagonal Architecture as building a fortress around your business logic. Also known as Ports and Adapters, this pattern creates a protective barrier that shields your core application from the volatile world of external dependencies.

Imagine your application as a medieval castle. The inner keep (your domain logic) contains all the valuable assets - your business rules, entities, and critical operations. The outer walls (infrastructure layer) handle all the messy interactions with the outside world - databases, APIs, file systems, and user interfaces.

In our news aggregator, this fortress approach delivers concrete benefits:

  • Business rules live in isolation - News validation and processing logic remains pure and testable
  • External dependencies become pluggable - Swap RSS sources or database engines without breaking anything
  • Multiple interfaces, same core - Whether users access via web browser or CLI, they interact with identical business logic
  • Testing becomes a breeze - Mock external dependencies and test your core logic in isolation

The Game-Changing Benefits

Still skeptical? Let me show you why Hexagonal Architecture will revolutionize your PHP development experience:

πŸš€ Fearless Refactoring: Change your database from MySQL to PostgreSQL without breaking a sweat - your business logic doesn't even notice

πŸ§ͺ Lightning-Fast Testing: Write comprehensive unit tests that run in milliseconds, not minutes. No more waiting for database setup in your test suite

πŸ”§ Effortless Feature Addition: Adding a new RSS source? Just create a new adapter. Your existing code remains untouched and unbroken

🎯 Framework Freedom: Not married to Laravel? Great! Switch to Symfony, CodeIgniter, or go completely framework-free - your core logic travels with you

πŸ’Ž Business Logic Clarity: Your domain rules become crystal clear, living in their own dedicated space where they belong

The Three Pillars of Hexagonal Architecture

Let's demystify the core concepts that make Hexagonal Architecture so powerful. Think of these as the three pillars supporting your architectural fortress - remove one, and the whole structure crumbles.

Pillar 1: Ports and Adapters - The Gateway System

Picture your application as a bustling medieval city. Ports are the gates in the city wall - they define what can enter and exit, but don't care about the details of who's coming or going. Adapters are the specific gatekeepers - they handle the actual mechanics of opening the gate for merchants, soldiers, or travelers.

Ports (interfaces) declare the contract: "I need to save news articles, but I don't care if you use MySQL, PostgreSQL, or carrier pigeons."

Adapters (implementations) fulfill the contract: "I'll handle saving to MySQL, complete with all the messy SQL details."

Here's a real example from our news aggregator:

// Port: An interface defining news persistence operations
interface NewsRepository
{
    public function save(NewsEntity $entity): NewsEntity;
    public function findById(int $id): ?NewsEntity;
    public function existsByUrl(string $url): bool;
    public function findAll(): array;
}

// Adapter: MySQL implementation of the repository
class MysqlNewsRepository extends BaseRepository implements NewsRepository
{
    public function save(NewsEntity $entity): NewsEntity
    {
        if ($entity->getId() === 0) {
            return $this->insert($entity);
        }
        return $this->update($entity);
    }

    public function findById(int $id): ?NewsEntity
    {
        $sql = "SELECT * FROM news WHERE id = :id";
        $stmt = $this->connection->prepare($sql);
        $stmt->execute(['id' => $id]);

        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ? MysqlNewsMapper::toDomain($row) : null;
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, NewsRepository is the port, and MysqlNewsRepository is the adapter.

Pillar 2: Dependency Injection - The Plug-and-Play System

Dependency Injection is the secret sauce that makes Hexagonal Architecture truly magical. Instead of your classes creating their own dependencies (which leads to tight coupling), you inject the dependencies from the outside. It's like having a universal power adapter - plug in any device, and it just works.

Here's how our CLI command system uses DI:

// Command registry with factory functions for DI
$commands = [
    'extract-news' => function () {
        // Dependencies are injected here
        $newsRepository = new MysqlNewsRepository();
        $parserFactory = new RssParserFactory();
        $rssParser = $parserFactory->createCompositeParser();
        $extractNewsUseCase = new ExtractNewsUseCase($newsRepository, $rssParser);

        return new ExtractNewsCommand($extractNewsUseCase, $rssSources);
    }
];
Enter fullscreen mode Exit fullscreen mode

With this setup, you can easily switch from MysqlNewsRepository to PostgresNewsRepository or even MongoNewsRepository without changing any business logic.

Pillar 3: Separation of Concerns - The Organization System

This is where the magic happens. Separation of Concerns is like having a well-organized toolbox where every tool has its place. Your screwdrivers don't mingle with your wrenches, and your business logic doesn't get tangled up with database code. Our news aggregator exemplifies this organization:

src/app/
β”œβ”€β”€ Core/                    # Framework-agnostic utilities
β”œβ”€β”€ Domain/                  # Pure business logic
β”‚   └── News/
β”‚       β”œβ”€β”€ Entities/        # Rich domain models
β”‚       β”œβ”€β”€ Repositories/    # Repository interfaces (ports)
β”‚       β”œβ”€β”€ UseCases/        # Application use cases
β”‚       └── Exceptions/      # Domain-specific exceptions
└── Infrastructure/          # External adapters
    β”œβ”€β”€ Database/           # Database implementations
    β”œβ”€β”€ Rss/               # RSS parsing implementations
    β”œβ”€β”€ Http/              # Web controllers
    └── Cli/               # CLI commands
Enter fullscreen mode Exit fullscreen mode

Building Your Hexagonal Fortress: A Step-by-Step Guide

Ready to transform your chaotic codebase? Let's build a robust Hexagonal Architecture from the ground up using our news aggregator as the blueprint. No more theoretical talk - it's time to get our hands dirty with real code!

Step 1: Laying the Foundation

Think of this as constructing the blueprint for your fortress. We're building three distinct territories, each with its own purpose and rules:

  • Core: The foundation stones - universal utilities that work anywhere, anytime
  • Domain: The treasure vault - where your precious business logic lives, protected from external chaos
  • Infrastructure: The outer defenses - handling all the messy interactions with databases, APIs, and user interfaces

Your project structure should look like this:

src/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ Core/
β”‚   β”‚   β”œβ”€β”€ Entities/
β”‚   β”‚   β”œβ”€β”€ Http/
β”‚   β”‚   └── Helpers/
β”‚   β”œβ”€β”€ Domain/
β”‚   β”‚   └── News/
β”‚   β”‚       β”œβ”€β”€ Entities/
β”‚   β”‚       β”œβ”€β”€ Repositories/
β”‚   β”‚       β”œβ”€β”€ UseCases/
β”‚   β”‚       β”œβ”€β”€ Services/
β”‚   β”‚       └── Exceptions/
β”‚   └── Infrastructure/
β”‚       β”œβ”€β”€ Database/
β”‚       β”œβ”€β”€ Rss/
β”‚       β”œβ”€β”€ Http/
β”‚       └── Cli/
└── config/
Enter fullscreen mode Exit fullscreen mode

Step 2: Creating the Gates (Defining Ports)

Now we're defining the entry points to our domain. These interfaces are like customs checkpoints - they specify exactly what can pass through, but don't care about the implementation details:

// Domain/News/Repositories/NewsRepository.php
interface NewsRepository
{
    public function save(NewsEntity $entity): NewsEntity;
    public function findById(int $id): ?NewsEntity;
    public function delete(int $id): bool;
    public function findAll(): array;
    public function search(string $searchTerm, int $page = 1, int $limit = 15): array;
    public function existsByUrl(string $url): bool;
}

// Domain/News/Services/RssParserInterface.php
interface RssParserInterface
{
    public function parseFromUrl(string $url): array;
    public function canHandle(string $url): bool;
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Building the Gatekeepers (Implementing Adapters)

Time to create the concrete implementations that do the actual work. These adapters are like skilled craftsmen who know exactly how to fulfill each contract:

// Infrastructure/Database/Repositories/MysqlNewsRepository.php
class MysqlNewsRepository extends BaseRepository implements NewsRepository
{
    public function save(NewsEntity $entity): NewsEntity
    {
        if ($entity->getId() === 0) {
            return $this->insert($entity);
        }
        return $this->update($entity);
    }

    public function existsByUrl(string $url): bool
    {
        $sql = "SELECT COUNT(*) FROM news WHERE url = :url";
        $stmt = $this->connection->prepare($sql);
        $stmt->execute(['url' => $url]);

        return (int) $stmt->fetchColumn() > 0;
    }
}

// Infrastructure/Rss/Parsers/JetBrainsRssParser.php
class JetBrainsRssParser extends AbstractRssParser
{
    public function canHandle(string $url): bool
    {
        return str_contains($url, 'jetbrains.com');
    }

    protected function extractSource(SimpleXMLElement $item): ?string
    {
        return 'JetBrains';
    }
}
Enter fullscreen mode Exit fullscreen mode

Structuring Your PHP Project for Hexagonal Architecture

By now, you should have a clear structure in place. Here's what your complete project structure should look like:

/news-aggregator
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ app/
β”‚   β”‚   β”œβ”€β”€ Core/
β”‚   β”‚   β”‚   β”œβ”€β”€ Entities/Entity.php
β”‚   β”‚   β”‚   β”œβ”€β”€ Http/
β”‚   β”‚   β”‚   └── Router.php
β”‚   β”‚   β”œβ”€β”€ Domain/
β”‚   β”‚   β”‚   └── News/
β”‚   β”‚   β”‚       β”œβ”€β”€ Entities/NewsEntity.php
β”‚   β”‚   β”‚       β”œβ”€β”€ Repositories/NewsRepository.php
β”‚   β”‚   β”‚       β”œβ”€β”€ UseCases/
β”‚   β”‚   β”‚       β”‚   β”œβ”€β”€ ExtractNewsUseCase.php
β”‚   β”‚   β”‚       β”‚   └── ListNewsUseCase.php
β”‚   β”‚   β”‚       └── Exceptions/
β”‚   β”‚   └── Infrastructure/
β”‚   β”‚       β”œβ”€β”€ Database/
β”‚   β”‚       β”‚   β”œβ”€β”€ Repositories/MysqlNewsRepository.php
β”‚   β”‚       β”‚   └── Mappers/MysqlNewsMapper.php
β”‚   β”‚       β”œβ”€β”€ Rss/
β”‚   β”‚       β”‚   β”œβ”€β”€ Parsers/
β”‚   β”‚       β”‚   └── Factories/RssParserFactory.php
β”‚   β”‚       β”œβ”€β”€ Http/Controllers/
β”‚   β”‚       └── Cli/Commands/
β”‚   β”œβ”€β”€ config/
β”‚   └── tests/
β”œβ”€β”€ docker-compose.yml
└── composer.json
Enter fullscreen mode Exit fullscreen mode

This structure keeps everything neat and organized, making it easy to navigate and maintain.

Putting It All Together: Building Our News Aggregator

Enough theory! Let's roll up our sleeves and build a real news aggregator that showcases every aspect of Hexagonal Architecture. You'll see how each piece fits together like a perfectly crafted puzzle.

The Complete Implementation Walkthrough

Step 1: Define the Domain Entity

// Domain/News/Entities/NewsEntity.php
class NewsEntity
{
    private const MAX_TITLE_LENGTH = 255;
    private const MAX_URL_LENGTH = 255;
    private const VALID_STATUSES = ['draft', 'published', 'archived'];

    private int $id;
    private string $title;
    private string $url;
    private ?string $description;
    private ?string $author;
    private ?DateTimeInterface $publishedDate;
    private string $status;

    public function __construct(
        string $title,
        string $url,
        int $id = 0,
        ?string $description = null,
        ?string $author = null,
        ?DateTimeInterface $publishedDate = null,
        string $status = 'draft'
    ) {
        $this->setTitle($title);
        $this->setUrl($url);
        $this->setId($id);
        $this->description = $description;
        $this->author = $author;
        $this->publishedDate = $publishedDate;
        $this->setStatus($status);
    }

    public function setTitle(string $title): void
    {
        $trimmedTitle = trim($title);

        if (empty($trimmedTitle)) {
            throw InvalidNewsDataException::emptyTitle();
        }

        if (strlen($trimmedTitle) > self::MAX_TITLE_LENGTH) {
            throw InvalidNewsDataException::titleTooLong(self::MAX_TITLE_LENGTH);
        }

        $this->title = $trimmedTitle;
    }

    public function setUrl(string $url): void
    {
        $trimmedUrl = trim($url);

        if (empty($trimmedUrl)) {
            throw InvalidNewsDataException::emptyUrl();
        }

        if (!filter_var($trimmedUrl, FILTER_VALIDATE_URL)) {
            throw InvalidNewsDataException::invalidUrlFormat();
        }

        $this->url = $trimmedUrl;
    }

    // ... other getters and setters
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create Use Cases in Domain Layer

// Domain/News/UseCases/ExtractNewsUseCase.php
class ExtractNewsUseCase
{
    private NewsRepository $newsRepository;
    private RssParserInterface $rssParser;

    public function __construct(
        NewsRepository $newsRepository,
        RssParserInterface $rssParser
    ) {
        $this->newsRepository = $newsRepository;
        $this->rssParser = $rssParser;
    }

    public function execute(string $rssUrl): int
    {
        try {
            echo "Fetching RSS feed from: {$rssUrl}\n";

            // Parse RSS feed using the injected parser
            $newsItems = $this->rssParser->parseFromUrl($rssUrl);

            $savedCount = 0;
            foreach ($newsItems as $newsItem) {
                // Check for duplicates
                if ($this->newsRepository->existsByUrl($newsItem->getUrl())) {
                    continue;
                }

                // Save news item
                $this->newsRepository->save($newsItem);
                $savedCount++;
            }

            return $savedCount;
        } catch (Exception $e) {
            throw new InvalidNewsDataException("Failed to extract news: " . $e->getMessage());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Implement Infrastructure Adapters

// Infrastructure/Rss/Parsers/AbstractRssParser.php
abstract class AbstractRssParser implements RssParserInterface
{
    public function parseFromUrl(string $url): array
    {
        $xml = $this->fetchRssContent($url);
        return $this->parseRssXml($xml);
    }

    protected function createNewsEntityFromItem(SimpleXMLElement $item): ?NewsEntity
    {
        try {
            $title = $this->extractTitle($item);
            $url = $this->extractUrl($item);
            $description = $this->extractDescription($item);
            $author = $this->extractAuthor($item);
            $pubDate = $this->extractDate($item);
            $source = $this->extractSource($item);

            if (empty($title) || empty($url)) {
                return null;
            }

            return new NewsEntity(
                title: $title,
                url: $url,
                description: $description,
                author: $author,
                publishedDate: $pubDate,
                status: 'published'
            );
        } catch (Exception $e) {
            return null;
        }
    }

    abstract protected function extractSource(SimpleXMLElement $item): ?string;

    // ... other extraction methods
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Create Factory for Parser Selection

// Infrastructure/Rss/Factories/RssParserFactory.php
class RssParserFactory
{
    private array $parsers;

    public function __construct()
    {
        $this->parsers = [
            new JetBrainsRssParser(),
            new RedditRssParser(),
            new StackOverflowRssParser(),
            new PhpClassesRssParser()
        ];
    }

    public function createParser(string $url): RssParserInterface
    {
        foreach ($this->parsers as $parser) {
            if ($parser->canHandle($url)) {
                return $parser;
            }
        }

        throw new InvalidNewsDataException("No parser found for URL: {$url}");
    }

    public function createCompositeParser(): CompositeRssParser
    {
        return new CompositeRssParser($this);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Integration with CLI and Web

// Infrastructure/Cli/Commands/ExtractNewsCommand.php
class ExtractNewsCommand implements CliCommandInterface
{
    private ExtractNewsUseCase $extractNewsUseCase;
    private array $rssSources;

    public function __construct(
        ExtractNewsUseCase $extractNewsUseCase,
        array $rssSources = []
    ) {
        $this->extractNewsUseCase = $extractNewsUseCase;
        $this->rssSources = $rssSources;
    }

    public function execute(array $args = []): void
    {
        echo "Starting RSS news extraction...\n";

        $totalExtracted = 0;
        foreach ($this->rssSources as $rssUrl) {
            $extractedCount = $this->extractNewsUseCase->execute($rssUrl);
            $totalExtracted += $extractedCount;
        }

        echo "Successfully extracted {$totalExtracted} news items total.\n";
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the CLI command interacts with the Use Case from the Domain layer, which in turn uses the Repository interface that's implemented in the Infrastructure layer.

Real-World Use Cases

Hexagonal Architecture is highly versatile and can be applied to various kinds of projects:

  • E-commerce platforms: Product catalog, order management, payment processing
  • Content Management Systems: Article publishing, user management, media handling
  • API backends: Data aggregation, third-party integrations, RESTful services
  • News aggregators: Like our example - RSS parsing, content management, search functionality

Migration Strategies from Traditional Architectures

If you're working with a legacy PHP system, migrating to Hexagonal Architecture can seem daunting. Here's a practical strategy:

  1. Identify Core Logic: Start by identifying the core business logic of your application
  2. Extract Domain Entities: Move business objects to a Domain layer with proper validation
  3. Define Repository Interfaces: Create interfaces for data access patterns
  4. Implement Adapters Gradually: Replace direct database calls with repository implementations
  5. Create Use Cases: Extract complex business workflows into dedicated use case classes
  6. Refactor Controllers: Make controllers thin by delegating to use cases

This incremental approach helps you adopt Hexagonal Architecture without major disruptions.

Testing: Where Hexagonal Architecture Truly Shines

Here's where you'll fall in love with Hexagonal Architecture. Testing becomes not just easier, but actually enjoyable. No more wrestling with database connections in your unit tests or mocking half the internet just to test a simple business rule.

Unit Testing: Pure Logic, Pure Joy

Unit tests in Hexagonal Architecture are like examining individual gears in a well-oiled machine. Each component can be tested in complete isolation, with crystal-clear boundaries and predictable behavior. Here's how we test our ExtractNewsUseCase:

// tests/Unit/Domain/News/UseCases/ExtractNewsUseCaseTest.php
class ExtractNewsUseCaseTest extends TestCase
{
    private ExtractNewsUseCase $useCase;
    private MockObject $newsRepository;
    private MockObject $rssParser;

    protected function setUp(): void
    {
        $this->newsRepository = $this->createMock(NewsRepository::class);
        $this->rssParser = $this->createMock(RssParserInterface::class);
        $this->useCase = new ExtractNewsUseCase(
            $this->newsRepository,
            $this->rssParser
        );
    }

    public function testExecuteShouldSaveNewNewsItems(): void
    {
        // Arrange
        $rssUrl = 'https://example.com/rss';
        $newsEntity = new NewsEntity('Test Title', 'https://example.com/article');

        $this->rssParser->expects($this->once())
            ->method('parseFromUrl')
            ->with($rssUrl)
            ->willReturn([$newsEntity]);

        $this->newsRepository->expects($this->once())
            ->method('existsByUrl')
            ->with('https://example.com/article')
            ->willReturn(false);

        $this->newsRepository->expects($this->once())
            ->method('save')
            ->with($newsEntity);

        // Act
        $result = $this->useCase->execute($rssUrl);

        // Assert
        $this->assertEquals(1, $result);
    }
}
Enter fullscreen mode Exit fullscreen mode

Integration Testing: When Components Dance Together

Integration tests are where we verify that our adapters actually fulfill their contracts. These tests ensure that our real implementations work correctly with actual databases, file systems, or external APIs:

// tests/Integration/Infrastructure/Database/MysqlNewsRepositoryTest.php
class MysqlNewsRepositoryTest extends TestCase
{
    private MysqlNewsRepository $repository;
    private PDO $connection;

    protected function setUp(): void
    {
        // Setup test database connection
        $this->connection = new PDO('sqlite::memory:');
        $this->createNewsTable();
        $this->repository = new MysqlNewsRepository();
    }

    public function testSaveShouldPersistNewsEntity(): void
    {
        // Arrange
        $newsEntity = new NewsEntity(
            'Test News',
            'https://example.com/news'
        );

        // Act
        $savedEntity = $this->repository->save($newsEntity);

        // Assert
        $this->assertNotEquals(0, $savedEntity->getId());
        $this->assertTrue($this->repository->existsByUrl('https://example.com/news'));
    }

    private function createNewsTable(): void
    {
        $sql = "CREATE TABLE news (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            url TEXT NOT NULL,
            description TEXT,
            author TEXT,
            published_date DATETIME,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
        )";

        $this->connection->exec($sql);
    }
}
Enter fullscreen mode Exit fullscreen mode

End-to-End Testing: The Full Symphony

End-to-end tests are like conducting a full orchestra - every component plays its part to create the complete user experience. These tests validate that your entire application works as intended from the user's perspective:

// tests/EndToEnd/NewsAggregatorE2ETest.php
class NewsAggregatorE2ETest extends TestCase
{
    public function testCompleteNewsExtractionWorkflow(): void
    {
        // Arrange: Setup test RSS feed
        $testRssUrl = 'https://httpbin.org/xml'; // Mock RSS endpoint

        // Act: Run the CLI command
        $output = shell_exec("cd src && php app/Infrastructure/Cli/index.php extract-news");

        // Assert: Verify news was extracted and saved
        $this->assertStringContains('Successfully extracted', $output);

        // Verify via web interface
        $webOutput = file_get_contents('http://localhost:8080');
        $this->assertStringContains('PHP News', $webOutput);
    }
}
Enter fullscreen mode Exit fullscreen mode

The Do's and Don'ts: Hard-Earned Wisdom

After building countless Hexagonal Architecture applications, here's the battle-tested wisdom that will save you from common mistakes and guide you toward success.

Golden Rules for Success

🎯 Start Small, Think Big: Begin with simple interfaces and grow complexity only when needed

🎯 Type Everything: PHP's type system is your friend - use it to make your contracts bulletproof

🎯 Test-Drive Your Design: Write tests first to ensure your architecture actually works

🎯 Embrace Standards: PSR-4 autoloading and PSR-12 coding standards are your foundation

🎯 Composition Over Inheritance: Build flexible systems by combining simple parts

Landmines to Avoid

πŸ’₯ The Over-Engineering Trap: Don't create interfaces for every single class - keep it practical

πŸ’₯ The Anemic Domain: Your entities should have behavior, not just getters and setters

πŸ’₯ The Performance Blind Spot: Architecture is meaningless if your app crawls

πŸ’₯ The Silent Failure: Implement proper error handling or debugging becomes a nightmare

πŸ’₯ The Dependency Leak: Keep your arrows pointing inward - domain should never depend on infrastructure

Performance: Speed Without Compromising Clean Architecture

Clean architecture doesn't mean slow architecture! Here's how to keep your Hexagonal fortress running at lightning speed:

⚑ Database Optimization: Prepared statements and smart indexing are your best friends

⚑ Strategic Caching: Redis or Memcached can dramatically reduce database load

⚑ Connection Management: Pool your database connections for high-traffic scenarios

⚑ Profile Everything: Xdebug and profiling tools reveal hidden performance bottlenecks

⚑ Queue Heavy Work: Message queues handle long-running tasks without blocking users

From Chaos to Clean: Your Journey Complete

Congratulations! You've just witnessed the transformation from architectural chaos to clean, maintainable code. Our news aggregator isn't just a project - it's proof that you can build enterprise-grade PHP applications without drowning in framework complexity.

You now possess the knowledge to:

  • Build applications that laugh in the face of changing requirements
  • Write tests that actually make sense and run lightning-fast
  • Swap out entire system components without breaking a sweat
  • Sleep peacefully knowing your business logic is protected and pure

The Path Forward

Remember, Hexagonal Architecture isn't about building perfect code from day one. It's about building code that can evolve, adapt, and thrive in the face of inevitable change. Start small, apply these principles gradually, and watch your codebase transform from a maintenance nightmare into a joy to work with.

Your fellow developers will thank you. Your future self will thank you. And most importantly, you'll never have to fear that "simple" feature request again.

πŸ”— Explore the Complete Implementation

Want to dive deeper into the code? Check out the complete news aggregator implementation on GitHub:

πŸ“° PHP News Aggregator - Complete Source Code

Clone it, explore it, and make it your own. Happy coding! πŸš€

Top comments (3)

Collapse
 
xwero profile image
david duymelinck

You don't need hexagonal architecture for testing, just dependency injection and composition.

I prefer a modular approach. The main reason is that I want to keep related files together and I want a flatter file structure.

/news-aggregator
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ Shared/
β”‚   β”‚   β”œβ”€β”€ Entities/Entity.php
β”‚   β”‚   β”œβ”€β”€ Http/
β”‚   β”‚   β”œβ”€β”€ Rss/
β”‚   β”‚   β”‚   β”œβ”€β”€ Parsers/
β”‚   β”‚   β”‚   └── Factories/RssParserFactory.php
β”‚   β”‚   └── Router.php
β”‚   β”œβ”€β”€ News/
β”‚   β”‚   β”œβ”€β”€ Entities/Entity.php
β”‚   β”‚   β”œβ”€β”€ Repositories
β”‚   β”‚   β”‚   β”œβ”€β”€ Repository.php
β”‚   β”‚   β”‚   └── MysqlRepository.php
β”‚   β”‚   β”œβ”€β”€ Mappers/MysqlMapper.php
β”‚   β”‚   β”œβ”€β”€ UseCases/
β”‚   β”‚   β”‚   β”œβ”€β”€ ExtractUseCase.php
β”‚   β”‚   β”‚   └── ListUseCase.php
β”‚   β”‚   β”œβ”€β”€ Exceptions/
β”‚   β”‚   β”œβ”€β”€ Controllers/
β”‚   β”‚   β”œβ”€β”€ Commands/
β”‚   β”‚   β”œβ”€β”€ config/
β”‚   β”‚   └── tests/
Enter fullscreen mode Exit fullscreen mode

The domain-infrastructure file separation it more a burden than a blessing for me.

A bit off topic

foreach ($newsItems as $newsItem) {
                // Check for duplicates
                if ($this->newsRepository->existsByUrl($newsItem->getUrl())) {
                    continue;
                }
Enter fullscreen mode Exit fullscreen mode

One of the first rules I learned is never run database queries in a loop.

Collapse
 
helmarjunior profile image
Helmar

Nice one! Hexagonal Architecture is just one tool in the box (in this case, an architecture), and it’s not the only way to achieve clean, testable code. Also, HA is not only about the file structure, but I got your point.

I liked your modular approach, how do you handle cross-module dependencies at scale? Great catch on the query loop, that's a nasty N+1 waiting to happen, btw.

Collapse
 
xwero profile image
david duymelinck • Edited

HA is not only about the file structure

Yes I know, but the file structure is the one point that bothers me the most about it.
I understand why the file structure is that way, it provides separation. But I prefer more cohesion.

how do you handle cross-module dependencies at scale?

I assume you are thinking about application dependencies and not third party dependencies?
It depends on how much of the functionality is shared. If it is only a few methods or variables I tend to duplicate them in the modules. Otherwise it is a parent class or classes in the shared directory and child classes in the modules.
I noticed that the more functionality classes have in common, the more likely it is that a change in the parent class is for all child classes. But it is only a rule of thumb, there will be cases where it is needed to separate the classes.