Ports and Adapters implementation in PHP, with a little help from Symfony

Because in my bounded contexts, Song means “A song that I would like to vote on” and is different from “A song from the external database” that comes from another bounded context.

They share only the same id.

The first test covers a vote for the new song added from the external db, it covers if the first vote is equal to the overall score.

The second test checks whether the overall score is the average of all votes.

The next two tests check if the votes are scored between 1 and 10.

There are no getters and setters, I don’t want my entity to be anemic.

The business logic is in thevote() method.

This file is created in a new directory called Domain.

I have created it outside the Symfony directory structure.

If I want to move my domain to another application, I simply copy the Domain directory.

Domain ServiceNow I can implement the rest of the business logic.

This is work for a service that uses repositories to receive and store data.

Test first:I want to findSong in the external db, add a score and then save it in my db.

As these are external repositories I have to create ports for them.

Ports are represented by Interfaces.

Interfaces exposes the methods that must be implemented in the application layer.

These implementations will be the adapters.

You might have noticed something interesting here.

The behaviour of the vote() method is written in just 3 lines of code and can be easily explained to a domain expert or non-technical person.

The vote() method is not working as expected.

The reasons and explanation will be discussed further in this article.

Out of the DomainFor this project I created a very simple “in memory” song database.

In the future it could be connected to real providers like Discogs or MusicBrainz.

To connect to this external db I need to implement SongRepositoryInterface.

This will be placed at the application level, in the Symfony Repository directory.

I am leaving the Domain level and starting to create adapters at the Application level.

The implementation of VoteRepositoryInterface is more complex because I’m using Doctrine.

But I have Symfony to help!Next levelI don’t want to use Doctrine annotations in my domain objects so mappings are defined in Symfony’s configurations.

This is the Framework level.

I also configured my db in Symfony’s .

env file:DATABASE_URL=sqlite:///%kernel.

project_dir%/var/data.

dband ran migrations:$ php bin/console make:migration$ php bin/console doctrine:migrations:migrateLet’s get back to the application level.

VoteRepositoryInterface implementation:TestingNow I can put everything together.

So I have created Controller to invoke my VotingService.

In the web browser or Postman I enter http://127.

0.

0.

1:8000/vote/1/9 and it shows success.

OK… but how this is possible?VotingService is passed to the constructor and Symfony’s DI container knows there is only one VotingService, so this one is injected.

How areSongRepositoryand DoctrineVoteRepository injected intoVotingService?.There are only interfaces passed to the constructor?!The answer is: Autowiring.

If there is only one implementation of the interface, that implementation will be passed.

There’s something wrong…The application is doing its job.

It saves data to my database but not quite as expected:So I reviewed the code.

Every song that came from the external db was saved as a new record in my db.

These three lines in VotingService were too simple:$song = $this->songRepository->findSong($id); $song->vote($score); $this->voteRepository->saveVote($song);So I had to fix this:It’s more complex, but still easy to explain: if there is no particular song in my db — add it from the external db and save the vote.

If the song was already in my db, get it and save the vote.

This fulfils all the requirements.

More than a little Symfony helpUsing Symfony with my PHP project helped me to focus on the Port and Adapters pattern implementation.

In this project I used:symfony/website-skeleton to generate project structure and all infrastructure to run a web server and test the project quicklysymfony/orm-pack to create migrations and use databaseSymfony’s DI Container and Autowiring to avoid tones of configurationsI could write simple infrastructure by myself, but I wanted to concentrate on architecture and not on framework!This project is available on Github.

.

. More details

Leave a Reply