xp-framework/xp-framework — pull request #235 by thekid (2013-02-16 09:42:58+00:00)
Title: Fix class loading inconsistencies with case-insensitive filesystems 

When working on case-insensitive filesystems such as Windows or OSX, class loading will also pick up for case spelling errors. This leads to inconsistent handling in all kinds of parts of the XP Framework's type system, and may surface in situations such as the unittest runner reporting "no tests found" although the class clearly has test methods.

Explanation
==

Class loading interna
--
Class loading works by translating the fully qualified class names to paths and then asking the class loader's delegates (file system or XAR file based implementations) whether they provide this path. So using `ClassLoader::getDefault()->loadClass($class)` will do something like the following given the content of `$class` is `Testclass` and a `Testclass.class.php` is in the user's home directory:

```
- ClassLoader::getDefault()->loadClass('Testclass')
- FileSystemCL::providesClass('Testclass')
- is_file('/usr/local/xp/core/Testclass.class.php')? => FALSE
- FileSystemCL::providesClass('Testclass')
- is_file('/home/user/Testclass.class.php')? => TRUE
- loadClass('Testclass')
- loadClass0('Testclass') => $c
- return new XPClass('Testclass')
```

So, given the following `Testclass.class.php` contents:

```php
getClass());
}
}
?>
```

Running it will yield the following:

```sh
$ xp Testclass
Hello from lang.XPClass
```

...which is the expected behaviour.

The problem with case-insensitve filesystems
--
Unfortunately, this will also "work" when we misspell the class name, e.g. as follows:

```sh
$ xp TestClass
Hello from lang.XPClass
```

...although we can see the class name being slightly different. If we go back to the above flow diagram for class loading and replace all occurrences of `Testclass` with `TestClass` (the version with an uppercase 'C' inside), we will see where the problem occurs: Operating systems with case-insensitive filesystems will also return `TRUE` for `is_file('.../TestClass.class.php')`. The inconsistency then happens when we create the `XPClass` instance with the reflected class name. Because this class name is later on used for looking up the fully qualified class name, the lookup fails to find it and falls back to the `php` namespace (which is used for e.g. `php.ReflectionClass` or `php.IteratorAggregate`).

This doesn't really seem to be a problem at first, but will surface later on. Say we change the `Testclass.class.php` to the following:

```php
getClass()->getAnnotations());
}
}
?>
```

Now we will see the behaviour change causing problems:

```
# OK
$ xp Testclass
Hello from [
demo => "Showing that it works"
]

# Problematic
$ xp TestClass
Hello from [
]
```

The annotations will not parse correctly because the class detail parsing relies on the class name lookup as expected.

Strategies for fixing this
==

1 - Fail fast
--
The first strategy, which I employ here, is to fail fast and make the filesystem classloader behave consistently over platforms. This is done by changing the `providesClass()` method to look up the name on the filesystem using `realpath()` and compare that to the name calculate by transforming the class name into a path. If these don't match, report the class does not exist.

Mistyping the class name will now yield the following (as seen on Linux, e.g.):
```
$ xp TestClass
Uncaught exception: Exception lang.ClassNotFoundException (Class "TestClass" could not be found) {
[...]
}
```

2 - Make it work
--
The second strategy would be to change lots of places to use `strtolower()` on the class names and make this work. The obvious downsides are:

* It's not consistent with Linux and will create "but it worked on my machine" type of problems
* The (minimal) performance impact with `strtolower()` calls.

Chosen: 1
==
This pull request implements the first strategy and changes the `is_file()` call to `realpath()`. A performance impact is not directly measurable, though the second should be slightly more expensive, as it actually corrects all path segments by using `stat` calls on them, while the first only uses one `stat` call on the entire path.