Skip to content

Commit

Permalink
Merge pull request #2193 from Haehnchen/feature/twig-attribute-html
Browse files Browse the repository at this point in the history
support "ExposeInTemplate" variables for html attributes of "<twig:>" prefix
  • Loading branch information
Haehnchen committed Jul 2, 2023
2 parents e98bde7 + 9654509 commit eaf4634
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@
import com.intellij.psi.xml.XmlTokenType;
import com.jetbrains.php.lang.psi.elements.Field;
import com.jetbrains.php.lang.psi.elements.PhpClass;
import com.jetbrains.php.lang.psi.elements.PhpNamedElement;
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.routing.Route;
import fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper;
import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigHtmlCompletionUtil;
import fr.adrienbrault.idea.symfony2plugin.util.UxUtil;
import kotlin.Pair;
import org.apache.commons.lang.StringUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Consumer;

/**
* @author Daniel Espendiller <[email protected]>
Expand Down Expand Up @@ -87,10 +90,11 @@ public PsiElement[] getGotoDeclarationTargets(PsiElement psiElement, int offset,
Project project = psiElement.getProject();

for (PhpClass phpClass : UxUtil.getTwigComponentNameTargets(project, htmlTag.getName().substring(5))) {
Field fieldByName = phpClass.findFieldByName(StringUtils.stripStart(text, ":"), false);
if (fieldByName != null) {
targets.add(fieldByName);
}
UxUtil.visitComponentVariables(phpClass, pair -> {
if (pair.getFirst().equals(StringUtils.stripStart(text, ":"))) {
targets.add(pair.getSecond());
}
});
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.ProcessingContext;
import com.jetbrains.php.lang.psi.elements.PhpClass;
import com.jetbrains.php.lang.psi.elements.PhpNamedElement;
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.asset.AssetDirectoryReader;
Expand All @@ -26,7 +27,6 @@
import fr.adrienbrault.idea.symfony2plugin.util.UxUtil;
import org.jetbrains.annotations.NotNull;

import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -184,7 +184,9 @@ protected void addCompletions(@NotNull CompletionParameters parameters, @NotNull
}

for (PhpClass phpClass : UxUtil.getTwigComponentNameTargets(position.getProject(), parentOfType.getName().substring(5))) {
Arrays.stream(phpClass.getOwnFields()).filter(field -> field.getModifier().isPublic()).forEach(field -> {
UxUtil.visitComponentVariables(phpClass, pair -> {
PhpNamedElement field = pair.getSecond();

LookupElementBuilder element = LookupElementBuilder
.create(field.getName())
.withIcon(Symfony2Icons.SYMFONY)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
package fr.adrienbrault.idea.symfony2plugin.twig.variable.collector;

import com.intellij.psi.PsiFile;
import com.jetbrains.php.lang.psi.elements.*;
import com.jetbrains.php.lang.psi.elements.PhpClass;
import com.jetbrains.twig.TwigFile;
import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigTypeResolveUtil;
import fr.adrienbrault.idea.symfony2plugin.templating.variable.TwigFileVariableCollector;
import fr.adrienbrault.idea.symfony2plugin.templating.variable.TwigFileVariableCollectorParameter;
import fr.adrienbrault.idea.symfony2plugin.templating.variable.dict.PsiVariable;
import fr.adrienbrault.idea.symfony2plugin.util.PhpPsiAttributesUtil;
import fr.adrienbrault.idea.symfony2plugin.util.UxUtil;
import org.jetbrains.annotations.NotNull;

import java.util.Collection;
import java.util.HashSet;
import java.util.Map;

/**
Expand All @@ -22,72 +18,14 @@
* @author Daniel Espendiller <[email protected]>
*/
public class UxComponentVariableCollector implements TwigFileVariableCollector {
private static final String ATTRIBUTE_EXPOSE_IN_TEMPLATE = "\\Symfony\\UX\\TwigComponent\\Attribute\\ExposeInTemplate";

public void collectPsiVariables(@NotNull TwigFileVariableCollectorParameter parameter, @NotNull Map<String, PsiVariable> variables) {
PsiFile psiFile = parameter.getElement().getContainingFile();
if (!(psiFile instanceof TwigFile)) {
return;
}

for (PhpClass phpClass : UxUtil.getComponentClassesForTemplateFile(parameter.getProject(), psiFile)) {
for (Field field : phpClass.getFields()) {
if (field.getModifier().isPublic()) {
for (String name : getExposeName(field)) {
variables.put(name, new PsiVariable(field.getType().getTypes(), field));
}
}

if (field.getModifier().isPrivate() && field.getAttributes(ATTRIBUTE_EXPOSE_IN_TEMPLATE).size() > 0) {
for (String name : getExposeName(field)) {
variables.put(name, new PsiVariable(field.getType().getTypes(), field));
}
}
}

for (Method method : phpClass.getMethods()) {
if (method.getAccess().isPublic() && method.getAttributes(ATTRIBUTE_EXPOSE_IN_TEMPLATE).size() > 0) {
for (String name : getExposeName(method)) {
variables.put(name, new PsiVariable(method.getType().getTypes(), method));
}
}
}
UxUtil.visitComponentVariables(phpClass, pair -> variables.put(pair.getFirst(), new PsiVariable(pair.getSecond().getType().getTypes(), pair.getSecond())));
}
}

private Collection<String> getExposeName(@NotNull PhpAttributesOwner phpAttributesOwner) {
Collection<String> names = new HashSet<>();

// public state
Collection<@NotNull PhpAttribute> attributes = phpAttributesOwner.getAttributes(ATTRIBUTE_EXPOSE_IN_TEMPLATE);
if (attributes.size() == 0) {
String name = phpAttributesOwner.getName();

if (phpAttributesOwner instanceof Method method) {
names.add(TwigTypeResolveUtil.getPropertyShortcutMethodName(method));
} else {
names.add(name);
}

return names;
}

// attributes given
for (PhpAttribute attribute : attributes) {
String name = PhpPsiAttributesUtil.getAttributeValueByNameAsStringWithDefaultParameterFallback(attribute, "name");
if (name != null && !name.isBlank()) {
names.add(name);
break;
}

if (phpAttributesOwner instanceof Method method) {
// public function getActions(): array // available as `{{ actions }}`
names.add(TwigTypeResolveUtil.getPropertyShortcutMethodName(method));
} else {
names.add(name);
}
}

return names;
}
}
68 changes: 64 additions & 4 deletions src/main/java/fr/adrienbrault/idea/symfony2plugin/util/UxUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@
import com.intellij.psi.util.CachedValue;
import com.intellij.util.indexing.FileBasedIndex;
import com.jetbrains.php.lang.psi.PhpFile;
import com.jetbrains.php.lang.psi.elements.Field;
import com.jetbrains.php.lang.psi.elements.PhpAttribute;
import com.jetbrains.php.lang.psi.elements.PhpClass;
import com.jetbrains.php.lang.psi.elements.PhpNamedElement;
import com.jetbrains.php.lang.psi.elements.*;
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
import fr.adrienbrault.idea.symfony2plugin.stubs.cache.FileIndexCaches;
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.UxTemplateStubIndex;
import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigTypeResolveUtil;
import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigUtil;
import fr.adrienbrault.idea.symfony2plugin.templating.variable.dict.PsiVariable;
import kotlin.Pair;
Expand All @@ -33,6 +31,7 @@
*/
public class UxUtil {
private static final String AS_TWIG_COMPONENT = "\\Symfony\\UX\\TwigComponent\\Attribute\\AsTwigComponent";
private static final String ATTRIBUTE_EXPOSE_IN_TEMPLATE = "\\Symfony\\UX\\TwigComponent\\Attribute\\ExposeInTemplate";

private static final Key<CachedValue<Set<String>>> TWIG_COMPONENTS = new Key<>("SYMFONY_TWIG_COMPONENTS");

Expand Down Expand Up @@ -118,4 +117,65 @@ public static Collection<LookupElement> getComponentLookupElements(@NotNull Proj
)
.collect(Collectors.toList());
}

