-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New Post: Decorator pattern in open source code
- Loading branch information
Showing
2 changed files
with
243 additions
and
0 deletions.
There are no files selected for viewing
Binary file added
BIN
+94.6 KB
src/assets/images/decorator-post-symfony-profile-page-on-log-section.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
243 changes: 243 additions & 0 deletions
243
src/content/blog/decorator-pattern-in-open-source-code.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
--- | ||
author: Erison Silva | ||
pubDatetime: 2024-09-27T10:00:00Z | ||
title: Decorator pattern in open source code | ||
slug: decorator-pattern-in-open-source-code | ||
featured: true | ||
draft: false | ||
tags: | ||
- pattern | ||
- developer | ||
description: How Decorator pattern can help us modify some functionalities from open source code. | ||
--- | ||
|
||
In some open source repositories that contain a **small** number of maintainers are common to see classes as final, it happens because they can make modification without complex _Backward Compatibility Promise_, But on the other hand it keeps code a bit "complicated" to modify! | ||
|
||
But well it is not so complicated since they implements interfaces and use _composition_ in the code 😁, and here starts the beneficial of [Decorator pattern][decorator-pattern] 💅. | ||
|
||
## Table of contents | ||
|
||
## Open source code | ||
|
||
I was looking for some project that has a good number of users, Then I choose [EasyAdminBundle][easy-admin-bundle] as example, it has more then **+18k users** and **4k starts** | ||
|
||
the piece of code that I gonna use is [AdminUrlGenerator.php][admin-url-generator], and we gonna add a dispatch event when the [generate method][admin-url-generator-method] is called. | ||
|
||
## Creating a decorator | ||
|
||
As the AdminUrlGenerator.php is a final class, it is not possible to extends this class and override `generate` method, and in this case an alternative is use a decorator. | ||
|
||
```php | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace App\Decorator; | ||
|
||
use App\Event\UrlGeneratedEvent; | ||
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator; | ||
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGeneratorInterface; | ||
use Symfony\Component\DependencyInjection\Attribute\AsDecorator; | ||
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; | ||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; | ||
|
||
#[AsDecorator(decorates: AdminUrlGenerator::class)] | ||
class AdminUrlGeneratorDecorator implements AdminUrlGeneratorInterface | ||
{ | ||
public function __construct( | ||
#[AutowireDecorated] | ||
private AdminUrlGeneratorInterface $adminUrlGenerator, | ||
private EventDispatcherInterface $event, | ||
) { | ||
} | ||
|
||
public function generateUrl(): string | ||
{ | ||
$url = $this->adminUrlGenerator->generate(); | ||
$this->event->dispatch(new UrlGeneratedEvent($url)); | ||
|
||
return $url; | ||
} | ||
|
||
public function setRoute(string $routeName, array $routeParameters = []): self | ||
{ | ||
$this->adminUrlGenerator->setRoute($routeName, $routeParameters); | ||
|
||
return $this; | ||
} | ||
|
||
public function get(string $paramName): mixed | ||
{ | ||
return $this->adminUrlGenerator->get($paramName); | ||
} | ||
|
||
//.... | ||
} | ||
``` | ||
|
||
## Breaking Down AdminUrlGeneratorDecorator class | ||
|
||
```php | ||
#[AsDecorator(decorates: AdminUrlGenerator::class)] | ||
|
||
//... | ||
|
||
#[AutowireDecorated] | ||
``` | ||
|
||
As EasyAdminBundle is based on _Symfony Freamework_. it is one of the way to [decorate][symfony-docorator] a class. | ||
|
||
in `decorates` parameter you pass the class that you want to decorate, this way Symfony will inject `AdminUrlGeneratorDecorator` instead of `AdminUrlGenerator` | ||
|
||
and as we want reuse the original class, we are going to add `#[AutowireDecorated]` on top of `$adminUrlGenerator`. | ||
It will inject AdminUrlGenerator class on that parameter. | ||
|
||
```php | ||
<?php | ||
|
||
final class AdminUrlGeneratorDecorator implements AdminContextProvider | ||
``` | ||
|
||
Here we are creating the decorator and implementing the same interface used in AdminUrlGenerator class, it is important because this class is injected via `__construct` via [AdminContextProvider][admin-context-provider-interface] interface, and as both class implements the same interface we can choose which class we want to in inject and in our case we gonna inject the new class created (AdminUrlGeneratorDecorator) | ||
|
||
```php | ||
<?php | ||
|
||
public function __construct( | ||
#[AutowireDecorated] | ||
private AdminUrlGenerator $adminUrlGenerator, | ||
private EventDispatcherInterface $event, | ||
) { | ||
} | ||
``` | ||
|
||
- `AdminUrlGenerator` is the original class that we want to modify the behavour | ||
- `EventDispatcherInterface` is the Symofony event class, we gonna use this inside of method that we want to override. | ||
|
||
```php | ||
<?php | ||
public function generateUrl(): string | ||
{ | ||
$url = $this->adminUrlGenerator->generate(); | ||
$this->event->dispatch(new UrlGeneratedEvent($url)); | ||
|
||
return $url; | ||
} | ||
``` | ||
|
||
It is the method that we want to modify, on the first line we are calling the original method to generate the url. | ||
the second line is dispatching our event and in the last one we are returning the url generated by original method. | ||
|
||
```php | ||
<?php | ||
public function setRoute(string $routeName, array $routeParameters = []): self | ||
{ | ||
$this->adminUrlGenerator->setRoute($routeName, $routeParameters); | ||
|
||
return $this; | ||
} | ||
|
||
public function get(string $paramName): mixed | ||
{ | ||
return $this->adminUrlGenerator->get($paramName); | ||
} | ||
|
||
//.... | ||
``` | ||
|
||
Maybe you were asking yourself, "why do I need those methods if I just want to change `generate` method?" | ||
|
||
It is needed because [AdminContextProvider][admin-context-provider-interface] is forcing to implement those methods. | ||
and this case we must implement them and call the original implementation, like I did with `setRoute` and `get` methods | ||
|
||
> Just to simplify the example I did not add all methods, But you must add all of them. | ||
## Full example | ||
|
||
```php | ||
//src/Decorator/AdminUrlGeneratorDecorator.php | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace App\Decorator; | ||
|
||
use App\Event\UrlGeneratedEvent; | ||
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator; | ||
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGeneratorInterface; | ||
use Symfony\Component\DependencyInjection\Attribute\AsDecorator; | ||
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; | ||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; | ||
|
||
#[AsDecorator(decorates: AdminUrlGenerator::class)] | ||
class AdminUrlGeneratorDecorator implements AdminUrlGeneratorInterface | ||
{ | ||
// ... | ||
``` | ||
|
||
```php | ||
//src/Event/UrlGeneratedEvent.php | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace App\Event; | ||
|
||
class UrlGeneratedEvent | ||
{ | ||
public function __construct(private string $url) | ||
{ | ||
} | ||
|
||
public function getUrl(): string | ||
{ | ||
return $this->url; | ||
} | ||
} | ||
``` | ||
|
||
```php | ||
//src/Listener/UrlGeneratedListener.php | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace App\Listener; | ||
|
||
use App\Event\UrlGeneratedEvent; | ||
use Psr\Log\LoggerInterface; | ||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener; | ||
|
||
#[AsEventListener] | ||
class UrlGeneratedListener | ||
{ | ||
public function __construct(private LoggerInterface $logger) | ||
{ | ||
} | ||
|
||
public function __invoke(UrlGeneratedEvent $event) | ||
{ | ||
$this->logger->info( | ||
'New url generated.', | ||
[ | ||
'url' => $event->getUrl(), | ||
'eventClass' => get_class($event), | ||
'listenerClass' => get_class($this), | ||
] | ||
); | ||
} | ||
} | ||
``` | ||
|
||
AdminContextProvider interface is used in some classes as [MenuFactory.php][easy-admin-menu-factory], this class is called when we load some admin page that contain menu! | ||
|
||
Then when a call `/admin` page, you can see there is a info log created by our Listener. | ||
![Image with symfony profile page on log section](@assets/images/decorator-post-symfony-profile-page-on-log-section.png) | ||
|
||
[decorator-pattern]: https://refactoring.guru/design-patterns/decorator | ||
[easy-admin-bundle]: https://github.com/EasyCorp/EasyAdminBundle | ||
[easy-admin-menu-factory]: https://github.com/EasyCorp/EasyAdminBundle/blob/v4.12.0/src/Factory/MenuFactory.php#L31 | ||
[admin-url-generator]: https://github.com/EasyCorp/EasyAdminBundle/blob/v4.12.0/src/Router/AdminUrlGenerator.php | ||
[admin-url-generator-method]: https://github.com/EasyCorp/EasyAdminBundle/blob/v4.12.0/src/Router/AdminUrlGenerator.php#L261 | ||
[admin-context-provider-interface]: https://github.com/EasyCorp/EasyAdminBundle/blob/v4.12.0/src/Router/AdminUrlGeneratorInterface.php#L10 | ||
[symfony-docorator]: https://symfony.com/doc/current/service_container/service_decoration.html |