Skip to content

Commit 3dc0329

Browse files
committed
OXDEV-8947 Add decoration example
Signed-off-by: Anton Fedurtsya <anton@fedurtsya.com>
1 parent c4c5717 commit 3dc0329

4 files changed

Lines changed: 172 additions & 0 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ The repository contains examples of following cases and more:
9494

9595
* [Using Symfony DI](services.yaml)
9696
* [Injection of Registry classes with bind](https://github.com/OXID-eSales/examples-module/blob/b-7.4.x/services.yaml#L16)
97+
* [Service decoration](src/Greeting/Service/Decorator/GreetingValidationDecorator.php) - shows how to decorate services
98+
* Note: While the example uses validation/truncation for simplicity, better use cases include logging, caching, performance monitoring, or audit trails
99+
* [Decorator registration](src/Greeting/services.yaml) - using `decorates:` in DI configuration
97100

98101
* [Migrations](migration)
99102
* extending a shop database table (`oxuser`)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
/**
4+
* Copyright © . All rights reserved.
5+
* See LICENSE file for license details.
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OxidEsales\ExamplesModule\Greeting\Service\Decorator;
11+
12+
use OxidEsales\Eshop\Application\Model\User as EshopModelUser;
13+
use OxidEsales\ExamplesModule\Greeting\Service\GreetingMessageServiceInterface;
14+
15+
/**
16+
* Decorator that adds validation to greeting messages.
17+
*
18+
* This is an example of the Decorator pattern in Symfony DI.
19+
* Decorators allow you to add cross-cutting concerns (like validation,
20+
* logging, caching) without modifying the original service.
21+
*
22+
* Benefits:
23+
* - Open/Closed Principle: extend functionality without modifying existing code
24+
* - Single Responsibility: validation logic separate from business logic
25+
* - Easy to enable/disable: just register/unregister the decorator
26+
* - Testable: can test validation independently
27+
*/
28+
readonly class GreetingValidationDecorator implements GreetingMessageServiceInterface
29+
{
30+
private const MAX_GREETING_LENGTH = 50;
31+
32+
public function __construct(
33+
private GreetingMessageServiceInterface $originalService
34+
) {
35+
}
36+
37+
public function getGeneralGreeting(): string
38+
{
39+
return $this->originalService->getGeneralGreeting();
40+
}
41+
42+
public function getGreeting(?EshopModelUser $user = null): string
43+
{
44+
return $this->originalService->getGreeting($user);
45+
}
46+
47+
/**
48+
* Truncates greeting message if it exceeds maximum length before saving.
49+
*/
50+
public function saveGreetingForCurrentUser(string $message): void
51+
{
52+
if (mb_strlen($message) > self::MAX_GREETING_LENGTH) {
53+
$message = mb_substr($message, 0, self::MAX_GREETING_LENGTH);
54+
}
55+
56+
$this->originalService->saveGreetingForCurrentUser($message);
57+
}
58+
}

src/Greeting/services.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ services:
1616
$shopName: '%env(OEEM_SHOP_NAME)%'
1717
public: true
1818

19+
OxidEsales\ExamplesModule\Greeting\Service\Decorator\GreetingValidationDecorator:
20+
decorates: OxidEsales\ExamplesModule\Greeting\Service\GreetingMessageServiceInterface
21+
arguments:
22+
$originalService: '@.inner'
23+
1924
OxidEsales\ExamplesModule\Greeting\Service\UserServiceInterface:
2025
class: OxidEsales\ExamplesModule\Greeting\Service\UserService
2126

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
/**
4+
* Copyright © . All rights reserved.
5+
* See LICENSE file for license details.
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OxidEsales\ExamplesModule\Tests\Unit\Greeting\Service\Decorator;
11+
12+
use OxidEsales\Eshop\Application\Model\User;
13+
use OxidEsales\ExamplesModule\Greeting\Service\Decorator\GreetingValidationDecorator;
14+
use OxidEsales\ExamplesModule\Greeting\Service\GreetingMessageServiceInterface;
15+
use PHPUnit\Framework\Attributes\CoversClass;
16+
use PHPUnit\Framework\Attributes\DataProvider;
17+
use PHPUnit\Framework\Attributes\Test;
18+
use PHPUnit\Framework\TestCase;
19+
20+
#[CoversClass(GreetingValidationDecorator::class)]
21+
final class GreetingValidationDecoratorTest extends TestCase
22+
{
23+
#[Test]
24+
public function getGeneralGreetingDelegatesToInnerService(): void
25+
{
26+
$expectedGreeting = uniqid('greeting');
27+
$innerService = $this->createMock(GreetingMessageServiceInterface::class);
28+
$innerService->expects($this->once())
29+
->method('getGeneralGreeting')
30+
->willReturn($expectedGreeting);
31+
32+
$sut = $this->getSut(originalService: $innerService);
33+
34+
$this->assertSame($expectedGreeting, $sut->getGeneralGreeting());
35+
}
36+
37+
#[Test]
38+
public function getGreetingDelegatesToInnerService(): void
39+
{
40+
$expectedGreeting = uniqid('greeting');
41+
$userMock = $this->createMock(User::class);
42+
43+
$innerService = $this->createMock(GreetingMessageServiceInterface::class);
44+
$innerService->expects($this->once())
45+
->method('getGreeting')
46+
->with($userMock)
47+
->willReturn($expectedGreeting);
48+
49+
$sut = $this->getSut(originalService: $innerService);
50+
51+
$this->assertSame($expectedGreeting, $sut->getGreeting($userMock));
52+
}
53+
54+
#[Test]
55+
#[DataProvider('truncationProvider')]
56+
public function saveGreetingForCurrentUserTruncatesLongMessages(
57+
string $inputMessage,
58+
string $expectedMessage
59+
): void {
60+
$innerService = $this->createMock(GreetingMessageServiceInterface::class);
61+
$innerService->expects($this->once())
62+
->method('saveGreetingForCurrentUser')
63+
->with($expectedMessage);
64+
65+
$sut = $this->getSut(originalService: $innerService);
66+
$sut->saveGreetingForCurrentUser($inputMessage);
67+
}
68+
69+
public static function truncationProvider(): array
70+
{
71+
return [
72+
'exactly 50 chars' => [
73+
str_repeat('a', 50),
74+
str_repeat('a', 50),
75+
],
76+
'exactly 51 chars - truncated to 50' => [
77+
str_repeat('a', 51),
78+
str_repeat('a', 50),
79+
],
80+
'long message - 100 chars truncated to 50' => [
81+
str_repeat('a', 100),
82+
str_repeat('a', 50),
83+
],
84+
'message with multibyte chars' => [
85+
'Это очень длинное приветствие которое должно быть обрезано',
86+
'Это очень длинное приветствие которое должно быть ',
87+
],
88+
'short message passed through' => [
89+
'Hello',
90+
'Hello',
91+
],
92+
'empty string passed through' => [
93+
'',
94+
'',
95+
],
96+
];
97+
}
98+
99+
private function getSut(
100+
?GreetingMessageServiceInterface $originalService = null
101+
): GreetingValidationDecorator {
102+
return new GreetingValidationDecorator(
103+
originalService: $originalService ?? $this->createStub(GreetingMessageServiceInterface::class)
104+
);
105+
}
106+
}

0 commit comments

Comments
 (0)