Skip to content

Instantly share code, notes, and snippets.

@lcobucci
Last active May 24, 2018 16:33
Show Gist options
  • Save lcobucci/6c89a97ea9c11a6cccfc887afa15df81 to your computer and use it in GitHub Desktop.
Save lcobucci/6c89a97ea9c11a6cccfc887afa15df81 to your computer and use it in GitHub Desktop.
OMG I've created yet another microframework!

OMG I've created yet another microframework!

Yeap, that's right... you didn't misread it 😂

Oh no, are you crazy? Why on Earth did you do that!?

Great question! I'll try to explain my reasons here.

But first, let me make one thing straight: I actually didn't create a microframework. I've created a small set of abstractions that allow us to use many different components together (like Zend Expressive v3 and Tactician) and focus on the behaviour of our software.

Okay, now that things are a bit clearer it's time to explain you my main reason (there're more, sure): Evolvability.

Evolvability

What's that? In summary, it is the ability of our software architecture to evolve easily according to the business' needs.

IMHO, the way we create software in PHP is usually either quite bloated or way too slim. And it forces us to think way too much about the architectural needs of our application instead of understanding the subtleties of the domain. But please, don't get me wrong! I'm not saying that we shouldn't think about architecture while creating things. What I mean is that we should have a proper balance between these two focus areas, following the basic advice that Robert C. Martin repeats over and over again: we should be able to postpone (abstract) architectural decisions.

For a long time, I've been thinking and applying this mindset in the code I write. But I used to have a feeling that I was alone in all this craziness (our community usually speaks about onion/hexagonal/clean architecture but don't go much further than that) until I've found out that there're people discussing this topic thoroughly at ThoughtWorks since 2011! The gist of their thoughts is that combining layered architecture, microservices, and a good CI/CD pipeline we end up having multiple ways to evolve each Bounded Context of our software in a very sustainable manner - which is just amazing. Take a look at http://evolutionaryarchitecture.com and http://shop.oreilly.com/product/0636920080237.do to learn more.

Leaving the microservices topic aside, I've decided to enable the evolution of the application and domain layers by using service buses to handle both write and read sides. That allows us to easily modify the command/query handlers' behaviour, without breaking the interface with the UI layer, effectively decoupling our software from the different delivery mechanisms we decide to use.

So theoretically, it's absolutely trivial to make architectural changes, like applying Event Sourcing in just some aggregate roots.

That's pretty much the foundation of my creation.

Extensibility and reusability

Like everyone, I don't want to reinvent the wheel. We have marvellous tools created by very talented people. Also, with the PSR's we are able to interchange libraries and use our preferred tools. My goal was then to combine those tools, rather than introduce new ones.

I've decided to put my focus primarily on writing API's, so PSR-7 and PSR-15 were very relevant. However, I chose to leave an extension point for the application input, which allows handling input from CLI as well.

Since we have a common interface for communicating with the application layer (command/query bus), we can have reusable request handlers for each type of endpoint. Which roughly translates to NO MORE CONTROLLERS! Well, at least we don't need to write them anymore.

More examples of extensions points are the generation of identifiers, message (command/query) instance creation strategy, and read model conversion.

To glue everything together, I used the Symfony dependency injection component, which allows us to compile the DI container and boosts the overall performance of the software. Another interesting take on reusability is the fact that well established Symfony bundles can be plugged in, even though we're not using Symfony.

Strict separation of layers

I mentioned this before but it's really important to stress that I did the best I could to ensure that the UI layer only knows about components of the Application layer, which only manipulates elements from the Domain layer.

This might increase the number of calls we have in the software, but it's a good trade-off to ensure that we don't have any layer accessing or exposing things that shouldn't be accessed/exposed.

Performance

A huge challenge that I decided to take was to reduce, as much as possible, the number of operations executed during the application runtime. To achieve that, I moved all the complexity to the container compilation process. Yes, the creation of the HTTP middleware pipeline, command bus middleware pipeline, and query bus middleware pipeline is done via compiler passes.

I did some basic benchmarks (with only 1 concurrent user) in my development laptop (a Mac) and I'm quite happy with the results - everything is way faster on a Linux, though.

The sample application I have is a book collection management, where you can register books and retrieve information about the books you have.

Everything is quite simple but since I decided to keep everything separated there're many steps to get to the response. The flow is this:

  1. Translate HTTP request to query
  2. Perform retrieval action, reading from a JSON file and hydrating into Book objects
  3. Convert Book objects into BookDto objects to not expose the domain layer
  4. Format the result according to the Accept header

These are results:

Test duration: 0:01:05
Samples count: 22554, 0.00% failures
Average times: total 0.002, latency 0.002, connect 0.000
Percentile 0.0%: 0.001
Percentile 50.0%: 0.002
Percentile 90.0%: 0.003
Percentile 95.0%: 0.004
Percentile 99.0%: 0.007
Percentile 99.9%: 0.020
Percentile 100.0%: 0.206

I've created another version using React HTTP component and an in-memory repository and it's even faster:

Test duration: 0:01:05
Samples count: 24162, 0.00% failures
Average times: total 0.002, latency 0.002, connect 0.001
Percentile 0.0%: 0.001
Percentile 50.0%: 0.002
Percentile 90.0%: 0.003
Percentile 95.0%: 0.003
Percentile 99.0%: 0.006
Percentile 99.9%: 0.021
Percentile 100.0%: 6.715

Note: even having a higher throughput, the 99.9% and 100.0% percentiles are quite high for the kind of application we have here. Which makes me think that there's something in the React HTTP's I/O handling that can be improved.

Note 2: server and client (stress test using BlazeMeter Taurus) were running on the same machine, which obviously stressed out my processor and memory.

I might be crazy but it does make sense, right?

If you do want to play with this thing, go ahead and take a look at https://github.com/chimeraphp. All the abstractions and adapters are already available and I'll be sending the sample app pretty soon. But please keep in mind that EVERYTHING is experimental.

I really hope that everything I wrote here is interesting for someone and can potentially help people to write better software using PHP. It would be wonderful if you could share your thought with me, you'll either convince me that I'm completely mad and should just stop or give me more ideas to expand this whole thing!

@Ocramius
Copy link

So... what happens if the front controller becomes ReactPHP?

@hgraca
Copy link

hgraca commented May 23, 2018

The idea seems sound!
I will put it in my todo list to take that baby for a ride!

@lcobucci
Copy link
Author

lcobucci commented May 23, 2018

@Ocramius of course you can use ReactPHP as the front controller, but since it doesn't follow PSR-15 for the middleware and request handler the stuff I wrote is not applicable. However you can simply use Zend\Expressive\Application#handle() to handle the PSR-7 request and produce the response (which was what I used for the tests: https://github.com/chimeraphp/sample-react).

@wesleyvicthor
Copy link

smooth! I like how the orchestration is being done. If I work for an implementation using this design I would skip the *Dto thing.
I personally don't like them, and my alternative would come implementing \JsonSerializable; one less class, self encapsulation of the presentation and seamlessly conversion approach.

@lcobucci
Copy link
Author

@wesleyvicthor thanks =) Having the DTO thing is not mandatory. If your query handler already returns a read model, which usually what you have when using event sourcing, the read model middleware will simply not kick in.

I wouldn't use JsonSerializable, though. Mostly because we can have that read model being rendered in different formats according to your needs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment