Skip to content

Commit 4c83f16

Browse files
authored
Merge pull request Haehnchen#1489 from Haehnchen/feature/index-php-services
index service definition for PHP files
2 parents 5f65015 + 65c6795 commit 4c83f16

File tree

7 files changed

+262
-10
lines changed

7 files changed

+262
-10
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package fr.adrienbrault.idea.symfony2plugin.dic.attribute.value;
2+
3+
import com.intellij.psi.PsiElement;
4+
import org.jetbrains.annotations.NotNull;
5+
import org.jetbrains.annotations.Nullable;
6+
7+
import java.util.Map;
8+
9+
/**
10+
* @author Daniel Espendiller <daniel@espendiller.net>
11+
*/
12+
public class PhpKeyValueAttributeValue extends AttributeValueAbstract {
13+
@NotNull
14+
private final Map<String, String> values;
15+
16+
public PhpKeyValueAttributeValue(@NotNull PsiElement psiElement, @NotNull Map<String, String> values) {
17+
super(psiElement);
18+
this.values = values;
19+
}
20+
21+
@Nullable
22+
@Override
23+
public String getString(@NotNull String key) {
24+
return this.values.get(key);
25+
}
26+
}

src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/container/util/ServiceContainerUtil.java

Lines changed: 122 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import com.intellij.openapi.project.Project;
44
import com.intellij.openapi.util.Key;
5-
import com.intellij.openapi.util.ModificationTracker;
65
import com.intellij.openapi.vfs.VfsUtil;
76
import com.intellij.openapi.vfs.VirtualFile;
87
import com.intellij.patterns.PlatformPatterns;
@@ -14,13 +13,12 @@
1413
import com.intellij.util.Consumer;
1514
import com.intellij.util.indexing.FileBasedIndex;
1615
import com.jetbrains.php.PhpIndex;
17-
import com.jetbrains.php.lang.psi.elements.Field;
18-
import com.jetbrains.php.lang.psi.elements.Method;
19-
import com.jetbrains.php.lang.psi.elements.Parameter;
20-
import com.jetbrains.php.lang.psi.elements.PhpClass;
16+
import com.jetbrains.php.lang.psi.PhpFile;
17+
import com.jetbrains.php.lang.psi.elements.*;
18+
import com.jetbrains.php.refactoring.PhpNamespaceBraceConverter;
2119
import fr.adrienbrault.idea.symfony2plugin.config.xml.XmlHelper;
22-
import fr.adrienbrault.idea.symfony2plugin.config.yaml.YamlElementPatternHelper;
2320
import fr.adrienbrault.idea.symfony2plugin.dic.attribute.value.AttributeValueInterface;
21+
import fr.adrienbrault.idea.symfony2plugin.dic.attribute.value.PhpKeyValueAttributeValue;
2422
import fr.adrienbrault.idea.symfony2plugin.dic.attribute.value.XmlTagAttributeValue;
2523
import fr.adrienbrault.idea.symfony2plugin.dic.attribute.value.YamlKeyValueAttributeValue;
2624
import fr.adrienbrault.idea.symfony2plugin.dic.container.SerializableService;
@@ -41,7 +39,6 @@
4139
import org.jetbrains.yaml.psi.*;
4240

4341
import java.util.*;
44-
import java.util.function.Predicate;
4542
import java.util.stream.Collectors;
4643
import java.util.stream.Stream;
4744

@@ -112,6 +109,11 @@ public static Collection<ServiceSerializable> getServicesInFile(@NotNull PsiFile
112109
serializableService.setIsDeprecated(serviceConsumer.attributes().getBoolean("deprecated"));
113110
}
114111

112+
services.add(serializableService);
113+
});
114+
} else if (psiFile instanceof PhpFile) {
115+
visitFile((PhpFile) psiFile, serviceConsumer -> {
116+
SerializableService serializableService = createService(serviceConsumer);
115117
services.add(serializableService);
116118
});
117119
}
@@ -239,6 +241,119 @@ public static void visitFile(@NotNull XmlFile psiFile, @NotNull Consumer<Service
239241
}
240242
}
241243

244+
@Nullable
245+
private static String getStringValueIndexSafe(@NotNull PsiElement psiElement) {
246+
String serviceClass = null;
247+
if (psiElement instanceof StringLiteralExpression) {
248+
serviceClass = ((StringLiteralExpression) psiElement).getContents();
249+
} else if(psiElement instanceof ClassConstantReference) {
250+
serviceClass = PhpElementsUtil.getClassConstantPhpFqn((ClassConstantReference) psiElement);
251+
}
252+
253+
return StringUtils.isNotBlank(serviceClass)
254+
? serviceClass
255+
: null;
256+
}
257+
258+
/**
259+
* return static function (ContainerConfigurator $container) { ... }
260+
*/
261+
@NotNull
262+
private static Collection<Function> getPhpContainerConfiguratorFunctions(@NotNull PhpFile phpFile) {
263+
Collection<Function> functions = new HashSet<>();
264+
265+
for (PhpNamespace phpNamespace : PhpNamespaceBraceConverter.getAllNamespaces(phpFile)) {
266+
// its used for all service files:
267+
// namespace \Symfony\Component\DependencyInjection\Loader\Configurator { ... }
268+
String fqn = phpNamespace.getFQN();
269+
if (!fqn.equals("\\Symfony\\Component\\DependencyInjection\\Loader\\Configurator")) {
270+
continue;
271+
}
272+
273+
for (PhpReturn phpReturn : PsiTreeUtil.collectElementsOfType(phpNamespace, PhpReturn.class)) {
274+
for (Function function : PsiTreeUtil.collectElementsOfType(phpReturn, Function.class)) {
275+
Parameter parameter = function.getParameter(0);
276+
if (parameter == null) {
277+
continue;
278+
}
279+
280+
// \Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator
281+
if(parameter.getLocalType().getTypes().stream().noneMatch(s -> s.contains("\\ContainerConfigurator"))) {
282+
continue;
283+
}
284+
285+
functions.add(function);
286+
}
287+
}
288+
}
289+
290+
return functions;
291+
}
292+
293+
/**
294+
* Services as PHP definition
295+
*
296+
* "namespace Symfony\Component\DependencyInjection\Loader\Configurator;"
297+
* "..."
298+
* "return static function (ContainerConfigurator $container) { ... }"
299+
*/
300+
public static void visitFile(@NotNull PhpFile phpFile, @NotNull Consumer<ServiceConsumer> consumer) {
301+
for (Function function : getPhpContainerConfiguratorFunctions(phpFile)) {
302+
// we only want "set" and "alias" methods
303+
PsiElement[] methodReferences = PsiTreeUtil.collectElements(
304+
function,
305+
psiElement -> psiElement instanceof MethodReference && ("set".equals(((MethodReference) psiElement).getName()) || "alias".equals(((MethodReference) psiElement).getName()))
306+
);
307+
308+
for (PsiElement psiElement : methodReferences) {
309+
// ->set('translator.default', Translator::class)
310+
if (psiElement instanceof MethodReference && "set".equals(((MethodReference) psiElement).getName())) {
311+
PsiElement[] parameters = ((MethodReference) psiElement).getParameters();
312+
String serviceName = null;
313+
if (parameters.length >= 1) {
314+
serviceName = getStringValueIndexSafe(parameters[0]);
315+
}
316+
317+
if (StringUtils.isBlank(serviceName)) {
318+
continue;
319+
}
320+
321+
Map<String, String> keyValue = new HashMap<>();
322+
if (parameters.length >= 2) {
323+
String serviceClass = getStringValueIndexSafe(parameters[1]);
324+
if (StringUtils.isNotBlank(serviceClass)) {
325+
keyValue.put("class", serviceClass);
326+
}
327+
}
328+
329+
consumer.consume(new ServiceConsumer(psiElement, serviceName, new PhpKeyValueAttributeValue(psiElement, keyValue), ServiceFileDefaults.EMPTY));
330+
}
331+
332+
// ->alias(TranslatorInterface::class, 'translator')
333+
if (psiElement instanceof MethodReference && "alias".equals(((MethodReference) psiElement).getName())) {
334+
PsiElement[] parameters = ((MethodReference) psiElement).getParameters();
335+
String serviceName = null;
336+
if (parameters.length >= 1) {
337+
serviceName = getStringValueIndexSafe(parameters[0]);
338+
}
339+
340+
if (StringUtils.isBlank(serviceName)) {
341+
continue;
342+
}
343+
344+
Map<String, String> keyValue = new HashMap<>();
345+
if (parameters.length >= 2) {
346+
String serviceClass = getStringValueIndexSafe(parameters[1]);
347+
if (StringUtils.isNotBlank(serviceClass)) {
348+
keyValue.put("alias", serviceClass);
349+
}
350+
}
351+
352+
consumer.consume(new ServiceConsumer(psiElement, serviceName, new PhpKeyValueAttributeValue(psiElement, keyValue), ServiceFileDefaults.EMPTY));
353+
}
354+
}
355+
}
356+
}
242357

