I don’t care about frameworks, and I don’t care about the web. “The web is just an I/O device”. If we quote Bob C. Martin.

When I write a web app, I try to abstract away things I need to depend on to interact with concrete resources, from the business logic, so that the application can be easily updated, or extended. Without the need to care about the underlying framework, API, library etc.

One example are controllers and services. Let’s say I have to change the name of a user, specified by a query parameter called ID, to a string in a query parameter called NAME.

class UserController extends BaseController
{
    public function __construct(private UserService $userService)
    {}

    public function updateUser(Request $request)
    {
        $this->userService->update($request->getParams());
    }
}

class UserService
{
    public function __construct(private UserRepositoryInterface $userRepository)
    {}

    public function updateUser(array $params)
    {
        if (!isset($params['ID']) || !isset($params['NAME'])) {
            throw new Exception;
        }

        $user = $this->userRepository->find($params['ID']);
        $user->name = $params['NAME'];
        $this->userRepository->save($user);
    }
}

As you can see, UserService expects some parameters, coming from somewhere, but it’s just a plain array, doesn’t need to be an array, all I need is the service receiving plain representation of the data needed. This service can carry on its duties not worrying about http, cli, christmas, nor anything. In whatever state is the world, it only cares about its data, and its job.

What about data? I abstract where the data comes from too. My service depends on a interface, and the class implementing these interfaces are just returning data, the service doesn’t care.

interface UserRepositoryInterface
{
    function find(int $id);
}

class DoctrineUserRepository extends ServiceEntityRepository implements UserRepositoryInterface
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, User::class);
    }

}

class RestUserRepository implements UserRepositoryInterface
{
    public function __construct(private RestClient $restClient)
    {}

    public function find(int $id)
    {
        $userData = $this->restClient->fetchData($id);

        return (new User())->setId($id)->setName($userData['NAME']);
    }
}

We can also implement a shared object model builder to share the logic of going from raw data to a given model.

By abstracting the repository implementation I can only not make my logic dependent on a certain data source, but I can abstract away indefinitely, to make my app easier to change and more resilient, “There is no problem which can’t be solved by another layer of indirection, except the one of too many layers of indirection”