Originally published on decentphp.dev — part of the Writing decent PHP course. View this lesson on the original site.

Over-engineering often starts with a reasonable-sounding sentence:

"We might need this in future."

Most of the time, that future never arrives, and your team inherits extra complexity for no current benefit.

Before

This version is intentionally over-engineered for a simple rule.

<?php

interface UserSelectionStrategy
{
    public function select(array $users): array;
}

final class ActiveUserSelectionStrategy implements UserSelectionStrategy
{
    public function select(array $users): array
    {
        return array_filter(
            $users,
            fn (array $user): bool => ($user['active'] ?? false) === true
        );
    }
}

final class EmailProjection
{
    public function project(array $users): array
    {
        return array_values(
            array_map(
                fn (array $user): string => (string) ($user['email'] ?? ''),
                $users
            )
        );
    }
}

final class UserExportPipeline
{
    public function __construct(
        private UserSelectionStrategy $selection,
        private EmailProjection $projection
    ) {
    }

    public function run(array $users): array
    {
        $selected = $this->selection->select($users);
        return $this->projection->project($selected);
    }
}

After

The behaviour stays the same, but the code matches the problem we actually have today.

<?php

function activeEmails(array $users): array
{
    $emails = [];

    foreach ($users as $user) {
        if (($user['active'] ?? false) !== true) {
            continue;
        }

        $email = (string) ($user['email'] ?? '');
        if ($email === '') {
            continue;
        }

        $emails[] = $email;
    }

    return $emails;
}

Why this version is better

  • it solves today's requirement directly, without speculative abstraction
  • fewer moving parts make review and debugging faster
  • behaviour is unchanged, but maintenance cost is lower

This is a tough one. You will absolutely meet developers who argue the "before" version is better.

Sometimes they are right.
Sometimes that level of abstraction is exactly what the situation needs.

The key is context.

If you over-engineer everything you check into the codebase, it gets messy fast.
Abstract when there is a real pressure for it, not just a hypothetical future.

From Writing decent PHP — What decent PHP looks like