diff --git a/src/bindings/php/SConscript b/src/bindings/php/SConscript
index fc4c1d1a5a780e592a45a73ff7b9745de8dce618..34728af238c9a1b3ad478737e997921e8a0ff0b8 100644
--- a/src/bindings/php/SConscript
+++ b/src/bindings/php/SConscript
@@ -23,7 +23,7 @@ phplib = phptestenv.Command(os.path.join(phpextprefix, "hammer.so"), libhammer_p
 AlwaysBuild(phplib)
 phpprefix = os.popen("php-config --prefix").read().rstrip()
 phpincl = phptestenv.Command(os.path.join(os.path.join(phpprefix, "etc/conf.d"), "hammer.ini"), "hammer.ini", Copy("$TARGET", "$SOURCE"))
-phptestexec = phptestenv.Command(phptests, [phplib, phpincl], "phpenv exec phpunit -v --debug --include-path " + os.path.dirname(libhammer_php[0].path) +" src/bindings/php/Tests")
+phptestexec = phptestenv.Command(phptests, [phplib, phpincl], "phpenv exec phpunit --include-path " + os.path.dirname(libhammer_php[0].path) +" src/bindings/php/Tests")
 phptest = Alias("testphp", [phptestexec], phptestexec)
 AlwaysBuild(phptest)
 testruns.append(phptest)
diff --git a/src/bindings/php/Tests/PredicateTest.php b/src/bindings/php/Tests/PredicateTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..1df45e273ac1bed5fd8fc0b727e8c0a032c4ec58
--- /dev/null
+++ b/src/bindings/php/Tests/PredicateTest.php
@@ -0,0 +1,31 @@
+<?php
+include_once 'hammer.php';
+
+function predTest($token)
+{
+    return ($token[0] === $token[1]);
+}
+
+class PredicateTest extends PHPUnit_Framework_TestCase
+{
+    protected $parser;
+
+    protected function setUp()
+    {
+        $this->parser = predicate(h_many1(choice(ch('a'), ch('b'))), "predTest");
+    }
+    public function testSuccess()
+    {
+        $result1 = h_parse($this->parser, "aa");
+        $result2 = h_parse($this->parser, "bb");
+        $this->assertEquals(["a", "a"], $result1);
+        $this->assertEquals(["b", "b"], $result2);
+    }
+    public function testFailure()
+    {
+        $result = h_parse($this->parser, "ab");
+        $this->assertEquals(NULL, $result);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/src/bindings/php/hammer.i b/src/bindings/php/hammer.i
index 174cb9806c2f5fdfe0b86a0652f33d091331f425..c9753609b99604de7c04819bfa6e54f7e29c8693 100644
--- a/src/bindings/php/hammer.i
+++ b/src/bindings/php/hammer.i
@@ -4,15 +4,11 @@
 %ignore HCountedArray_;
 
 %inline %{
-#define PHP_H_TT_PHP_DESCRIPTOR_RES_NAME "Hammer Token"
   static int h_tt_php;
-  static int le_h_tt_php_descriptor;
   %}
 
 %init %{
   h_tt_php = h_allocate_token_type("com.upstandinghackers.hammer.php");
-  // TODO: implement h_arena_free, register a token dtor here
-  le_h_tt_php_descriptor = zend_register_list_destructors_ex(NULL, NULL, PHP_H_TT_PHP_DESCRIPTOR_RES_NAME, module_number);	     
   %}
 
 %inline {
@@ -146,9 +142,29 @@
     return tok;
   }
 
+  static int call_predicate(HParseResult *p, void *user_data) {
+    zval *args[1];
+    zval func;
+    zval *ret;
+    ALLOC_INIT_ZVAL(ret);
+    ZVAL_STRING(&func, (const char*)user_data, 0);
+    hpt_to_php(p->ast, args[0]);
+    int ok = call_user_function(EG(function_table), NULL, &func, ret, 1, args TSRMLS_CC);
+    if (ok != SUCCESS) {
+      printf("call_user_function failed\n");
+      // FIXME throw some error
+      return 0;
+    }
+    return Z_LVAL_P(ret);
+  }
+
   HParser* action(HParser *parser, const char *name) {
     return h_action(parser, call_action, (void*)name);
   }
+
+  HParser* predicate(HParser *parser, const char *name) {
+    return h_attr_bool(parser, call_predicate, (void*)name);
+  }
  }
 
 %pragma(php) code="