grom358 / pharborist

A PHP library to query and transform source code via tree operations.
GNU General Public License v3.0
44 stars 10 forks source link

Wrote foundational code for the filtering API. #178

Closed phenaproxima closed 9 years ago

phenaproxima commented 10 years ago

This is the foundation/proof-of-concept of the fluent filtering API (rolls off the tongue nicely, doesn't it?), as described, more or less, in #101.

Filters will go in the Filters sub-namespace and implement the Filter interface (which, at the moment, only enforces the __invoke method).

Node now has a filter() method, which is really just a factory method. If it's passed a node class name, it will check that said class implements FilterFactoryInterface (if no class name is passed, it will run this check on itself). If it does, it'll call the class's createFilter() static method, which returns an implementation of the Filter interface. If not, DomainException is thrown.

The filter returned by createFilter() should be configurable using a fluent syntax. It can then be passed to any of the normal filtering and traversal methods.

Filters that extend FilterBase have the concept of an "origin", which is simply the node on which createFilter() was originally called. I haven't actually used this in the proof of concept code, but the idea is to be able to do things like this:

// Assume $node is a ClassNode

// Equivalent to calling $node->is(Filter::isClass('FooClass'))
$node->filter()->name('FooClass')->isMatch();
// Equivalent to $node->find() with a complex callable that filters the ClassMemberListNodes by visibility
$node->filter('\Pharborist\ClassMemberListNode')->visibility('private')->matchDescendants();

That is: create the filter, configure it, then execute the filter on the node which originally created it.

phenaproxima commented 10 years ago

As of my most recent commit, the following three things would be equivalent (although I haven't actually written a filter for ClassNode, so it won't actually work):

// The old way
$node->find(Filter::isClass('Foobaz'));
// The "origin" way, where the filter searches outward from the node which created it
$node->filter('\Pharborist\ClassNode')->name('Foobaz')->find()
// Create a filter directly and pass it around as a callable
$node->find((new ClassFilter)->name('Foobaz'));

The difference is that, with the second way, it'll be possible to add more conditions, like checking specifically for abstract classes or whatever. Configurable filters FTW!

grom358 commented 10 years ago

Also any thoughts on how you will combine filters (like Filter::any etc)?

Also how would you convert where chain filters now, eg:

$node->children(Filter::isInstanceOf('\Pharborist\Types\StatementNode'))
  ->filter(function ($node) { return someTest($node); });

Will it be like so?

$node->filter('\Pharborist\Filters\InstanceOfFilter')
  ->name('\Pharborist\Types\StatementNode')
  ->matchChildren()
  ->filter(new SomeTestFilter())
phenaproxima commented 10 years ago

This isn't in the PoC code, but my intention is that, by their very nature, most filters will be implicitly bound to a node type. ClassNodeFilter, for example -- produced by ClassNode::createFilter() -- searches only for classes, so InstanceOf becomes unnecessary.

So the second example will actually look like this:

// Get StatementNodes which are children of $node.
$node->filter('\Pharborist\Types\StatementNode')->matchChildren()->filter(...)

What would also be neat, somehow, is for separate filters to be chainable:

// Create an "any" chain.
$node->filter('\Pharborist\Objects\ClassNode')->name('Wambooli', 'Foo')->or('\Pharborist\InterfaceNode')->name('Baz')

// Create an "all" chain.
$node->filter('MyNodeType')->name('Whatever')->and('AnotherNodeType')->etc(...)

The or() and and() methods -- I'll call it something else, obviously -- will let you daisy-chain filters together for complex filtering logic comprised of several smaller filters. Very "functional programming".

grom358 commented 10 years ago

CombinatorInterface is nice.. I think thats the strategy pattern in Design pattern lingo.

$filter1 = FunctionFilter::create('hello');
$filter2 = SomeOtherFilter::create('world');
$filter = new AnyCombinator();
$filter->add($filter1)->add($filter2);
$results = $node->find($filter);

Is that the right? for when wanting to combine two different filters?

What about a shorthand something like

abstract class FilterBase implements FilterInterface {
  public function and(FilterInterface $filter) {
    $result = new AnyCombinator();
    $result->add($this)->add($filter);
    return $result;
  }
}

$filter = FunctionFilter::create('hello')->and(SomeOtherFilter::create('world'));

That is FilterBase has and/or methods that take another filter and return a new filter that is combination of the two.

phenaproxima commented 10 years ago

I dig that idea; the only problem is that and and or are reserved keywords in PHP, so we'll need a different name for those builder methods. Any thoughts?

phenaproxima commented 9 years ago

I think this is ready for review. I've merged master back into it, updated all the tests, and made Pharborist\Filter's methods return executable filter objects. It's only upwards from here!

phenaproxima commented 9 years ago

I want to take this back to the drawing board for some re-thinking. Closing, and deleting the branch.