public static void visitComponentVariables(@NotNull PhpClass phpClass, @NotNull Consumer<Pair<String, PhpNamedElement>> consumer) {
for (Field field : phpClass.getFields()) {
if (field.getModifier().isPublic()) {
for (String name : getExposeName(field)) {
consumer.accept(new Pair<>(name, field));
}
}

if (field.getModifier().isPrivate() && field.getAttributes(ATTRIBUTE_EXPOSE_IN_TEMPLATE).size() > 0) {
for (String name : getExposeName(field)) {
consumer.accept(new Pair<>(name, field));
}
}
}

for (Method method : phpClass.getMethods()) {
if (method.getAccess().isPublic() && method.getAttributes(ATTRIBUTE_EXPOSE_IN_TEMPLATE).size() > 0) {
for (String name : getExposeName(method)) {
consumer.accept(new Pair<>(name, method));
}
}
}
}


private static Collection<String> getExposeName(@NotNull PhpAttributesOwner phpAttributesOwner) {
Collection<String> names = new HashSet<>();

// public state
Collection<@NotNull PhpAttribute> attributes = phpAttributesOwner.getAttributes(ATTRIBUTE_EXPOSE_IN_TEMPLATE);
if (attributes.size() == 0) {
String name = phpAttributesOwner.getName();

if (phpAttributesOwner instanceof Method method) {
names.add(TwigTypeResolveUtil.getPropertyShortcutMethodName(method));
} else {
names.add(name);
}

return names;
}

// attributes given
for (PhpAttribute attribute : attributes) {
String name = PhpPsiAttributesUtil.getAttributeValueByNameAsStringWithDefaultParameterFallback(attribute, "name");
if (name != null && !name.isBlank()) {
names.add(name);
break;
}

if (phpAttributesOwner instanceof Method method) {
// public function getActions(): array // available as `{{ actions }}`
names.add(TwigTypeResolveUtil.getPropertyShortcutMethodName(method));
} else {
names.add(phpAttributesOwner.getName());
}
}

return names;
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
package fr.adrienbrault.idea.symfony2plugin.tests.util;

import com.jetbrains.php.PhpIndex;
import com.jetbrains.php.lang.PhpFileType;
import com.jetbrains.php.lang.psi.PhpFile;
import com.jetbrains.php.lang.psi.PhpPsiElementFactory;
import com.jetbrains.php.lang.psi.elements.Field;
import com.jetbrains.php.lang.psi.elements.Method;
import com.jetbrains.php.lang.psi.elements.PhpClass;
import com.jetbrains.php.lang.psi.elements.PhpNamedElement;
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
import fr.adrienbrault.idea.symfony2plugin.util.UxUtil;
import kotlin.Pair;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;

/**
* @author Daniel Espendiller <[email protected]>
*/
public class UxUtilTest extends SymfonyLightCodeInsightFixtureTestCase {

public void setUp() throws Exception {
super.setUp();
myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("UxUtil.php"));
}

public String getTestDataPath() {
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/util/fixtures";
}

public void testVisitAsTwigComponent() {
PhpFile phpFile = (PhpFile) PhpPsiElementFactory.createPsiFileFromText(getProject(), "<?php\n" +
"namespace App\\Components;\n" +
Expand Down Expand Up @@ -67,4 +83,21 @@ public void testGetTwigComponentNameTarget() {
UxUtil.getTwigComponentNameTargets(getProject(), "Alert").iterator().next().getFQN()
);
}

public void testVisitComponentVariables() {
Collection<PhpClass> anyByFQN = PhpIndex.getInstance(getProject()).getAnyByFQN("\\App\\Alert");

Map<String, PhpNamedElement> map = new HashMap<>();
UxUtil.visitComponentVariables(anyByFQN.iterator().next(), pair -> map.put(pair.getFirst(), pair.getSecond()));

assertTrue(map.get("message") instanceof Field);
assertTrue(map.get("ico") instanceof Field);
assertTrue(map.get("dismissable") instanceof Method);
assertTrue(map.get("actions") instanceof Method);
assertTrue(map.get("alert_type") instanceof Field);assertTrue(map.get("alert_type") instanceof Field);

assertFalse(map.containsKey("notPublicField"));
assertFalse(map.containsKey("notPrivateMethod"));
assertFalse(map.containsKey("notExposedPublicMethod"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace App
{
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;

#[AsTwigComponent]
class Alert
{
#[ExposeInTemplate]
private string $message; // available as `{{ message }}` in the template

#[ExposeInTemplate('alert_type')]
private string $type = 'success'; // available as `{{ alert_type }}` in the template

#[ExposeInTemplate(name: 'ico', getter: 'fetchIcon')]
private string $icon = 'ico-warning'; // available as `{{ ico }}` in the template using `fetchIcon()` as the getter

private string $notPublicField = 'test';

/**
* Required to access $this->message
*/
public function getMessage(): string
{
return $this->message;
}

/**
* Required to access $this->type
*/
public function getType(): string
{
return $this->type;
}

/**
* Required to access $this->icon
*/
public function fetchIcon(): string
{
return $this->icon;
}

#[ExposeInTemplate]
public function getActions(): array // available as `{{ actions }}` in the template
{
}

#[ExposeInTemplate('dismissable')]
public function canBeDismissed(): bool // available as `{{ dismissable }}` in the template
{
}

private function notPrivateMethod(): array {}
public function notExposedPublicMethod(): array {}
}

}

0 comments on commit eaf4634

Please sign in to comment.