Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

ENHANCEMENT Allow ClassManifest to handle classes with namespaces, or…

… that extend classes in namspaces or that implement interfaces in namespaces.
  • Loading branch information...
commit 3e6a91a07fd272523043e79580696f54f0009c8f 1 parent bad1b88
@simonwelsh simonwelsh authored
View
119 core/manifest/ClassManifest.php
@@ -40,19 +40,59 @@ public static function get_class_parser() {
3 => T_WHITESPACE,
4 => T_EXTENDS,
5 => T_WHITESPACE,
- 6 => array(T_STRING, 'save_to' => 'extends', 'can_jump_to' => 14),
+ 6 => array(T_STRING, 'save_to' => 'extends[]', 'can_jump_to' => 14),
7 => T_WHITESPACE,
8 => T_IMPLEMENTS,
9 => T_WHITESPACE,
10 => array(T_STRING, 'can_jump_to' => 14, 'save_to' => 'interfaces[]'),
11 => array(T_WHITESPACE, 'optional' => true),
- 12 => array(',', 'can_jump_to' => 10),
+ 12 => array(',', 'can_jump_to' => 10, 'save_to' => 'interfaces[]'),
13 => array(T_WHITESPACE, 'can_jump_to' => 10),
14 => array(T_WHITESPACE, 'optional' => true),
15 => '{',
));
}
-
+
+ /**
+ * @return TokenisedRegularExpression
+ */
+ public static function get_namespaced_class_parser() {
+ return new TokenisedRegularExpression(array(
+ 0 => T_CLASS,
+ 1 => T_WHITESPACE,
+ 2 => array(T_STRING, 'can_jump_to' => array(8, 16), 'save_to' => 'className'),
+ 3 => T_WHITESPACE,
+ 4 => T_EXTENDS,
+ 5 => T_WHITESPACE,
+ 6 => array(T_NS_SEPARATOR, 'save_to' => 'extends[]', 'optional' => true),
+ 7 => array(T_STRING, 'save_to' => 'extends[]', 'can_jump_to' => array(6, 16)),
+ 8 => T_WHITESPACE,
+ 9 => T_IMPLEMENTS,
+ 10 => T_WHITESPACE,
+ 11 => array(T_NS_SEPARATOR, 'save_to' => 'interfaces[]', 'optional' => true),
+ 12 => array(T_STRING, 'can_jump_to' => array(11, 16), 'save_to' => 'interfaces[]'),
+ 13 => array(T_WHITESPACE, 'optional' => true),
+ 14 => array(',', 'can_jump_to' => 11, 'save_to' => 'interfaces[]'),
+ 15 => array(T_WHITESPACE, 'can_jump_to' => 11),
+ 16 => array(T_WHITESPACE, 'optional' => true),
+ 17 => '{',
+ ));
+ }
+
+ /**
+ * @return TokenisedRegularExpression
+ */
+ public static function get_namespace_parser() {
+ return new TokenisedRegularExpression(array(
+ 0 => T_NAMESPACE,
+ 1 => T_WHITESPACE,
+ 2 => array(T_NS_SEPARATOR, 'save_to' => 'namespaceName[]', 'optional' => true),
+ 3 => array(T_STRING, 'save_to' => 'namespaceName[]', 'can_jump_to' => 2),
+ 4 => array(T_WHITESPACE, 'optional' => true),
+ 5 => ';',
+ ));
+ }
+
/**
* @return TokenisedRegularExpression
*/
@@ -63,7 +103,7 @@ public static function get_interface_parser() {
2 => array(T_STRING, 'save_to' => 'interfaceName')
));
}
-
+
/**
* Constructs and initialises a new class manifest, either loading the data
* from the cache or re-scanning for classes.
@@ -228,7 +268,7 @@ public function regenerate($cache = true) {
'classes', 'roots', 'children', 'descendants', 'interfaces',
'implementors', 'configs'
);
-
+
// Reset the manifest so stale info doesn't cause errors.
foreach ($reset as $reset) {
$this->$reset = array();
@@ -267,6 +307,7 @@ public function handleFile($basename, $pathname, $depth) {
$classes = null;
$interfaces = null;
+ $namespace = null;
// The results of individual file parses are cached, since only a few
// files will have changed and TokenisedRegularExpression is quite
@@ -277,29 +318,47 @@ public function handleFile($basename, $pathname, $depth) {
if ($data = $this->cache->load($key)) {
$valid = (
- isset($data['classes']) && isset($data['interfaces'])
- && is_array($data['classes']) && is_array($data['interfaces'])
+ isset($data['classes']) && isset($data['interfaces']) && isset($data['namespace'])
+ && is_array($data['classes']) && is_array($data['interfaces']) && is_array($data['namespace'])
);
if ($valid) {
$classes = $data['classes'];
$interfaces = $data['interfaces'];
+ $namespace = $data['namespace'];
}
}
-
+
if (!$classes) {
$tokens = token_get_all($file);
- $classes = self::get_class_parser()->findAll($tokens);
+ if(version_compare(PHP_VERSION, '5.3', '>=')) {
+ $classes = self::get_namespaced_class_parser()->findAll($tokens);
+ $namespace = self::get_namespace_parser()->findAll($tokens);
+ if($namespace) {
+ $namespace = implode('', $namespace[0]['namespaceName']) . '\\';
+ } else {
+ $namespace = '';
+ }
+ } else {
+ $classes = self::get_class_parser()->findAll($tokens);
+ $namespace = '';
+ }
$interfaces = self::get_interface_parser()->findAll($tokens);
- $cache = array('classes' => $classes, 'interfaces' => $interfaces);
+ $cache = array('classes' => $classes, 'interfaces' => $interfaces, 'namespace' => $namespace);
$this->cache->save($cache, $key, array('fileparse'));
}
foreach ($classes as $class) {
- $name = $class['className'];
- $extends = isset($class['extends']) ? $class['extends'] : null;
+ $name = $namespace . $class['className'];
+ $extends = isset($class['extends']) ? implode('', $class['extends']) : null;
$implements = isset($class['interfaces']) ? $class['interfaces'] : null;
+
+ if($extends && $extends[0] != '/\\') {
+ $extends = $namespace . $extends;
+ } elseif($extends) {
+ $extends = substr($extends, 1);
+ }
if (array_key_exists($name, $this->classes)) {
throw new Exception(sprintf(
@@ -321,20 +380,34 @@ public function handleFile($basename, $pathname, $depth) {
} else {
$this->roots[] = $name;
}
-
- if ($implements) foreach ($implements as $interface) {
- $interface = strtolower($interface);
-
- if (!isset($this->implementors[$interface])) {
- $this->implementors[$interface] = array($name);
- } else {
- $this->implementors[$interface][] = $name;
+
+ if ($implements) {
+ $interface = $namespace;
+ for($i = 0; $i < count($implements); ++$i) {
+ if($implements[$i] == ',') {
+ $interface = $namespace;
+ continue;
+ }
+ if($implements[$i] == '\\' && $interface == $namespace) {
+ $interface = '';
+ } else {
+ $interface .= $implements[$i];
+ }
+ if($i == count($implements)-1 || $implements[$i+1] == ',') {
+ $interface = strtolower($interface);
+
+ if (!isset($this->implementors[$interface])) {
+ $this->implementors[$interface] = array($name);
+ } else {
+ $this->implementors[$interface][] = $name;
+ }
+ }
}
}
}
-
+
foreach ($interfaces as $interface) {
- $this->interfaces[strtolower($interface['interfaceName'])] = $pathname;
+ $this->interfaces[strtolower($namespace . $interface['interfaceName'])] = $pathname;
}
}
@@ -348,7 +421,7 @@ public function handleFile($basename, $pathname, $depth) {
protected function coalesceDescendants($class) {
$result = array();
$lClass = strtolower($class);
-
+
if (array_key_exists($lClass, $this->children)) {
$this->descendants[$lClass] = array();
View
114 tests/core/manifest/NamespacedClassManifestTest.php
@@ -0,0 +1,114 @@
+<?php
+/**
+ * Tests for the {@link SS_ClassManifest} class.
+ *
+ * @package sapphire
+ * @subpackage tests
+ */
+class NamespacedClassManifestTest extends SapphireTest {
+
+ protected $base;
+ protected $manifest;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->base = dirname(__FILE__) . '/fixtures/namespaced_classmanifest';
+ $this->manifest = new SS_ClassManifest($this->base, false, true, false);
+ }
+
+ public function testGetItemPath() {
+ $expect = array(
+ 'SAPPHIRE\TEST\CLASSA' => 'module/classes/ClassA.php',
+ 'Sapphire\Test\ClassA' => 'module/classes/ClassA.php',
+ 'sapphire\test\classa' => 'module/classes/ClassA.php',
+ 'SAPPHIRE\TEST\INTERFACEA' => 'module/interfaces/InterfaceA.php',
+ 'Sapphire\Test\InterfaceA' => 'module/interfaces/InterfaceA.php',
+ 'sapphire\test\interfacea' => 'module/interfaces/InterfaceA.php'
+ );
+
+ foreach ($expect as $name => $path) {
+ $this->assertEquals("{$this->base}/$path", $this->manifest->getItemPath($name));
+ }
+ }
+
+ public function testGetClasses() {
+ $expect = array(
+ 'sapphire\test\classa' => "{$this->base}/module/classes/ClassA.php",
+ 'sapphire\test\classb' => "{$this->base}/module/classes/ClassB.php",
+ 'sapphire\test\classc' => "{$this->base}/module/classes/ClassC.php",
+ 'sapphire\test\classd' => "{$this->base}/module/classes/ClassD.php",
+ 'sapphire\test\classe' => "{$this->base}/module/classes/ClassE.php",
+ 'sapphire\test\classf' => "{$this->base}/module/classes/ClassF.php",
+ 'sapphire\test\classg' => "{$this->base}/module/classes/ClassG.php"
+ );
+
+ $this->assertEquals($expect, $this->manifest->getClasses());
+ }
+
+ public function testGetClassNames() {
+ $this->assertEquals(
+ array('sapphire\test\classa', 'sapphire\test\classb', 'sapphire\test\classc', 'sapphire\test\classd', 'sapphire\test\classe', 'sapphire\test\classf', 'sapphire\test\classg'),
+ $this->manifest->getClassNames());
+ }
+
+ public function testGetDescendants() {
+ $expect = array(
+ 'sapphire\test\classa' => array('sapphire\test\ClassB')
+ );
+
+ $this->assertEquals($expect, $this->manifest->getDescendants());
+ }
+
+ public function testGetDescendantsOf() {
+ $expect = array(
+ 'SAPPHIRE\TEST\CLASSA' => array('sapphire\test\ClassB'),
+ 'sapphire\test\classa' => array('sapphire\test\ClassB'),
+ );
+
+ foreach ($expect as $class => $desc) {
+ $this->assertEquals($desc, $this->manifest->getDescendantsOf($class));
+ }
+ }
+
+ public function testGetInterfaces() {
+ $expect = array(
+ 'sapphire\test\interfacea' => "{$this->base}/module/interfaces/InterfaceA.php",
+ );
+ $this->assertEquals($expect, $this->manifest->getInterfaces());
+ }
+
+ public function testGetImplementors() {
+ $expect = array(
+ 'sapphire\test\interfacea' => array('sapphire\test\ClassE'),
+ 'interfacea' => array('sapphire\test\ClassF'),
+ 'sapphire\test\subtest\interfacea' => array('sapphire\test\ClassG')
+ );
+ $this->assertEquals($expect, $this->manifest->getImplementors());
+ }
+
+ public function testGetImplementorsOf() {
+ $expect = array(
+ 'SAPPHIRE\TEST\INTERFACEA' => array('sapphire\test\ClassE'),
+ 'sapphire\test\interfacea' => array('sapphire\test\ClassE'),
+ 'INTERFACEA' => array('sapphire\test\ClassF'),
+ 'interfacea' => array('sapphire\test\ClassF'),
+ 'SAPPHIRE\TEST\SUBTEST\INTERFACEA' => array('sapphire\test\ClassG'),
+ 'sapphire\test\subtest\interfacea' => array('sapphire\test\ClassG'),
+ );
+
+ foreach ($expect as $interface => $impl) {
+ $this->assertEquals($impl, $this->manifest->getImplementorsOf($interface));
+ }
+ }
+
+ public function testGetConfigs() {
+ $expect = array("{$this->base}/module/_config.php");
+ $this->assertEquals($expect, $this->manifest->getConfigs());
+ }
+
+ public function testGetModules() {
+ $expect = array("module" => "{$this->base}/module");
+ $this->assertEquals($expect, $this->manifest->getModules());
+ }
+}
View
111 tests/core/manifest/TokenisedRegularExpressionTest.php
@@ -52,48 +52,133 @@ class ClassF extends ParentClassF
);
}
+ function getNamespaceTokens() {
+ return token_get_all(<<<PHP
+<?php
+
+namespace sapphire\\test;
+
+class ClassA {
+
+}
+
+class ClassB extends ParentClassB {
+
+}
+
+class ClassC extends \\ParentClassC {
+
+}
+
+class ClassD extends subtest\\ParentClassD {
+
+}
+
+class ClassE implements InterfaceE {
+
+}
+
+class ClassF implements \\InterfaceF {
+
+}
+
+class ClassG implements subtest\\InterfaceG {
+
+}
+
+?>
+PHP
+);
+ }
+
function testClassDefParser() {
$parser = SS_ClassManifest::get_class_parser();
$tokens = $this->getTokens();
-
+
$matches = $parser->findAll($tokens);
$classes = array();
if($matches) foreach($matches as $match) $classes[$match['className']] = $match;
-
+
$this->assertArrayHasKey('ClassA', $classes);
$this->assertArrayHasKey('ClassB', $classes);
-
+
$this->assertArrayHasKey('ClassC', $classes);
- $this->assertEquals('ParentClassC', $classes['ClassC']['extends']);
-
+ $this->assertEquals(array('ParentClassC'), $classes['ClassC']['extends']);
+
$this->assertArrayHasKey('ClassD', $classes);
- $this->assertEquals('ParentClassD', $classes['ClassD']['extends']);
+ $this->assertEquals(array('ParentClassD'), $classes['ClassD']['extends']);
$this->assertContains('InterfaceA', $classes['ClassD']['interfaces']);
-
+
$this->assertArrayHasKey('ClassE', $classes);
- $this->assertEquals('ParentClassE', $classes['ClassE']['extends']);
+ $this->assertEquals(array('ParentClassE'), $classes['ClassE']['extends']);
$this->assertContains('InterfaceA', $classes['ClassE']['interfaces']);
$this->assertContains('InterfaceB', $classes['ClassE']['interfaces']);
-
+
$this->assertArrayHasKey('ClassF', $classes);
- $this->assertEquals('ParentClassF', $classes['ClassF']['extends']);
+ $this->assertEquals(array('ParentClassF'), $classes['ClassF']['extends']);
$this->assertContains('InterfaceA', $classes['ClassF']['interfaces']);
$this->assertContains('InterfaceB', $classes['ClassF']['interfaces']);
}
-
+
+ function testNamesapcedClassDefParser() {
+ if(version_compare(PHP_VERSION, '5.3', '<')) {
+ return;
+ }
+ $parser = SS_ClassManifest::get_namespaced_class_parser();
+
+ $tokens = $this->getNamespaceTokens();
+
+ $matches = $parser->findAll($tokens);
+
+ $classes = array();
+ if($matches) foreach($matches as $match) $classes[$match['className']] = $match;
+
+ $this->assertArrayHasKey('ClassA', $classes);
+ $this->assertArrayHasKey('ClassB', $classes);
+ $this->assertEquals(array('ParentClassB'), $classes['ClassB']['extends']);
+
+ $this->assertArrayHasKey('ClassC', $classes);
+ $this->assertEquals(array('\\', 'ParentClassC'), $classes['ClassC']['extends']);
+
+ $this->assertArrayHasKey('ClassD', $classes);
+ $this->assertEquals(array('subtest', '\\', 'ParentClassD'), $classes['ClassD']['extends']);
+
+ $this->assertArrayHasKey('ClassE', $classes);
+ $this->assertContains('InterfaceE', $classes['ClassE']['interfaces']);
+
+ $this->assertArrayHasKey('ClassF', $classes);
+ $this->assertEquals(array('\\', 'InterfaceF'), $classes['ClassF']['interfaces']);
+ }
+
function testInterfaceDefParser() {
$parser = SS_ClassManifest::get_interface_parser();
$tokens = $this->getTokens();
-
+
$matches = $parser->findAll($tokens);
$interfaces = array();
if($matches) foreach($matches as $match) $interfaces[$match['interfaceName']] = $match;
-
+
$this->assertArrayHasKey('InterfaceA', $interfaces);
$this->assertArrayHasKey('InterfaceB', $interfaces);
$this->assertArrayHasKey('InterfaceC', $interfaces);
$this->assertArrayHasKey('InterfaceD', $interfaces);
}
+
+ function testNamespaceDefParser() {
+ if(version_compare(PHP_VERSION, '5.3', '<')) {
+ return;
+ }
+ $parser = SS_ClassManifest::get_namespace_parser();
+
+ $namespacedTokens = $this->getNamespaceTokens();
+ $tokens = $this->getTokens();
+
+ $namespacedMatches = $parser->findAll($namespacedTokens);
+ $matches = $parser->findAll($tokens);
+
+ $this->assertEquals(array(), $matches);
+ $this->assertEquals(array('sapphire', '\\', 'test'), $namespacedMatches[0]['namespaceName']);
+ }
}
View
0  tests/core/manifest/fixtures/namespaced_classmanifest/module/_config.php
No changes.
View
8 tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassA.php
@@ -0,0 +1,8 @@
+<?php
+/**
+ * @ignore
+ */
+
+namespace sapphire\test;
+
+class ClassA { }
View
8 tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassB.php
@@ -0,0 +1,8 @@
+<?php
+/**
+ * @ignore
+ */
+
+namespace sapphire\test;
+
+class ClassB extends ClassA { }
View
8 tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassC.php
@@ -0,0 +1,8 @@
+<?php
+/**
+ * @ignore
+ */
+
+namespace sapphire\test;
+
+class ClassC extends \ClassA { }
View
8 tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassD.php
@@ -0,0 +1,8 @@
+<?php
+/**
+ * @ignore
+ */
+
+namespace sapphire\test;
+
+class ClassD extends subtest\ClassC { }
View
8 tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassE.php
@@ -0,0 +1,8 @@
+<?php
+/**
+ * @ignore
+ */
+
+namespace sapphire\test;
+
+class ClassE implements InterfaceA { }
View
8 tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassF.php
@@ -0,0 +1,8 @@
+<?php
+/**
+ * @ignore
+ */
+
+namespace sapphire\test;
+
+class ClassF implements \InterfaceA { }
View
8 tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassG.php
@@ -0,0 +1,8 @@
+<?php
+/**
+ * @ignore
+ */
+
+namespace sapphire\test;
+
+class ClassG implements subtest\InterfaceA { }
View
8 tests/core/manifest/fixtures/namespaced_classmanifest/module/interfaces/InterfaceA.php
@@ -0,0 +1,8 @@
+<?php
+/**
+ * @ignore
+ */
+
+namespace sapphire\test;
+
+interface InterfaceA { }
Please sign in to comment.
Something went wrong with that request. Please try again.