Do you write open-source? If so, you probably get many PR and issues about adding new feature, that people miss.
You can add them and increase project complexity or deny them and increase people's frustration. Both sucks for somebody. I prefer win-win situations: keep complexity low and add new features.
Magic? No, just patterns! Today we look on 3 of them I found and fond in Symfony and Laravel world.
There is a big mind-shift from closed-source to open-source. To make it really work, you need to move from my ego first to other people's feelings first.
It is like building Matrix open to everybody. You have to predict future and unexpected use cases. Your code have to be extendable without making any changes in it.
Now, I can refer to Open/closed principle on Wikipedia, which is the worst way to explain it.
Instead, I took a time to find simple example (pro tip: Google with "simple") and actually found one - go check it, it clearly shows wrong approach.
Today I will show you 3 ways to create such entrances.
When I first saw Laravel, I've noticed one big difference to other PHP projects I've seen. There is Contracts directory, that contains interface for every service there is in Laravel. And they're not only in this directory, but used everywhere in the code. Crazy move by Taylor Otwell in that times, but very useful.
To promote using interfaces instead of extending your classes like this:
class BoothCallEntrance implements MatrixEntranceInterface
{
}
class ComputerEntrance extends BoothCallEntrance
{
}
always mark your classes final. There is event sniff for that. Use it.
final class BoothCallEntrance implements MatrixEntranceInterface
{
}
final class ComputerEntrance implements MatrixEntranceInterface
{
}
Programmers won't have to think about raping your classes in the night - they just use the interface you provide.
This is code-embodied composition over inheritance. No documentation nor Wikipedia links required.
Back to Matrix world: imagine you can listen to every phone booth. Let's say you write a script, that sends you sms with geo location of the booth every time it gets called (favorite tool for agent Smith ;-)).
This approach is implemented in PHP under name of EventDispatcher. While working with Symfony, events gave me very similar feeling of freedom - in docs as well in small book A Year with Symfony.
Do you want simple example of such listening script? Check this tested post with all code snippets you need.
Matrix situation above would look like this:
Code of your package:
// some CRON script checking all booths are working
if ($booth->isCalled()) {
// you with people could get here without sending you a PR for everything they might need? Easy! ↓
// this is the entry point, just listen to 'boothCall'
$this->eventDispatcher->dispatch('boothCall', $booth->getLocation();
// ...
}
And custom script listening:
final class BoothSpy
{
public function listenTo()
{
return 'boothCall';
}
public function runOnPing($location)
{
$this->smsSender->sendABoothAlert($location);
}
}
It might be confusing while using at first, but after few weeks I get used to it. Trust me, it's the best.
This is most powerful and less known architecture pattern.
1 service collects all services of specific type
Do you know service tagging in Symfony?
services:
app.custom_subscriber:
class: AppBundle\EventListener\CustomSubscriber
tags:
- { name: kernel.event_subscriber }
All services of EventSubscriber
type are collected by EventDispatcher.
As for tags - they often promote bad practise of duplicated information. Don't use it if you don't have to.
services:
- YourSubscriber
Let's say we have a service to render Matrix. It might look like this:
final class MatrixRenderer()
{
public function render()
{
$this->agents->render();
$this->environment->render();
$this->people->render();
}
}
Later, one customer wants to render night clubs. Another customer wants to add weather. How to do that without modifying the code? Like this:
final class MatrixRenderer()
{
/**
* @var LayerRendererInterface[]
*/
private $layerRenderers = [];
public function addLayerRenderer(LayerRendererInterface $layerRenderer)
{
$this->layerRenderers[] = $layerRenderer;
}
public function render()
{
foreach ($this->layerRenderers as $layerRenderer) {
$layerRenderer->render();
}
}
}
And decouple to services:
services:
- AgentLayerRenderer
- EnvironmentLayerRenderer
- PeopleLayerRenderer
# added by customers
- NightClubsLayerRenderer
- WeatherLayerRenderer
Do you want to use collectors without pain? In case you use Symfony, Nette or Laravel, here is PackageBuilder that makes it simple.
Let me know if you use any of these patterns. Or do you use something else? I'd love to hear about that!
Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!