Notice: This Wiki is now read only and edits are no longer possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.
Extending PDT
Contents
Purpose
There are different purposes for extending PDT. One of them is adding support for specific PHP framework to the IDE features like: Code Assist, Navigation (CTRL + click), Presentation (Outline, PHP Explorer). In this document we'll describe how to achieve these goals using PDT extension points. If you have any questions regarding this document, please ask them on PDT-Dev mailing list.
Extending
Code Assist
Type inference hinting
Suppose your framework uses the following language structure for object instantiation:
$myObject = ClassRegistry::init('MyClass');
In this case PDT type inference engine is unable to detect the type of $myObject variable, so we'll have to add a specific rule that helps him. The following extension point allows to provide additional rules to the PHP type inference engine:
org.eclipse.php.core.goalEvaluatorFactories
For our example what we need to contribute is:
<extension point="org.eclipse.php.core.goalEvaluatorFactories"> <factory class="com.xyz.php.fmwrk.XYZGoalEvaluatorFactory" priority="100"> </factory> </extension>
Please note the priority is set to 100 in order to override the default PHP goal evaluator (its priority is 10).
public class XYZGoalEvaluatorFactory implements IGoalEvaluatorFactory { public GoalEvaluator createEvaluator(IGoal goal) { Class<?> goalClass = goal.getClass(); // We're overriding only the expression type goal: if (goalClass == ExpressionTypeGoal.class) { ASTNode expression = ((ExpressionTypeGoal) goal).getExpression(); // Check the expression AST node type if (expression instanceof StaticMethodInvocation) { StaticMethodInvocation inv = (StaticMethodInvocation) expression; ASTNode reciever = inv.getReceiver(); // Check that the class name is 'CallRegistry': if (reciever instanceof SimpleReference && "ClassRegistry".equals(((SimpleReference) reciever) .getName())) { // Check that the method name is 'init' if ("init".equals(inv.getCallName().getName())) { // Take the first call argument: List arguments = inv.getArgs().getChilds(); if (arguments.size() == 1) { Object first = arguments.get(0); if (first instanceof Scalar && ((Scalar) first).getScalarType() == Scalar.TYPE_STRING) { String className = ((Scalar) first).getValue(); // Return the evaluated type through dummy // evaluator return new DummyGoalEvaluator(goal, className); } } } } } } // Give the control to the default PHP goal evaluator return null; } }
public class DummyGoalEvaluator extends GoalEvaluator { private String className; public DummyGoalEvaluator(IGoal goal, String className) { super(goal); this.className = className; } public Object produceResult() { return new PHPClassType(className); } public IGoal[] init() { return null; } public IGoal[] subGoalDone(IGoal subgoal, Object result, GoalState state) { return null; } }
That's all. In case if 'MyClass' exists Code Assist and Navigation will work out of the box since they are both based on the type inference engine.
Code assist strategies
Some PHP frameworks provide class fields or methods that are not declared explicitly in a code. For instance, Zend Framework view helpers can be accessed from the view class using:
$this->helperName()
Behind the scenes, view loads the Zend_View_Helper_HelperName class (note the naming convention), creates an object instance of it, and calls its helperName() method.
In order to provide Code Assist for "helperName" after "$this->" we'll need to extend the following extensions:
org.eclipse.php.core.completionContextResolvers org.eclipse.php.core.completionStrategyFactories
First of all, we need to define the completion context - a class that verifies that the cursor is positioned after the object call operator and that the object type is a View.
public class XYZCompletionContext extends ClassMemberContext { public boolean isValid(ISourceModule sourceModule, int offset, CompletionRequestor requestor) { // Call to super to verify that cursor is in the class member call // context if (super.isValid(sourceModule, offset, requestor)) { // This context only supports "->" trigger type (not the "::") if (getTriggerType() == Trigger.OBJECT) { IType[] recieverClass = getLhsTypes(); // recieverClass contains types for the expression from the left // side of "->" for (IType c : recieverClass) { if (!isViewer(c)) { return false; } } return true; } } return false; } /** * Check that the type of the class is Viewer */ private boolean isViewer(IType type) { // XXX: add more sophisticated check return "Viewer".equalsIgnoreCase(type.getElementName()); } }
Register new context in the completion engine:
public class XYZContextResolver extends CompletionContextResolver implements ICompletionContextResolver { public ICompletionContext[] createContexts() { return new ICompletionContext[] { new XYZCompletionContext(); }; } }
Add the completion strategy that does the actual job - adds "helperName" method to the completion list:
public class XYZCompletionStrategy extends ClassMembersStrategy implements ICompletionStrategy { public XYZCompletionStrategy(ICompletionContext context) { super(context); } public void apply(ICompletionReporter reporter) throws Exception { XYZCompletionContext context = (XYZCompletionContext) getContext(); IType type = context.getLhsTypes()[0]; for (String helperName : getHelperNames()) { // Create fake model element for the helper method FakeMethod fakeHelperMethod = new FakeMethod((ModelElement) type, helperName); // Report the method to the completion proposals list reporter.reportMethod(fakeHelperMethod, "()", getReplacementRange(context)); } } private String[] getHelperNames() { // XXX: calculate existing helper names from code return new String[] { "helperName" }; } }
Create an association between the completion context and completion strategy:
public class XYZCompletionStrategyFactory implements ICompletionStrategyFactory { public ICompletionStrategy[] create(ICompletionContext[] contexts) { List<ICompletionStrategy> result = new LinkedList<ICompletionStrategy>(); for (ICompletionContext context : contexts) { if (context.getClass() == XYZCompletionContext.class) { result.add(new XYZCompletionStrategy(context)); } } return (ICompletionStrategy[]) result .toArray(new ICompletionStrategy[result.size()]); } }
And, finally, plugin.xml contributions:
<extension point="org.eclipse.php.core.completionStrategyFactories"> <factory class="com.xyz.php.framework.XYZCompletionStrategyFactory"/> </extension> <extension point="org.eclipse.php.core.completionContextResolvers"> <resolver class="com.xyz.php.framework.XYZContextResolver"/> </extension>
CTRL + click
Contributing to index
When you click while holding CTRL pressed on some PHP element in an editor, or when you ask for Code Assist by pressing CTRL+Space selection engine (or completion engine in case of Code Assist) tries to resolve PHP elements by accessing index. Index contains all the PHP element declarations and references that present in a workspace. The following example indexes non-existing select(), delete() and insert() methods from code snippet #1.
Extension point that we gonna use is:
org.eclipse.php.core.phpIndexingVisitors
Implement the indexing visitor:
public class XYZIndexingVisitorExtension extends PhpIndexingVisitorExtension { private ClassDeclaration currentClass; private MethodDeclaration currentMethod; private Set<Scalar> deferredMethods = new HashSet<Scalar>(); public boolean visit(TypeDeclaration s) throws Exception { if (s instanceof ClassDeclaration) { currentClass = (ClassDeclaration) s; } return true; } public boolean endvisit(TypeDeclaration s) throws Exception { for (Scalar method : deferredMethods) { int start = method.sourceStart(); int length = method.sourceEnd() - method.sourceStart(); String name = method.getValue().replaceAll("['\"]", ""); modifyDeclaration(method, new DeclarationInfo(IModelElement.METHOD, Modifiers.AccPublic, start, length, start, length, name, null, null, currentClass.getName())); } currentClass = null; deferredMethods.clear(); return true; } public boolean visit(MethodDeclaration s) throws Exception { if (currentClass != null && "__call".equals(s.getName())) { currentMethod = s; } return true; } public boolean endvisit(MethodDeclaration s) throws Exception { currentMethod = null; return true; } public boolean visit(Statement s) throws Exception { if (s instanceof IfStatement && currentClass != null && currentMethod != null) { Expression condition = ((IfStatement) s).getCondition(); if (condition instanceof InfixExpression) { InfixExpression infixExp = (InfixExpression) condition; if (infixExp.getOperatorType() == InfixExpression.OP_IS_EQUAL) { Expression rightExp = infixExp.getRight(); if (rightExp instanceof Scalar) { Scalar scalar = (Scalar) rightExp; if (scalar.getScalarType() == Scalar.TYPE_STRING) { deferredMethods.add(scalar); } } } } } return true; } }
Note: this visitor is very similar to the structured model contribution visitor.
Finally, register this class in plugin.xml:
<extension point="org.eclipse.php.core.phpIndexingVisitors"> <visitor class="com.xyz.php.framework.XYZIndexingVisitorExtension" /> </extension>
Outline and PHP Explorer
Adding additional nodes to the PHP explorer can be achieved in 2 different ways.
Contributing to the structured model
Sometimes you need to present some elements that don't really exist. For example, you may want to present fields and methods that are processed using __get() or __call() magic methods:
Code snippet #1
/** * Please don't judge me for this code, it's for the sake of example only * (even though I've seen PHP code like this in real PHP applications :-]) */ class MySQL_DB { public function __call($name, $arguments) { if ($name == "select") { mysql_query($this->db, "SELECT * FROM ".$arguments[0]); // ... } else if ($name == "insert") { // ... } else if ($name == "delete") { // ... } } }
Let's say we wish to see select(), insert() and delete() methods in the Outline and PHP Explorer under MySQL_DB class. We'll need to extend the following extension point:
org.eclipse.php.core.phpSourceElementRequestors
plugin.xml contribution:
<extension point="org.eclipse.php.core.phpSourceElementRequestors"> <requestor class="com.xyz.php.framework.XYZSourceElementRequestor"> </requestor> </extension>
Source element requester extension implementation:
public class XYZSourceElementRequestor extends PHPSourceElementRequestorExtension { private ClassDeclaration currentClass; private MethodDeclaration currentMethod; private Set<Scalar> deferredMethods = new HashSet<Scalar>(); public boolean visit(TypeDeclaration s) throws Exception { if (s instanceof ClassDeclaration) { currentClass = (ClassDeclaration) s; } return true; } public boolean endvisit(TypeDeclaration s) throws Exception { currentClass = null; for (Scalar method : deferredMethods) { ISourceElementRequestor.MethodInfo methodInfo = new ISourceElementRequestor.MethodInfo(); methodInfo.name = method.getValue().replaceAll("['\"]", ""); methodInfo.modifiers = Modifiers.AccPublic; methodInfo.nameSourceStart = method.sourceStart(); methodInfo.nameSourceEnd = method.sourceEnd(); methodInfo.declarationStart = method.sourceStart(); fRequestor.enterMethod(methodInfo); fRequestor.exitMethod(method.sourceEnd()); } deferredMethods.clear(); return true; } public boolean visit(MethodDeclaration s) throws Exception { if (currentClass != null && "__call".equals(s.getName())) { currentMethod = s; } return true; } public boolean endvisit(MethodDeclaration s) throws Exception { currentMethod = null; return true; } public boolean visit(Statement s) throws Exception { if (s instanceof IfStatement && currentClass != null && currentMethod != null) { Expression condition = ((IfStatement) s).getCondition(); if (condition instanceof InfixExpression) { InfixExpression infixExp = (InfixExpression) condition; if (infixExp.getOperatorType() == InfixExpression.OP_IS_EQUAL) { Expression rightExp = infixExp.getRight(); if (rightExp instanceof Scalar) { Scalar scalar = (Scalar) rightExp; if (scalar.getScalarType() == Scalar.TYPE_STRING) { deferredMethods.add(scalar); } } } } } return true; } }
Voila! Methods are added into PHP Explorer and Outline views, and even clicking on them changes focus in the editor:
Contributing a PHPTreeContentProvider
Since PDT 3.1.1. additional nodes can be added to the PHP explorer using the org.eclipse.php.ui.phpTreeContentProviders extension point. Let's say you want
to contribute a custom buildpath container using the phpTreeContentProviders extension point:
<extension point="org.eclipse.php.ui.phpTreeContentProviders"> <provider labelProvider="com.dubture.composer.core.ui.explorer.PackageTreeLabelProvider" contentProvider="com.dubture.composer.core.ui.explorer.PackageTreeContentProvider"/> </extension>
All you need to do is to create the labelProvider and contentProvider elements, by implementing an org.eclipse.jface.viewers.ILabelProvider and an org.eclipse.jface.viewers.ITreeContentProvider:
public class PackageTreeLabelProvider extends LabelProvider { @Override public String getText(Object element) { if (element instanceof PackagePath) { PackagePath path = (PackagePath) element; return path.getPackageName(); } return null; } @Override public Image getImage(Object element) { if (element instanceof PackagePath) { return PHPPluginImages .get(PHPPluginImages.IMG_OBJS_LIBRARY); } return null; } }
public class PackageTreeContentProvider extends ScriptExplorerContentProvider { public PackageTreeContentProvider() { super(true); } @Override public Object[] getChildren(Object parentElement) { if (parentElement instanceof PackagePath) { PackagePath pPath = (PackagePath) parentElement; IScriptProject scriptProject = pPath.getProject(); IBuildpathEntry entry = pPath.getEntry(); try { IProjectFragment[] allProjectFragments; allProjectFragments = scriptProject.getAllProjectFragments(); for (IProjectFragment fragment : allProjectFragments) { if (fragment instanceof ExternalProjectFragment) { ExternalProjectFragment external = (ExternalProjectFragment) fragment; if (external.getBuildpathEntry().equals(entry)) { return super.getChildren(external); } } } } catch (ModelException e) { Logger.logException(e); } } else if (parentElement instanceof ComposerBuildpathContainer) { ComposerBuildpathContainer container = (ComposerBuildpathContainer) parentElement; IAdaptable[] children = container.getChildren(); if (children == null || children.length == 0) { return NO_CHILDREN; } return children; } else if (parentElement instanceof IScriptProject) { try { IProject project = ((IScriptProject)parentElement).getProject(); if (project.hasNature(ComposerNature.NATURE_ID)) { return new Object[]{new ComposerBuildpathContainer((IScriptProject) parentElement)}; } } catch (Exception e) { Logger.logException(e); } } return null; } }
The example has been taken from the Composer Eclipse Plugin
Language library contributions
PHP language library contains builtin classes, functions and constants - you can see it under PHP project as a "PHP Language Library" node (see image above). This model is used in Code Assist and PHP Function View. By default, it contains only predefined set of PHP extensions. There's a way to contribute more PHP language fragments to this library. The key extension point is:
org.eclipse.php.core.languageModelProviders
Let's say we need to add PHP-newt extension functions to the Code Assist. First, create the language model provider class:
public class NewtLanguageModelProvider implements ILanguageModelProvider { public IPath getPath(IScriptProject project) { return new Path("resources/newt"); } public Plugin getPlugin() { return MyPlugin.getDefault(); } }
Please not that this interface has been changed since PDT 2.1 according to bug 291729
Register this class in plugin.xml:
<extension point="org.eclipse.php.core.languageModelProviders"> <provider class="com.xyz.php.framework.NewtLanguageModelProvider" /> </extension>
Create resources/newt folder in your plug-in directory, and add this folder to the build.properties, then place PHP stub files under this folder.
Error reporting / validation
Sometimes there's a need to add custom PHP source code validator that reports additional errors or warnings to user. Please learn how to do that from this plugin. This example adds validator that checks PHP code for places possibly vulnerable for XSS attack, and reports warnings to developer.
Providing quickfixes for your validation problems
Showing validation errors is fine, but helping out with a quickfix is even better:
You can use the org.eclipse.php.ui.quickFixProcessors extension point to do this. See the PDT Extensions plugin for an example implementation.