플러그인 확장 준비가 끝났으니 본격 확장을 시작.

PHP framework의 자체 FactoryMethod에 관한 확장에 대한 자료는 구글링을 좀 해보면 이런저런게 나옴.


구글링 결과를 바탕으로,

org.eclipse.php.core.goalEvaluatorFactories 가 적합한 extension point라는 것을 알게 되었고,

예제 소스를 보면서 이리저리 삽질을 해보다가 변수에 할당(Assignment)하는 부분을 hook 해서 내가 원하는 기능을 집어넣는 쪽으로 가닥을 잡음.


아래와 같은 형태의 소스에서 $this->getModel() 이라는 FactoryMethod에서 반환되는 객체를 변수에 할당한 후,

해당 변수에서 -> 하고 ctrl + space를 누르면 content assist 가 나올 수 있게 하는 것이 이번 장의 목표.

<?PHP
// 모델 A
class modelTypeA
{
    public $typeA;
    public function methodTypeA()
    {}
}

// 모델 B
class modelTypeB
{
    public $typeB;
    public function methodTypeB()
    {}
}

// getModel() 이라는 factory method가 있음
class ParentClass
{
    // 'model' 이란 문자열이 생략된 채로 넘겨받은 이름을 조합하여 적절한 객체를 반환하는 factory method
    public function getModel($modelName)
    {
        $className = 'model'.$modelName;
        if (class_exists($className) === true) return new $className;
        else return null;
    }
}

class ChildClass extends ParentClass
{
    public function getCount()
    {
        $modelTypeA = $this->getModel('TypeA');

        return $modelTypeA-> // 여기서 ctrl + space를 눌러도 content assist가 나타나지 않음
    }
}







extension point


생성된 플러그인 페이지의 아래 탭 중 Extensions를 클릭한 후 Add 버튼 클릭.



'Extension Point filter'에 org.eclipse.php.core 라고 입력한 후,

중간의 'Show only extension points from the required plug-ins' 체크 해제하고,

리스트에서 org.eclipse.php.core.goalEvaluatorFactories 선택 후 Finish 클릭.




Yes를 클릭하여 자동으로 org.eclipse.php.core를 dependencies 리스트에 추가.




아래 탭에서 plugin.xml을 클릭한 후 xml 소스코드를 아래와 같이 입력.

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
    <extension
        point="org.eclipse.php.core.goalEvaluatorFactories">
        <factory
            class="bloodguy.evaluator.GoalEvaluatorFactory"
            priority="100">
        </factory>
    </extension>
</plugin>














dependency


이 플러그인에서 사용하기 위한 플러그인을 추가하기 위해 아래 탭의 Dependencies 선택 후 Add 클릭.




org.eclipse.dltk.core 라고 입력한 후 org.eclipse.dltk.core 선택 후 OK 클릭.














package/class


좌측 탭에서 project 이름을 선택한 후 우클릭하여 New > Package 선택.





package 이름을 아까 plugin.xml 에 입력한 bloodguy.evaluator로 입력하고 Finish 클릭.




추가된 패키지를 선택한 후 우클릭하여 New > Class 선택.




클래스 이름을 plugin.xml에 입력한 GoalEvaluatorFactory로 입력하고 Finish를 클릭하여 java 파일 생성 후 아래의 소스코드 붙여넣고 저장.

package bloodguy.evaluator;

import org.eclipse.dltk.ast.ASTNode;
import org.eclipse.dltk.ast.references.SimpleReference;
import org.eclipse.dltk.ast.references.VariableReference;
import org.eclipse.dltk.ti.IGoalEvaluatorFactory;
import org.eclipse.dltk.ti.goals.ExpressionTypeGoal;
import org.eclipse.dltk.ti.goals.GoalEvaluator;
import org.eclipse.dltk.ti.goals.IGoal;
import org.eclipse.php.internal.core.compiler.ast.nodes.Assignment;
import org.eclipse.php.internal.core.compiler.ast.nodes.PHPCallArgumentsList;
import org.eclipse.php.internal.core.compiler.ast.nodes.PHPCallExpression;
import org.eclipse.php.internal.core.compiler.ast.nodes.Scalar;

