Skip to content

Commit 9dc1c34

Browse files
authored
[container] support subscribed services in child classes (#102)
1 parent e750732 commit 9dc1c34

File tree

3 files changed

+77
-21
lines changed

3 files changed

+77
-21
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ vendor/bin/psalm-plugin enable psalm/plugin-symfony
1212
### Features
1313

1414
- Detect `ContainerInterface::get()` result type. Works better if you [configure](#configuration) compiled container XML file.
15-
- Support [Service Subscribers](https://github.com/psalm/psalm-plugin-symfony/issues/20).
15+
- Support [Service Subscribers](https://github.com/psalm/psalm-plugin-symfony/issues/20). Works only if you [configure](#configuration) compiled container XML file.
1616
- Detect return type of console arguments (`InputInterface::getArgument()`) and options (`InputInterface::getOption()`). Enforces
1717
to use InputArgument and InputOption constants as a part of best practise.
1818
- Detects correct Doctrine repository class if entities are configured with annotations.

src/Handler/ContainerHandler.php

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PhpParser\Node\Expr;
66
use PhpParser\Node\Expr\ClassConstFetch;
7+
use PhpParser\Node\Name;
78
use PhpParser\Node\Scalar\String_;
89
use PhpParser\Node\Stmt\Class_;
910
use PhpParser\Node\Stmt\ClassLike;
@@ -18,6 +19,7 @@
1819
use Psalm\Plugin\Hook\AfterMethodCallAnalysisInterface;
1920
use Psalm\StatementsSource;
2021
use Psalm\Storage\ClassLikeStorage;
22+
use Psalm\Storage\FileStorage;
2123
use Psalm\SymfonyPsalmPlugin\Issue\NamingConventionViolation;
2224
use Psalm\SymfonyPsalmPlugin\Issue\PrivateService;
2325
use Psalm\SymfonyPsalmPlugin\Issue\ServiceNotFound;
@@ -146,30 +148,24 @@ public static function afterClassLikeVisit(
146148
}
147149

148150
// see https://symfony.com/doc/current/service_container/service_subscribers_locators.html
149-
if (self::$containerMeta && $stmt instanceof Class_ && isset($storage->class_implements['symfony\contracts\service\servicesubscriberinterface'])) {
151+
if (self::$containerMeta && $stmt instanceof Class_ && in_array('getsubscribedservices', array_keys($storage->methods))) {
150152
foreach ($stmt->stmts as $classStmt) {
151153
if ($classStmt instanceof ClassMethod && 'getSubscribedServices' === $classStmt->name->name && $classStmt->stmts) {
152154
foreach ($classStmt->stmts as $methodStmt) {
153-
if ($methodStmt instanceof Return_ && ($return = $methodStmt->expr) && $return instanceof Expr\Array_) {
154-
foreach ($return->items as $arrayItem) {
155-
if ($arrayItem instanceof Expr\ArrayItem) {
156-
$value = $arrayItem->value;
157-
if (!$value instanceof Expr\ClassConstFetch) {
158-
continue;
159-
}
160-
161-
/** @var string $className */
162-
$className = $value->class->getAttribute('resolvedName');
163-
164-
$key = $arrayItem->key;
165-
$serviceId = $key instanceof String_ ? $key->value : $className;
166-
167-
$service = new Service($serviceId, $className);
168-
$service->setIsPublic(true);
169-
self::$containerMeta->add($service);
155+
if (!$methodStmt instanceof Return_) {
156+
continue;
157+
}
170158

171-
$codebase->queueClassLikeForScanning($className);
172-
$fileStorage->referenced_classlikes[strtolower($className)] = $className;
159+
$return = $methodStmt->expr;
160+
if ($return instanceof Expr\Array_) {
161+
self::addSubscribedServicesArray($return, $codebase, $fileStorage);
162+
} elseif ($return instanceof Expr\FuncCall) {
163+
$funcName = $return->name;
164+
if ($funcName instanceof Name && in_array('array_merge', $funcName->parts)) {
165+
foreach ($return->args as $arg) {
166+
if ($arg->value instanceof Expr\Array_) {
167+
self::addSubscribedServicesArray($arg->value, $codebase, $fileStorage);
168+
}
173169
}
174170
}
175171
}
@@ -179,6 +175,35 @@ public static function afterClassLikeVisit(
179175
}
180176
}
181177

178+
private static function addSubscribedServicesArray(Expr\Array_ $array, Codebase $codebase, FileStorage $fileStorage): void
179+
{
180+
if (!self::$containerMeta) {
181+
return;
182+
}
183+
184+
foreach ($array->items as $arrayItem) {
185+
if ($arrayItem instanceof Expr\ArrayItem) {
186+
$value = $arrayItem->value;
187+
if (!$value instanceof Expr\ClassConstFetch) {
188+
continue;
189+
}
190+
191+
/** @var string $className */
192+
$className = $value->class->getAttribute('resolvedName');
193+
194+
$key = $arrayItem->key;
195+
$serviceId = $key instanceof String_ ? $key->value : $className;
196+
197+
$service = new Service($serviceId, $className);
198+
$service->setIsPublic(true);
199+
self::$containerMeta->add($service);
200+
201+
$codebase->queueClassLikeForScanning($className);
202+
$fileStorage->referenced_classlikes[strtolower($className)] = $className;
203+
}
204+
}
205+
}
206+
182207
private static function isContainerMethod(string $declaringMethodId, string $methodName): bool
183208
{
184209
return in_array(

tests/acceptance/acceptance/ServiceSubscriber.feature

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,34 @@ Feature: Service Subscriber
6262
| Trace | $entityManager: Doctrine\ORM\EntityManagerInterface |
6363
| Trace | $validator: Symfony\Component\Validator\Validator\ValidatorInterface |
6464
And I see no other errors
65+
66+
67+
Scenario: Asserting psalm recognizes return type of services defined in getSubscribedServices using array_merge
68+
Given I have the following code
69+
"""
70+
<?php
71+
72+
use Doctrine\ORM\EntityManagerInterface;
73+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
74+
75+
class SomeController extends AbstractController
76+
{
77+
public function __invoke()
78+
{
79+
/** @psalm-trace $entityManager */
80+
$entityManager = $this->container->get('custom_service');
81+
}
82+
83+
public static function getSubscribedServices(): array
84+
{
85+
return array_merge([
86+
'custom_service' => EntityManagerInterface::class,
87+
], parent::getSubscribedServices());
88+
}
89+
}
90+
"""
91+
When I run Psalm
92+
Then I see these errors
93+
| Type | Message |
94+
| Trace | $entityManager: Doctrine\ORM\EntityManagerInterface |
95+
And I see no other errors

0 commit comments

Comments
 (0)