Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Fix UniversalClassLoader matching collisions. #43

Closed
wants to merge 1 commit into from

3 participants

@bobthecow

The current loadClass() implementation tries to load a class from the first matching prefix or namespace then stops, producing false-negative results. This is especially evident in groups of related libraries, such as Doctrine:

Doctrine
Doctrine\Common
Doctrine\Common\DataFixtures
Doctrine\DBAL
Doctrine\DBAL\Migrations

Each of these libraries is submoduled into a different vendor directory. Classes may or may not actually be loaded depending on what order these libraries are added to a UniversalClassLoader instance. This fix continues searching registered namespaces and prefixes if the first partial match is negative.

@bobthecow bobthecow Fix UniversalClassLoader matching collisions.
The current `loadClass()` implementation tries to load a class from the first matching prefix then stops, producing false-negative results. This is especially evident in groups of related libraries, such as Doctrine:

    Doctrine
    Doctrine\Common
    Doctrine\Common\DataFixtures
    Doctrine\DBAL
    Doctrine\DBAL\Migrations

Each of these libraries is submoduled into a different vendor directory. Depending on what order these libraries are added to a UniversalClassLoader instance, classes may or may not actually be loaded. This fix continues searching registered namespaces and prefixes if the first partial match is negative.
54d20f2
@fabpot
Owner

Orders matter, and I think this is a good thing. You must put the most precise namespace first. I don't want the autoloader to be able to load the same class from different directories as it can become a nightmare pretty fast. Or is there another reason?

@chesteroni

If there is another reason (than putting namespaces in random order), maybe a solution is as follows: current behaviour remain unchanged, but when in configuration appears a proper setting, the loader look at all directories?
e.g.
classloader_search_directories = true/false [default=false]

@fabpot
Owner

Why make things more complicated?

@bobthecow

This isn't about putting namespaces in a random order. It's about keeping UniversalClassLoader from doing anything unexpected. In Symfony2 configs, anything set later will overload earlier values. By this precedent, I would expect the following to work:

$loader = new UniversalClassLoader();
$loader->registerNamespaces(array(
    'Doctrine\\Common' => __DIR__.'/vendor/doctrine-common/lib',
    'Doctrine\\DBAL'   => __DIR__.'/vendor/doctrine-dbal/lib',
    'Zend'             => __DIR__.'/vendor/zend/library',
));
$loader->registerNamespaces(array(
    'Doctrine\\Common\\DataFixtures' => __DIR__.'/vendor/doctrine-data-fixtures/lib',
    'Doctrine\\DBAL\\Migrations'     => __DIR__.'/vendor/doctrine-migrations/lib',
));
$loader->register();

But of course it doesn't, because the set of namespaces registered earlier include two collisions. Surprisingly, the following won't work either:

$loader = new UniversalClassLoader();
$loader->registerNamespaces(array(
    'Doctrine\\Common' => __DIR__.'/vendor/doctrine-common/lib',
    'Doctrine\\DBAL'   => __DIR__.'/vendor/doctrine-dbal/lib',
    'Zend'             => __DIR__.'/vendor/zend/library',
));
$loader->registerNamespaces(array(
    'Doctrine\\Common\\DataFixtures' => __DIR__.'/vendor/doctrine-data-fixtures/lib',
    'Doctrine\\Common'               => __DIR__.'/vendor/doctrine-common/lib',
    'Doctrine\\DBAL\\Migrations'     => __DIR__.'/vendor/doctrine-migrations/lib',
    'Doctrine\\DBAL'                 => __DIR__.'/vendor/doctrine-dbal/lib',
));
$loader->register();

But this works just fine:

$loader = new UniversalClassLoader();
$loader->registerNamespaces(array(
    'Doctrine\\Common' => __DIR__.'/vendor/doctrine-common/lib',
    'Doctrine\\DBAL'   => __DIR__.'/vendor/doctrine-dbal/lib',
    'Zend'             => __DIR__.'/vendor/zend/library',
));
$loader->register();