@SuppressWarnings("restriction")
public class GoalEvaluatorFactory implements IGoalEvaluatorFactory {
    @Override
    public GoalEvaluator createEvaluator(IGoal goal) {
        Class<?> goalClass = goal.getClass();
        // goal이 ExpressionTypeGoal이 아니라면 중지
        if (goalClass != ExpressionTypeGoal.class) return null;

        ASTNode expression = ((ExpressionTypeGoal)goal).getExpression();
        // expression이 변수할당이 아니라면 중지
        if (!(expression instanceof Assignment)) return null;

        // expression이 Assignment라면 getChilds() 해보면 아래처럼 2개의 child가 있음.
        // System.out.println(expression.getChilds().toString()); 해보면 알 수 있음.
        // 1st child = VariableReference
        //ASTNode vr = (VariableReference)expression.getChilds().get(0);
        // 2nd child = assignment expression
        ASTNode exp = expression.getChilds().get(1);

        // PHPCallExpression이 아니라면 중지
        if (!(exp instanceof PHPCallExpression)) return null;

        PHPCallExpression ce = (PHPCallExpression)exp;
        int numChilds = ce.getChilds().size();

        // $this->getModel('ModelName') 형태의 PHPCallExpression의 경우 numChilds가 3
        if (numChilds != 3) return null;

        // $this / getModel / 'ModelName' 이 아래처럼 3개의 child로 각각 존재함.
        // VariableReference = $this
        ASTNode caller = ce.getChilds().get(0);
        // SimpleReference = getModel
        ASTNode func = ce.getChilds().get(1);
        // PHPCallArgumentsList = 'ModelName'
        ASTNode args = ce.getChilds().get(2);

        // type check
        if (!(caller instanceof VariableReference) || !(func instanceof SimpleReference) || !(args instanceof PHPCallArgumentsList)) return null;

        // caller check
        String callerName = ((VariableReference)caller).getName();
        if (!callerName.equals("$this")) return null;

        // method check
        String methodName = ((SimpleReference)func).getName();
        if (!methodName.equals("getModel")) return null;

        // arg check
        if (((PHPCallArgumentsList)args).getChilds().size() != 1) return null;

        // class name
        Scalar arg = (Scalar)args.getChilds().get(0);
        if (!arg.getType().equals("string")) return null;

        // 매개변수로 넘어오는 'ModelName'의 경우 class 이름으로 만들기 위해 따옴표를 걷어내야 함
        String className = arg.getValue().replaceAll("['\"]", "").trim();
        if (className.isEmpty()) return null;

        className = "model"+className;
        return new bloodguy.evaluator.BloodguyGoalEvaluator(goal, className);
    }
}





factory는 완료되었고 이 factory에서 반환할 실제 goalEvaluator를 만들어야 함.

GoalEvaluatorFactory 클래스를 만들었던 것과 동일한 방식으로 BloodguyGoalEvaluator 클래스를 만들고 아래 소스코드를 붙여넣고 저장.

package bloodguy.evaluator;

import org.eclipse.dltk.ti.GoalState;
import org.eclipse.dltk.ti.goals.GoalEvaluator;
import org.eclipse.dltk.ti.goals.IGoal;
import org.eclipse.php.internal.core.typeinference.PHPClassType;

@SuppressWarnings("restriction")
public class BloodguyGoalEvaluator extends GoalEvaluator {
    private String className;

    /**
     * 생성자
     * 매개변수로 content assist할 클래스 이름을 받아 멤버변수로 저장
     * @param goal
     * @param className
     */
    public BloodguyGoalEvaluator(IGoal goal, String className) {
        super(goal);
        this.className = className;
    }

    /**
     * content assist시 호출되는 함수. 저장하고 있던 클래스 이름으로 PHPClassType 객체생성 후 반환
     * @return PHPClassType
     */
    public Object produceResult() {
        return new PHPClassType(className);
    }

    /**
     * 그냥 인터페이스 맞추기
     */
    public IGoal[] init() {
        return null;
    }

    /**
     * 그냥 인터페이스 맞추기
     */
    public IGoal[] subGoalDone(IGoal subgoal, Object result, GoalState state) {
        return null;
    }
}











run


이제 테스트를 하기 위해 실행을 해봄.

상단의 Run 버튼 클릭.




Run As 에서 Eclipse Application을 선택하고 OK 클릭.




현재 플러그인이 포함된 상태의 이클립스가 하나 더 실행됨.

Welcome 페이지는 끄고,

File > New > Project 선택,

PHP > PHP Project 선택 후 Next 클릭.

Project name은 아무거나 넣고, ex) bloodguy_test

Contents는 Create project at existing location 선택 후 C:\workspace\bloodguy_test 경로로 생성.

생성된 PHP 프로젝트에 새 php 파일을 하나 추가하고 현재 페이지 최상단의 php 소스코드를 넣은 후,

return $modelTypeA-> 부분에서 ctrl + space를 눌러보면 content assist가 나타나는 것을 확인 가능.






혹시나 디버깅용으로 System.out.println()을 찍어놨을 경우 새로 뜬 이클립스가 아니라 작업중이던 이클립스의 console 탭에서 확인 가능함.









jar export


정상작동을 확인했으니 이제 플러그인을 배포(-_-)하기 위해 jar 파일을 만들어야 함.

새로 뜬 이클립스는 끄고, 작업중이던 이클립스 창으로 돌아와서,

project 이름 선택 후 우클릭하여 Export 선택.




Plug-in Development > Deployable plug-ins and fragments 선택 후 Next 클릭.




jar 파일이 생성될 위치를 지정한 후 Finish 클릭.




생성완료 후 지정한 디렉토리로 가서 계속 파고 들어가보면 플러그인 jar 파일이 생성되어 있음을 확인 가능.




이 jar 파일을 실제 사용중인 eclipse PDT 설치 디렉토리의 plugins 디렉토리에 복사하고 해당 PDT를 실행하면 이제 직접 만든 플러그인을 사용할 수 있음.









문제점


제작한 플러그인은 변수할당을 hook 하여 클래스 이름을 맞춰주는 형태로,

FactoryMethod를 통해 할당된 변수에다 -> 한 다음 ctrl + space 하면 content assist도 나오고 ctrl + click 으로 선언부 이동이 가능하나,

$this->getModel('TypeA')-> 하는 식으로 MethodChain 형태에서는 여전히 content assist 기능을 사용할 수 없음.


이 부분에 대한 해결은 다음 장에서.












Posted by bloodguy
,