243358
/**
244359
* Extract default values for services tag scope

src/main/java/fr/adrienbrault/idea/symfony2plugin/stubs/indexes/ServicesDefinitionStubIndex.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.intellij.util.io.DataExternalizer;
88
import com.intellij.util.io.EnumeratorStringDescriptor;
99
import com.intellij.util.io.KeyDescriptor;
10+
import com.jetbrains.php.lang.PhpFileType;
1011
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
1112
import fr.adrienbrault.idea.symfony2plugin.dic.container.ServiceSerializable;
1213
import fr.adrienbrault.idea.symfony2plugin.dic.container.util.ServiceContainerUtil;
@@ -74,7 +75,7 @@ public DataExternalizer<ServiceSerializable> getValueExternalizer() {
7475
@Override
7576
public FileBasedIndex.InputFilter getInputFilter() {
7677
return file ->
77-
file.getFileType() == XmlFileType.INSTANCE || file.getFileType() == YAMLFileType.YML;
78+
file.getFileType() == XmlFileType.INSTANCE || file.getFileType() == YAMLFileType.YML || file.getFileType() == PhpFileType.INSTANCE;
7879
}
7980

8081
@Override
@@ -84,7 +85,7 @@ public boolean dependsOnFileContent() {
8485

8586
@Override
8687
public int getVersion() {
87-
return 4;
88+
return 5;
8889
}
8990

9091
public static boolean isValidForIndex(FileContent inputData, PsiFile psiFile) {
@@ -96,7 +97,7 @@ public static boolean isValidForIndex(FileContent inputData, PsiFile psiFile) {
9697

9798
// container file need to be xml file, eg xsd filetypes are not valid
9899
String extension = inputData.getFile().getExtension();
99-
if(extension == null || !(extension.equalsIgnoreCase("xml") || extension.equalsIgnoreCase("yml") || extension.equalsIgnoreCase("yaml"))) {
100+
if(extension == null || !(extension.equalsIgnoreCase("xml") || extension.equalsIgnoreCase("yml") || extension.equalsIgnoreCase("yaml") || extension.equalsIgnoreCase("php"))) {
100101
return false;
101102
}
102103

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/dic/container/util/ServiceContainerUtilTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.Collection;
2222
import java.util.HashSet;
2323
import java.util.List;
24+
import java.util.function.Predicate;
2425

2526
/**
2627
* @author Daniel Espendiller <daniel@espendiller.net>
@@ -114,6 +115,24 @@ public void testNonDefaultValuesAreSerialized() {
114115
}
115116
}
116117

118+
public void testPhpServicesAreInIndex() {
119+
PsiFile phpFile = myFixture.configureByFile("services.php");
120+
121+
Collection<ServiceSerializable> servicesInFile = ServiceContainerUtil.getServicesInFile(phpFile);
122+
123+
ServiceSerializable translationWarmer = servicesInFile.stream().filter(s -> "translator.default".equals(s.getId())).findFirst().get();
124+
assertEquals(
125+
"Symfony\\Bundle\\FrameworkBundle\\Translation\\Translator",
126+
translationWarmer.getClassName()
127+
);
128+
129+
ServiceSerializable translationReader = servicesInFile.stream().filter(s -> "Symfony\\Contracts\\Translation\\TranslatorInterface".equals(s.getId())).findFirst().get();
130+
assertEquals(
131+
"translator",
132+
translationReader.getAlias()
133+
);
134+
}
135+
117136
public void testThatDefaultValueAreNullAndNotSerialized() {
118137
for (PsiFile psiFile : new PsiFile[]{xmlFile, ymlFile}) {
119138
ServiceInterface bar = ContainerUtil.find(ServiceContainerUtil.getServicesInFile(psiFile), MyStringServiceInterfaceCondition.create("defaults"));
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
4+
5+
use Psr\Container\ContainerInterface;
6+
use Symfony\Bundle\FrameworkBundle\CacheWarmer\TranslationsCacheWarmer;
7+
use Symfony\Bundle\FrameworkBundle\Translation\Translator;
8+
use Symfony\Component\Translation\Dumper\CsvFileDumper;
9+
use Symfony\Component\Translation\Dumper\IcuResFileDumper;
10+
use Symfony\Component\Translation\Dumper\IniFileDumper;
11+
use Symfony\Component\Translation\Dumper\JsonFileDumper;
12+
use Symfony\Component\Translation\Dumper\MoFileDumper;
13+
use Symfony\Component\Translation\Dumper\PhpFileDumper;
14+
use Symfony\Component\Translation\Dumper\PoFileDumper;
15+
use Symfony\Component\Translation\Dumper\QtFileDumper;
16+
use Symfony\Component\Translation\Dumper\XliffFileDumper;
17+
use Symfony\Component\Translation\Dumper\YamlFileDumper;
18+
use Symfony\Component\Translation\Extractor\ChainExtractor;
19+
use Symfony\Component\Translation\Extractor\ExtractorInterface;
20+
use Symfony\Component\Translation\Extractor\PhpExtractor;
21+
use Symfony\Component\Translation\Formatter\MessageFormatter;
22+
use Symfony\Component\Translation\Loader\CsvFileLoader;
23+
use Symfony\Component\Translation\Loader\IcuDatFileLoader;
24+
use Symfony\Component\Translation\Loader\IcuResFileLoader;
25+
use Symfony\Component\Translation\Loader\IniFileLoader;
26+
use Symfony\Component\Translation\Loader\JsonFileLoader;
27+
use Symfony\Component\Translation\Loader\MoFileLoader;
28+
use Symfony\Component\Translation\Loader\PhpFileLoader;
29+
use Symfony\Component\Translation\Loader\PoFileLoader;
30+
use Symfony\Component\Translation\Loader\QtFileLoader;
31+
use Symfony\Component\Translation\Loader\XliffFileLoader;
32+
use Symfony\Component\Translation\Loader\YamlFileLoader;
33+
use Symfony\Component\Translation\LoggingTranslator;
34+
use Symfony\Component\Translation\Reader\TranslationReader;
35+
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
36+
use Symfony\Component\Translation\Writer\TranslationWriter;
37+
use Symfony\Component\Translation\Writer\TranslationWriterInterface;
38+
use Symfony\Contracts\Translation\TranslatorInterface;
39+
40+
return static function (ContainerConfigurator $container) {
41+
$container->services()
42+
->set('translator.default', Translator::class)
43+
->args([
44+
service_locator([]), // translation loaders locator
45+
service('translator.formatter'),
46+
param('kernel.default_locale'),
47+
abstract_arg('translation loaders ids'),
48+
[
49+
'cache_dir' => param('kernel.cache_dir').'/translations',
50+
'debug' => param('kernel.debug'),
51+
],
52+
abstract_arg('enabled locales'),
53+
])
54+
->call('setConfigCacheFactory', [service('config_cache_factory')])
55+
->tag('kernel.locale_aware')
56+
57+
->alias(TranslatorInterface::class, 'translator')
58+
59+
->set('translator.logging', LoggingTranslator::class)
60+
->args([
61+
service('translator.logging.inner'),
62+
service('logger'),
63+
])
64+
->tag('monolog.logger', ['channel' => 'translation'])
65+
66+
->set('translation.warmer', TranslationsCacheWarmer::class)
67+
->args([service(ContainerInterface::class)])
68+
->tag('container.service_subscriber', ['id' => 'translator'])
69+
->tag('kernel.cache_warmer')
70+
;
71+
};

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/stubs/indexes/ServicesDefinitionStubIndexTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public void setUp() throws Exception {
1919
myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("services.xml"));
2020
myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("services.yml"));
2121
myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("services.yaml"));
22+
myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("services.php"));
2223
}
2324

2425
public String getTestDataPath() {
@@ -45,6 +46,11 @@ public void testThatServiceIdOfXmlFileIsIndexed() {
4546
assertEquals(false, getFirstValue("foo.xml_id.private").isPublic());
4647
}
4748

49+
public void testThatServiceIdOfPhpFileIsIndexed() {
50+
assertIndexContains(ServicesDefinitionStubIndex.KEY, "twig.command.debug");
51+
assertIndexContains(ServicesDefinitionStubIndex.KEY, "twig.command.debug_alias");
52+
}
53+
4854
public void testServiceIdOfYmlWithDeprecatedShortcut() {
4955
assertTrue(getFirstValue("foo.yml_id.deprecated_tilde").isDeprecated());
5056
assertTrue(getFirstValue("foo.yml_id.deprecated_bool").isDeprecated());
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
4+
5+
use Symfony\Bridge\Twig\Command\DebugCommand;
6+
use Symfony\Bundle\TwigBundle\Command\LintCommand;
7+
use Symfony\Component\DependencyInjection\Container;
8+
9+
return static function (ContainerConfigurator $container) {
10+
$container->services()
11+
->set('twig.command.debug', DebugCommand::class)
12+
->alias('twig.command.debug_alias', 'foo_alias')
13+
;
14+
};

0 commit comments

Comments
 (0)