$loader2 = new UniversalClassLoader();
$loader2->registerNamespaces(array(
    'Doctrine\\Common\\DataFixtures' => __DIR__.'/vendor/doctrine-data-fixtures/lib',
    'Doctrine\\Common'               => __DIR__.'/vendor/doctrine-common/lib',
    'Doctrine\\DBAL\\Migrations'     => __DIR__.'/vendor/doctrine-migrations/lib',
    'Doctrine\\DBAL'                 => __DIR__.'/vendor/doctrine-dbal/lib',
));
$loader2->register();

I'm not sure I understand your concern. This fix seems to be a net win. If you maintain the current "most precise first" namespaces, this will not be any slower or more complicated: it will return matches just as fast, and will never enter the fallback checks.

@fabpot
Owner

merged.

@bobthecow

Thanks!

This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 3, 2011
  1. @bobthecow

    Fix UniversalClassLoader matching collisions.

    bobthecow authored
    The current `loadClass()` implementation tries to load a class from the first matching prefix then stops, producing false-negative results. This is especially evident in groups of related libraries, such as Doctrine:
    
        Doctrine
        Doctrine\Common
        Doctrine\Common\DataFixtures
        Doctrine\DBAL
        Doctrine\DBAL\Migrations
    
    Each of these libraries is submoduled into a different vendor directory. Depending on what order these libraries are added to a UniversalClassLoader instance, classes may or may not actually be loaded. This fix continues searching registered namespaces and prefixes if the first partial match is negative.
