Skip to content

Commit 3b4f265

Browse files
Merge branch '5.4' into 6.0
* 5.4: [DependencyInjection] Add `SubscribedService` attribute, deprecate current `ServiceSubscriberTrait` usage Use try/finally to restore error handlers Allow serializer default context configuration
2 parents ff7d2af + d4b1871 commit 3b4f265

File tree

3 files changed

+132
-6
lines changed

3 files changed

+132
-6
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Contracts\Service\Attribute;
13+
14+
use Symfony\Contracts\Service\ServiceSubscriberTrait;
15+
16+
/**
17+
* Use with {@see ServiceSubscriberTrait} to mark a method's return type
18+
* as a subscribed service.
19+
*
20+
* @author Kevin Bond <[email protected]>
21+
*/
22+
#[\Attribute(\Attribute::TARGET_METHOD)]
23+
final class SubscribedService
24+
{
25+
/**
26+
* @param string|null $key The key to use for the service
27+
* If null, use "ClassName::methodName"
28+
*/
29+
public function __construct(
30+
public ?string $key = null
31+
) {
32+
}
33+
}

Service/ServiceSubscriberTrait.php

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
namespace Symfony\Contracts\Service;
1313

1414
use Psr\Container\ContainerInterface;
15+
use Symfony\Contracts\Service\Attribute\SubscribedService;
1516

1617
/**
1718
* Implementation of ServiceSubscriberInterface that determines subscribed services from
18-
* private method return types. Service ids are available as "ClassName::methodName".
19+
* method return types. Service ids are available as "ClassName::methodName".
1920
*
2021
* @author Kevin Bond <[email protected]>
2122
*/
@@ -36,13 +37,59 @@ public static function getSubscribedServices(): array
3637
}
3738

3839
$services = \is_callable(['parent', __FUNCTION__]) ? parent::getSubscribedServices() : [];
40+
$attributeOptIn = false;
3941

40-
foreach ((new \ReflectionClass(self::class))->getMethods() as $method) {
41-
if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) {
42-
continue;
42+
if (\PHP_VERSION_ID >= 80000) {
43+
foreach ((new \ReflectionClass(self::class))->getMethods() as $method) {
44+
if (self::class !== $method->getDeclaringClass()->name) {
45+
continue;
46+
}
47+
48+
if (!$attribute = $method->getAttributes(SubscribedService::class)[0] ?? null) {
49+
continue;
50+
}
51+
52+
if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) {
53+
throw new \LogicException(sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $method->name));
54+
}
55+
56+
if (!$returnType = $method->getReturnType()) {
57+
throw new \LogicException(sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class));
58+
}
59+
60+
$serviceId = $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType;
61+
62+
if ($returnType->allowsNull()) {
63+
$serviceId = '?'.$serviceId;
64+
}
65+
66+
$services[$attribute->newInstance()->key ?? self::class.'::'.$method->name] = $serviceId;
67+
$attributeOptIn = true;
4368
}
69+
}
70+
71+
if (!$attributeOptIn) {
72+
foreach ((new \ReflectionClass(self::class))->getMethods() as $method) {
73+
if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) {
74+
continue;
75+
}
76+
77+
if (self::class !== $method->getDeclaringClass()->name) {
78+
continue;
79+
}
80+
81+
if (!$returnType = $method->getReturnType()) {
82+
continue;
83+
}
84+
85+
if ($returnType->isBuiltin()) {
86+
continue;
87+
}
88+
89+
if (\PHP_VERSION_ID >= 80000) {
90+
trigger_deprecation('symfony/service-contracts', '2.5', 'Using "%s" in "%s" without using the "%s" attribute on any method is deprecated.', ServiceSubscriberTrait::class, self::class, SubscribedService::class);
91+
}
4492

45-
if (self::class === $method->getDeclaringClass()->name && ($returnType = $method->getReturnType()) && !$returnType->isBuiltin()) {
4693
$services[self::class.'::'.$method->name] = '?'.($returnType instanceof \ReflectionNamedType ? $returnType->getName() : $returnType);
4794
}
4895
}

Tests/Service/ServiceSubscriberTraitTest.php

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,34 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Psr\Container\ContainerInterface;
16+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\Component1\Dir1\Service1;
17+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\Component1\Dir2\Service2;
18+
use Symfony\Contracts\Service\Attribute\SubscribedService;
1619
use Symfony\Contracts\Service\ServiceLocatorTrait;
1720
use Symfony\Contracts\Service\ServiceSubscriberInterface;
1821
use Symfony\Contracts\Service\ServiceSubscriberTrait;
1922

2023
class ServiceSubscriberTraitTest extends TestCase
2124
{
25+
/**
26+
* @group legacy
27+
*/
28+
public function testLegacyMethodsOnParentsAndChildrenAreIgnoredInGetSubscribedServices()
29+
{
30+
$expected = [LegacyTestService::class.'::aService' => '?'.Service2::class];
31+
32+
$this->assertEquals($expected, LegacyChildTestService::getSubscribedServices());
33+
}
34+
35+
/**
36+
* @requires PHP 8
37+
*/
2238
public function testMethodsOnParentsAndChildrenAreIgnoredInGetSubscribedServices()
2339
{
24-
$expected = [TestService::class.'::aService' => '?Symfony\Contracts\Tests\Service\Service2'];
40+
$expected = [
41+
TestService::class.'::aService' => Service2::class,
42+
TestService::class.'::nullableService' => '?'.Service2::class,
43+
];
2544

2645
$this->assertEquals($expected, ChildTestService::getSubscribedServices());
2746
}
@@ -48,18 +67,45 @@ public function setContainer(ContainerInterface $container)
4867
}
4968
}
5069

70+
class LegacyTestService extends ParentTestService implements ServiceSubscriberInterface
71+
{
72+
use ServiceSubscriberTrait;
73+
74+
public function aService(): Service2
75+
{
76+
}
77+
}
78+
79+
class LegacyChildTestService extends LegacyTestService
80+
{
81+
public function aChildService(): Service3
82+
{
83+
}
84+
}
85+
5186
class TestService extends ParentTestService implements ServiceSubscriberInterface
5287
{
5388
use ServiceSubscriberTrait;
5489

90+
#[SubscribedService]
5591
public function aService(): Service2
5692
{
5793
}
94+
95+
#[SubscribedService]
96+
public function nullableService(): ?Service2
97+
{
98+
}
5899
}
59100

60101
class ChildTestService extends TestService
61102
{
103+
#[SubscribedService]
62104
public function aChildService(): Service3
63105
{
64106
}
65107
}
108+
109+
class Service3
110+
{
111+
}

0 commit comments

Comments
 (0)