Skip to main content
Jump to: navigation, search

Extending PDT

Revision as of 09:52, 30 November 2009 by Spektom.gmail.com (Talk | contribs) (Contributing to model)

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.

org.eclipse.php.core.goalEvaluatorFactories extension point allows to provide additional rules to the PHP type inference engine. For our example what we'll 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

Outline and PHP Explorer

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:

  /**
   * 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.

Back to the top