This page is out of date. Refresh to see the latest.
View
10 src/Symfony/Component/HttpFoundation/UniversalClassLoader.php
@@ -132,13 +132,12 @@ public function loadClass($class)
$namespace = substr($class, 0, $pos);
foreach ($this->namespaces as $ns => $dir) {
if (0 === strpos($namespace, $ns)) {
- $class = substr($class, $pos + 1);
- $file = $dir.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $namespace).DIRECTORY_SEPARATOR.str_replace('_', DIRECTORY_SEPARATOR, $class).'.php';
+ $className = substr($class, $pos + 1);
+ $file = $dir.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $namespace).DIRECTORY_SEPARATOR.str_replace('_', DIRECTORY_SEPARATOR, $className).'.php';
if (file_exists($file)) {
require $file;
+ return;
}
-
- return;
}
}
} else {
@@ -148,9 +147,8 @@ public function loadClass($class)
$file = $dir.DIRECTORY_SEPARATOR.str_replace('_', DIRECTORY_SEPARATOR, $class).'.php';
if (file_exists($file)) {
require $file;
+ return;
}
-
- return;
}
}
}
View
8 tests/Symfony/Tests/Component/HttpFoundation/Fixtures/alpha/NamespaceCollision/A/Bar.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace NamespaceCollision\A;
+
+class Bar
+{
+ public static $loaded = true;
+}
View
8 tests/Symfony/Tests/Component/HttpFoundation/Fixtures/alpha/NamespaceCollision/A/Foo.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace NamespaceCollision\A;
+
+class Foo
+{
+ public static $loaded = true;
+}
View
6 tests/Symfony/Tests/Component/HttpFoundation/Fixtures/alpha/PrefixCollision/A/Bar.php
@@ -0,0 +1,6 @@
+<?php
+
+class PrefixCollision_A_Bar
+{
+ public static $loaded = true;
+}
View
6 tests/Symfony/Tests/Component/HttpFoundation/Fixtures/alpha/PrefixCollision/A/Foo.php
@@ -0,0 +1,6 @@
+<?php
+
+class PrefixCollision_A_Foo
+{
+ public static $loaded = true;
+}
View
8 tests/Symfony/Tests/Component/HttpFoundation/Fixtures/beta/NamespaceCollision/A/B/Bar.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace NamespaceCollision\A\B;
+
+class Bar
+{
+ public static $loaded = true;
+}
View
8 tests/Symfony/Tests/Component/HttpFoundation/Fixtures/beta/NamespaceCollision/A/B/Foo.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace NamespaceCollision\A\B;
+
+class Foo
+{
+ public static $loaded = true;
+}
View
6 tests/Symfony/Tests/Component/HttpFoundation/Fixtures/beta/PrefixCollision/A/B/Bar.php
@@ -0,0 +1,6 @@
+<?php
+
+class PrefixCollision_A_B_Bar
+{
+ public static $loaded = true;
+}
View
6 tests/Symfony/Tests/Component/HttpFoundation/Fixtures/beta/PrefixCollision/A/B/Foo.php
@@ -0,0 +1,6 @@
+<?php
+
+class PrefixCollision_A_B_Foo
+{
+ public static $loaded = true;
+}
View
101 tests/Symfony/Tests/Component/HttpFoundation/UniversalClassLoaderTest.php
@@ -37,5 +37,104 @@ public static function testClassProvider()
array('\\Pearlike_Bar', '\\Pearlike_Bar', '->loadClass() loads Pearlike_Bar class with a leading slash'),
);
}
-}
+ /**
+ * @dataProvider namespaceCollisionClassProvider
+ */
+ public function testLoadClassNamespaceCollision($namespaces, $className, $message)
+ {
+ $loader = new UniversalClassLoader();
+ $loader->registerNamespaces($namespaces);
+
+ $loader->loadClass($className);
+ $this->assertTrue(class_exists($className), $message);
+ }
+
+ public static function namespaceCollisionClassProvider()
+ {
+ return array(
+ array(
+ array(
+ 'NamespaceCollision\\A' => __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures/alpha',
+ 'NamespaceCollision\\A\\B' => __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures/beta',
+ ),
+ 'NamespaceCollision\A\Foo',
+ '->loadClass() loads NamespaceCollision\A\Foo from alpha.',
+ ),
+ array(
+ array(
+ 'NamespaceCollision\\A\\B' => __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures/beta',
+ 'NamespaceCollision\\A' => __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures/alpha',
+ ),
+ 'NamespaceCollision\A\Bar',
+ '->loadClass() loads NamespaceCollision\A\Bar from alpha.',
+ ),
+ array(
+ array(
+ 'NamespaceCollision\\A' => __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures/alpha',
+ 'NamespaceCollision\\A\\B' => __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures/beta',
+ ),
+ 'NamespaceCollision\A\B\Foo',
+ '->loadClass() loads NamespaceCollision\A\B\Foo from beta.',
+ ),
+ array(
+ array(
+ 'NamespaceCollision\\A\\B' => __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures/beta',
+ 'NamespaceCollision\\A' => __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures/alpha',
+ ),
+ 'NamespaceCollision\A\B\Bar',
+ '->loadClass() loads NamespaceCollision\A\B\Bar from beta.',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider prefixCollisionClassProvider
+ */
+ public function testLoadClassPrefixCollision($prefixes, $className, $message)
+ {
+ $loader = new UniversalClassLoader();
+ $loader->registerPrefixes($prefixes);
+
+ $loader->loadClass($className);
+ $this->assertTrue(class_exists($className), $message);
+ }
+
+ public static function prefixCollisionClassProvider()
+ {
+ return array(
+ array(
+ array(
+ 'PrefixCollision_A_' => __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures/alpha',
+ 'PrefixCollision_A_B_' => __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures/beta',
+ ),
+ 'PrefixCollision_A_Foo',
+ '->loadClass() loads PrefixCollision_A_Foo from alpha.',
+ ),
+ array(
+ array(
+ 'PrefixCollision_A_B_' => __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures/beta',
+ 'PrefixCollision_A_' => __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures/alpha',
+ ),
+ 'PrefixCollision_A_Bar',
+ '->loadClass() loads PrefixCollision_A_Bar from alpha.',
+ ),
+ array(
+ array(
+ 'PrefixCollision_A_' => __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures/alpha',
+ 'PrefixCollision_A_B_' => __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures/beta',
+ ),
+ 'PrefixCollision_A_B_Foo',
+ '->loadClass() loads PrefixCollision_A_B_Foo from beta.',
+ ),
+ array(
+ array(
+ 'PrefixCollision_A_B_' => __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures/beta',
+ 'PrefixCollision_A_' => __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures/alpha',
+ ),
+ 'PrefixCollision_A_B_Bar',
+ '->loadClass() loads PrefixCollision_A_B_Bar from beta.',
+ ),
+ );
+ }
+}
Something went wrong with that request. Please try again.