package fr.adrienbrault.idea.symfony2plugin.form.util; import com.intellij.codeInsight.completion.CompletionResultSet; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.openapi.project.Project; import com.intellij.psi.PsiElement; import com.intellij.psi.impl.source.xml.XmlDocumentImpl; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import com.jetbrains.php.PhpIndex; import com.jetbrains.php.lang.parser.PhpElementTypes; import com.jetbrains.php.lang.psi.elements.*; import com.jetbrains.php.lang.psi.elements.impl.PhpTypedElementImpl; import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons; import fr.adrienbrault.idea.symfony2plugin.Symfony2InterfacesUtil; import fr.adrienbrault.idea.symfony2plugin.form.FormTypeLookup; import fr.adrienbrault.idea.symfony2plugin.form.dict.EnumFormTypeSource; import fr.adrienbrault.idea.symfony2plugin.form.dict.FormTypeClass; import fr.adrienbrault.idea.symfony2plugin.form.dict.FormTypeServiceParser; import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils; import fr.adrienbrault.idea.symfony2plugin.util.psi.PsiElementAssertUtil; import fr.adrienbrault.idea.symfony2plugin.util.service.ServiceXmlParserFactory; import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.yaml.psi.YAMLFile; import org.jetbrains.yaml.psi.YAMLKeyValue; import java.util.*; /** * @author Daniel Espendiller <daniel@espendiller.net> */ public class FormUtil { final public static String ABSTRACT_FORM_INTERFACE = "\\Symfony\\Component\\Form\\FormTypeInterface"; final public static String FORM_EXTENSION_INTERFACE = "\\Symfony\\Component\\Form\\FormTypeExtensionInterface"; @Nullable public static PhpClass getFormTypeToClass(Project project, @Nullable String formType) { return new FormTypeCollector(project).collect().getFormTypeToClass(formType); } public static Collection<LookupElement> getFormTypeLookupElements(Project project) { Collection<LookupElement> lookupElements = new ArrayList<>(); FormUtil.FormTypeCollector collector = new FormUtil.FormTypeCollector(project).collect(); for(Map.Entry<String, FormTypeClass> entry: collector.getFormTypesMap().entrySet()) { String name = entry.getValue().getName(); String typeText = entry.getValue().getPhpClassName(); PhpClass phpClass = entry.getValue().getPhpClass(); if(phpClass != null) { typeText = phpClass.getName(); } FormTypeLookup formTypeLookup = new FormTypeLookup(typeText, name); if(entry.getValue().getSource() == EnumFormTypeSource.INDEX) { formTypeLookup.withWeak(true); } lookupElements.add(formTypeLookup); } return lookupElements; } public static MethodReference[] getFormBuilderTypes(Method method) { final List<MethodReference> methodReferences = new ArrayList<>(); final Symfony2InterfacesUtil symfony2InterfacesUtil = new Symfony2InterfacesUtil(); PsiTreeUtil.collectElements(method, psiElement -> { if (psiElement instanceof MethodReference) { String methodName = ((MethodReference) psiElement).getName(); if (methodName != null && (methodName.equals("add") || methodName.equals("create"))) { if(symfony2InterfacesUtil.isFormBuilderFormTypeCall(psiElement)) { methodReferences.add((MethodReference) psiElement); return true; } } } return false; }); return methodReferences.toArray(new MethodReference[methodReferences.size()]); } /** * $form->get .. */ @Nullable public static PhpClass resolveFormGetterCall(MethodReference methodReference) { // "$form"->get('field_name'); PhpPsiElement variable = methodReference.getFirstPsiChild(); if(!(variable instanceof Variable)) { return null; } // find "$form = $this->createForm" createView call PsiElement variableDecl = ((Variable) variable).resolve(); if(variableDecl == null) { return null; } // $form = "$this->createForm(new Type(), $entity)"; PsiElement assignmentExpression = variableDecl.getParent(); if(!(assignmentExpression instanceof AssignmentExpression)) { return null; } // $form = "$this->"createForm(new Type(), $entity)"; PhpPsiElement calledMethodReference = ((AssignmentExpression) assignmentExpression).getValue(); if(!(calledMethodReference instanceof MethodReference)) { return null; } return getFormTypeClass((MethodReference) calledMethodReference); } public static PhpClass getFormTypeClass(@Nullable MethodReference calledMethodReference) { if(calledMethodReference == null) { return null; } if(new Symfony2InterfacesUtil().isCallTo(calledMethodReference, "\\Symfony\\Component\\Form\\FormFactory", "create")) { return null; } // $form = "$this->createForm("new Type()", $entity)"; PsiElement formType = PsiElementUtils.getMethodParameterPsiElementAt(calledMethodReference, 0); if(formType == null) { return null; } return getFormTypeClassOnParameter(formType); } /** * Get form builder field for * $form->get('field'); */ @Nullable public static Method resolveFormGetterCallMethod(MethodReference methodReference) { PhpClass formPhpClass = FormUtil.resolveFormGetterCall(methodReference); if(formPhpClass == null) { return null; } Method method = formPhpClass.findMethodByName("buildForm"); if (method == null) { return null; } return method; } /** * Get form builder field for * $form->get('field', 'file'); * $form->get('field', new FileType()); */ @Nullable public static PhpClass getFormTypeClassOnParameter(@NotNull PsiElement psiElement) { if(psiElement instanceof StringLiteralExpression) { return getFormTypeToClass(psiElement.getProject(), ((StringLiteralExpression) psiElement).getContents()); } if(psiElement instanceof PhpTypedElementImpl) { String typeName = ((PhpTypedElementImpl) psiElement).getType().toString(); return getFormTypeToClass(psiElement.getProject(), typeName); } if(psiElement instanceof ClassConstantReference) { return PhpElementsUtil.getClassConstantPhpClass((ClassConstantReference) psiElement); } return null; } @NotNull public static Collection<String> getFormAliases(@NotNull PhpClass phpClass) { // check class implements form interface if(!PhpElementsUtil.isInstanceOf(phpClass, ABSTRACT_FORM_INTERFACE)) { return Collections.emptySet(); } String className = FormUtil.getFormNameOfPhpClass(phpClass); if(className == null) { return Collections.emptySet(); } return Collections.singleton(className); } public static void attachFormAliasesCompletions(@NotNull PhpClass phpClass, @NotNull CompletionResultSet completionResultSet) { for(String alias: getFormAliases(phpClass)) { completionResultSet.addElement(LookupElementBuilder.create(alias).withIcon(Symfony2Icons.FORM_TYPE).withTypeText(phpClass.getPresentableFQN(), true)); } } /** * acme_demo.form.type.gender: * class: espend\Form\TypeBundle\Form\FooType * tags: * - { name: form.type, alias: foo_type_alias } * - { name: foo } */ @NotNull public static Map<String, Set<String>> getTags(@NotNull YAMLFile yamlFile) { Map<String, Set<String>> map = new HashMap<>(); for(YAMLKeyValue yamlServiceKeyValue : YamlHelper.getQualifiedKeyValuesInFile(yamlFile, "services")) { String serviceName = yamlServiceKeyValue.getName(); Set<String> serviceTagMap = YamlHelper.collectServiceTags(yamlServiceKeyValue); if(serviceTagMap != null && serviceTagMap.size() > 0) { map.put(serviceName, serviceTagMap); } } return map; } public static Map<String, Set<String>> getTags(XmlFile psiFile) { Map<String, Set<String>> map = new HashMap<>(); XmlDocumentImpl document = PsiTreeUtil.getChildOfType(psiFile, XmlDocumentImpl.class); if(document == null) { return map; } /** * <services> * <service id="espend_form.foo_type" class="%espend_form.foo_type.class%"> * <tag name="form.type" alias="foo_type_alias" /> * </service> * </services> */ XmlTag xmlTags[] = PsiTreeUtil.getChildrenOfType(psiFile.getFirstChild(), XmlTag.class); if(xmlTags == null) { return map; } for(XmlTag xmlTag: xmlTags) { if(xmlTag.getName().equals("container")) { for(XmlTag servicesTag: xmlTag.getSubTags()) { if(servicesTag.getName().equals("services")) { for(XmlTag serviceTag: servicesTag.getSubTags()) { XmlAttribute attrValue = serviceTag.getAttribute("id"); if(attrValue != null) { // <service id="foo.bar" class="Class\Name"> String serviceNameId = attrValue.getValue(); if(serviceNameId != null) { Set<String> serviceTags = getTags(serviceTag); if(serviceTags.size() > 0) { map.put(serviceNameId, serviceTags); } } } } } } } } return map; } public static Set<String> getTags(XmlTag serviceTag) { Set<String> tags = new HashSet<>(); for(XmlTag serviceSubTag: serviceTag.getSubTags()) { if("tag".equals(serviceSubTag.getName())) { XmlAttribute attribute = serviceSubTag.getAttribute("name"); if(attribute != null) { String tagName = attribute.getValue(); if(tagName != null && StringUtils.isNotBlank(tagName)) { tags.add(tagName); } } } } return tags; } @NotNull public static Map<String, FormTypeClass> getFormTypeClasses(@NotNull Project project) { Map<String, FormTypeClass> map = new HashMap<>(); for(PhpClass phpClass: PhpIndex.getInstance(project).getAllSubclasses(ABSTRACT_FORM_INTERFACE)) { if(!isValidFormPhpClass(phpClass)) { continue; } String name = FormUtil.getFormNameOfPhpClass(phpClass); if (name == null) { continue; } map.put(name, new FormTypeClass(name, phpClass, EnumFormTypeSource.INDEX)); } return map; } public static boolean isValidFormPhpClass(PhpClass phpClass) { return !(phpClass.isAbstract() || phpClass.isInterface() || PhpElementsUtil.isTestClass(phpClass)); } public static class FormTypeCollector { private final Map<String, FormTypeClass> formTypesMap; private final Project project; public FormTypeCollector(Project project) { this.project = project; this.formTypesMap = new HashMap<>(); } public FormTypeCollector collect() { // on indexer, compiler wins... formTypesMap.putAll(FormUtil.getFormTypeClasses(project)); // find on registered formtype aliases on compiled container FormTypeServiceParser formTypeServiceParser = ServiceXmlParserFactory.getInstance(project, FormTypeServiceParser.class); for(Map.Entry<String, String> entry: formTypeServiceParser.getFormTypeMap().getMap().entrySet()) { String formTypeName = entry.getValue(); formTypesMap.put(formTypeName, new FormTypeClass(formTypeName, entry.getKey(), EnumFormTypeSource.COMPILER)); } return this; } @Nullable public FormTypeClass getFormType(String formTypeName) { if(this.formTypesMap.containsKey(formTypeName)) { return this.formTypesMap.get(formTypeName); } return null; } @Nullable public PhpClass getFormTypeToClass(@Nullable String formType) { if(formType == null) { return null; } // formtype can also be a direct class name if(formType.contains("\\")) { PhpClass phpClass = PhpElementsUtil.getClass(PhpIndex.getInstance(project), formType); if(phpClass != null) { return phpClass; } } return this.getFormTypeClass(formType); } @Nullable public PhpClass getFormTypeClass(String formTypeName) { // find on registered formtype aliases on compiled container FormTypeClass serviceName = this.formTypesMap.get(formTypeName); // compiled container resolve if(serviceName != null) { PhpClass phpClass = serviceName.getPhpClass(project); if (phpClass != null) { return phpClass; } } // on indexer Map<String, FormTypeClass> forms = FormUtil.getFormTypeClasses(project); if(!forms.containsKey(formTypeName)) { return null; } return forms.get(formTypeName).getPhpClass(); } public Map<String, FormTypeClass> getFormTypesMap() { return formTypesMap; } } /** * Replace "hidden" with HiddenType:class * @throws Exception */ public static void replaceFormStringAliasWithClassConstant(@NotNull StringLiteralExpression psiElement) throws Exception { String contents = psiElement.getContents(); if(StringUtils.isBlank(contents)) { throw new Exception("Empty content"); } PhpClass phpClass = getFormTypeToClass(psiElement.getProject(), contents); if(phpClass == null) { throw new Exception("No class found"); } PhpElementsUtil.replaceElementWithClassConstant(phpClass, psiElement); } /** * Finds form parent by "getParent" method * * Concatenation "__NAMESPACE__.'\Foo', "Foo::class" and string 'foo' supported */ @Nullable public static String getFormParentOfPhpClass(@NotNull PhpClass phpClass) { Method getParent = phpClass.findMethodByName("getParent"); if(getParent == null) { return null; } for (PhpReturn phpReturn : PsiTreeUtil.collectElementsOfType(getParent, PhpReturn.class)) { PhpPsiElement firstPsiChild = phpReturn.getFirstPsiChild(); if(firstPsiChild instanceof StringLiteralExpression) { String contents = ((StringLiteralExpression) firstPsiChild).getContents(); if(StringUtils.isNotBlank(contents)) { return contents; } continue; } // Foo::class if(firstPsiChild instanceof ClassConstantReference) { return PhpElementsUtil.getClassConstantPhpFqn((ClassConstantReference) firstPsiChild); } if(!(firstPsiChild instanceof BinaryExpression) || !PsiElementAssertUtil.isNotNullAndIsElementType(firstPsiChild, PhpElementTypes.CONCATENATION_EXPRESSION)) { continue; } PsiElement leftOperand = ((BinaryExpression) firstPsiChild).getLeftOperand(); ConstantReference constantReference = PsiElementAssertUtil.getInstanceOfOrNull(leftOperand, ConstantReference.class); if(constantReference == null || !"__NAMESPACE__".equals(constantReference.getName())) { continue; } StringLiteralExpression stringValue = PsiElementAssertUtil.getInstanceOfOrNull(((BinaryExpression) firstPsiChild).getRightOperand(), StringLiteralExpression.class); if(stringValue == null) { continue; } String contents = stringValue.getContents(); if(StringUtils.isBlank(contents)) { continue; } return StringUtils.strip(phpClass.getNamespaceName(), "\\") + contents; } return null; } /** * Finds form name by "getName" method * * Symfony < 2.8 * 'foo_bar' * * Symfony 2.8 * "$this->getName()" -> "$this->getBlockPrefix()" -> return 'datetime'; * "UserProfileType" => "user_profile" and namespace removal * * Symfony 3.0 * Use class name: Foo\Class * */ @Nullable public static String getFormNameOfPhpClass(@NotNull PhpClass phpClass) { Method method = phpClass.findOwnMethodByName("getName"); // @TODO: think of interface switches // method not found so use class fqn if(method == null) { String fqn = phpClass.getFQN(); return fqn != null ? StringUtils.stripStart(fqn, "\\") : null; } for (PhpReturn phpReturn : PsiTreeUtil.collectElementsOfType(method, PhpReturn.class)) { PhpPsiElement firstPsiChild = phpReturn.getFirstPsiChild(); // $this->getBlockPrefix() if(firstPsiChild instanceof MethodReference) { PhpExpression classReference = ((MethodReference) firstPsiChild).getClassReference(); if(classReference != null && "this".equals(classReference.getName())) { String name = firstPsiChild.getName(); if(name != null && "getBlockPrefix".equals(name)) { if(phpClass.findOwnMethodByName("getBlockPrefix") != null) { return PhpElementsUtil.getMethodReturnAsString(phpClass, name); } else { // method has no custom overwrite; rebuild expression here: // FooBarType -> foo_bar String className = phpClass.getName(); // strip Type and type if(className.toLowerCase().endsWith("type") && className.length() > 4) { className = className.substring(0, className.length() - 4); } return fr.adrienbrault.idea.symfony2plugin.util.StringUtils.underscore(className); } } } continue; } // string value fallback String stringValue = PhpElementsUtil.getStringValue(firstPsiChild); if(stringValue != null) { return stringValue; } } return null; } /** * Get getExtendedType as string * * 'Foo::class' and string 'foo' supported */ @Nullable public static String getFormExtendedType(@NotNull PhpClass phpClass) { Method getParent = phpClass.findMethodByName(FormOptionsUtil.EXTENDED_TYPE_METHOD); if(getParent == null) { return null; } for (PhpReturn phpReturn : PsiTreeUtil.collectElementsOfType(getParent, PhpReturn.class)) { PhpPsiElement firstPsiChild = phpReturn.getFirstPsiChild(); // Foo::class if(firstPsiChild instanceof ClassConstantReference) { return PhpElementsUtil.getClassConstantPhpFqn((ClassConstantReference) firstPsiChild); } String stringValue = PhpElementsUtil.getStringValue(firstPsiChild); if(stringValue != null) { return stringValue; } } return null; } }