PHP PSR-4 与 PSR-0 规范

1,282 阅读10分钟
原文链接: blog.csdn.net

上一篇文章中,介绍了PSR-0和autoload相关的内容。继PSR-0这个PHP autoloading的规范之后,PHP-FIG又推出了PSR-4,称为改进的autoloading规范。

在PSR-0中,\Symfony\Core\Request会被转换成文件系统的/path/to/project/lib/vendor/Symfony/Core/Request.php这个路径。PSR-4与PSR-0在内容上相差也不大。

在此就不详谈两者的定义了。来看看两者在实际中的一些区别吧。由于Composer的流行,这里对Composer中这两种风格进行比较。

在Composer中,遵循PSR-0标准的典型目录结构是这样的:

  1. vendor/  
  2.     vendor_name/  
  3.         package_name/  
  4.             src/  
  5.                 Vendor_Name/  
  6.                     Package_Name/  
  7.                         ClassName.php       # Vendor_Name\Package_Name\ClassName  
  8.             tests/  
  9.                 Vendor_Name/  
  10.                     Package_Name/  
  11.                         ClassNameTest.php   # Vendor_Name\Package_Name\ClassNameTest  
vendor/
    vendor_name/
        package_name/
            src/
                Vendor_Name/
                    Package_Name/
                        ClassName.php       # Vendor_Name\Package_Name\ClassName
            tests/
                Vendor_Name/
                    Package_Name/
                        ClassNameTest.php   # Vendor_Name\Package_Name\ClassNameTest

可以看到目录结构有明显的重复而且层次很深。src/和test/目录又重新包含了Vendor和Package目录。

再来看看PSR-4的:

  1. vendor/  
  2.     vendor_name/  
  3.         package_name/  
  4.             src/  
  5.                 ClassName.php       # Vendor_Name\Package_Name\ClassName  
  6.             tests/  
  7.                 ClassNameTest.php   # Vendor_Name\Package_Name\ClassNameTest  
vendor/
    vendor_name/
        package_name/
            src/
                ClassName.php       # Vendor_Name\Package_Name\ClassName
            tests/
                ClassNameTest.php   # Vendor_Name\Package_Name\ClassNameTest
可以看到目录结构更加简洁了。

在PSR-0中目录结构要与命名空间层层对应,无法插入一个单独的目录。Vendor\Package\Class在psr-0会里被直接转换成同样的路径,而PSR-4则没有这样的强制要求。

对比PSR-0,除了PSR-4可以更简洁外,需要注意PSR-0中对下划线(_)是有特殊的处理的,下划线会转换成DIRECTORY_SEPARATOR,这是出于对PHP5.3以前版本兼容的考虑,而PSR-4中是没有这个处理的,这也是两者比较大的一个区别。

此外,PSR-4要求在autoloader中不允许抛出exceptions以及引发任何级别的errors,也不应该有返回值。这是因为可能注册了多个autoloaders,如果一个autoloader没有找到对应的class,应该交给下一个来处理,而不是去阻断这个通道。

PSR-4更简洁更灵活了,但这使得它相对更复杂了。例如通过完全符合PSR-0标准的class name,通常可以明确的知道这个class的路径,而PSR-4可能就不是这样了。

  1. Given a foo-bar package of classes in the file system at the following paths ...  
  2.   
  3.     /path/to/packages/foo-bar/  
  4.         src/  
  5.             Baz.php             # Foo\Bar\Baz  
  6.             Qux/  
  7.                 Quux.php        # Foo\Bar\Qux\Quux  
  8.         tests/  
  9.             BazTest.php         # Foo\Bar\BazTest  
  10.             Qux/  
  11.                 QuuxTest.php    # Foo\Bar\Qux\QuuxTest  
  12.   
  13. ... add the path to the class files for the \Foo\Bar\ namespace prefix  as follows:  
  14.     <?php  
  15.      // instantiate the loader  
  16.      $loader = new \Example\Psr4AutoloaderClass;  
  17.        
  18.      // register the autoloader  
  19.      $loader->register();  
  20.        
  21.      // register the base directories for the namespace prefix  
  22.      $loader->addNamespace('Foo\Bar''/path/to/packages/foo-bar/src');  
  23.      $loader->addNamespace('Foo\Bar''/path/to/packages/foo-bar/tests');  
  24.   
  25.      //此时一个namespace prefix对应到了多个"base directory"  
  26.   
  27.      //autoloader会去加载/path/to/packages/foo-bar/src/Qux/Quux.php  
  28.      new \Foo\Bar\Qux\Quux;  
  29.   
  30.      //autoloader会去加载/path/to/packages/foo-bar/tests/Qux/QuuxTest.php  
  31.      new \Foo\Bar\Qux\QuuxTest;  
Given a foo-bar package of classes in the file system at the following paths ...

    /path/to/packages/foo-bar/
        src/
            Baz.php             # Foo\Bar\Baz
            Qux/
                Quux.php        # Foo\Bar\Qux\Quux
        tests/
            BazTest.php         # Foo\Bar\BazTest
            Qux/
                QuuxTest.php    # Foo\Bar\Qux\QuuxTest

... add the path to the class files for the \Foo\Bar\ namespace prefix as follows:
	<?php
     // instantiate the loader
     $loader = new \Example\Psr4AutoloaderClass;
     
     // register the autoloader
     $loader->register();
     
     // register the base directories for the namespace prefix
     $loader->addNamespace('Foo\Bar', '/path/to/packages/foo-bar/src');
     $loader->addNamespace('Foo\Bar', '/path/to/packages/foo-bar/tests');

     //此时一个namespace prefix对应到了多个"base directory"

     //autoloader会去加载/path/to/packages/foo-bar/src/Qux/Quux.php
     new \Foo\Bar\Qux\Quux;

     //autoloader会去加载/path/to/packages/foo-bar/tests/Qux/QuuxTest.php
     new \Foo\Bar\Qux\QuuxTest;

下面是如上PSR-4 autoloader的实现:

  1. <?php  
  2. namespace Example;  
  3.   
  4. class Psr4AutoloaderClass  
  5. {  
  6.     /** 
  7.      * An associative array where the key is a namespace prefix and the value 
  8.      * is an array of base directories for classes in that namespace. 
  9.      * 
  10.      * @var array 
  11.      */  
  12.     protected $prefixes = array();  
  13.   
  14.     /** 
  15.      * Register loader with SPL autoloader stack. 
  16.      *  
  17.      * @return void 
  18.      */  
  19.     public function register()  
  20.     {  
  21.         spl_autoload_register(array($this'loadClass'));  
  22.     }  
  23.   
  24.     /** 
  25.      * Adds a base directory for a namespace prefix. 
  26.      * 
  27.      * @param string $prefix The namespace prefix. 
  28.      * @param string $base_dir A base directory for class files in the 
  29.      * namespace. 
  30.      * @param bool $prepend If true, prepend the base directory to the stack 
  31.      * instead of appending it; this causes it to be searched first rather 
  32.      * than last. 
  33.      * @return void 
  34.      */  
  35.     public function addNamespace($prefix$base_dir $prepend = false)  
  36.     {  
  37.         // normalize namespace prefix  
  38.         $prefix = trim($prefix'\\') . '\\';  
  39.   
  40.         // normalize the base directory with a trailing separator  
  41.         $base_dir = rtrim($base_dir'/') . DIRECTORY_SEPARATOR;  
  42.         $base_dir = rtrim($base_dir, DIRECTORY_SEPARATOR) . '/';  
  43.   
  44.         // initialize the namespace prefix array  
  45.         if (isset($this->prefixes[$prefix]) === false) {  
  46.             $this->prefixes[$prefix] = array();  
  47.         }  
  48.   
  49.         // retain the base directory for the namespace prefix  
  50.         if ($prepend) {  
  51.             array_unshift($this->prefixes[$prefix],  $base_dir);  
  52.         } else {  
  53.             array_push($this->prefixes[$prefix],  $base_dir);  
  54.         }  
  55.     }  
  56.   
  57.     /** 
  58.      * Loads the class file for a given class name. 
  59.      * 
  60.      * @param string $class The fully-qualified class name. 
  61.      * @return mixed The mapped file name on success, or boolean false on 
  62.      * failure. 
  63.      */  
  64.     public function loadClass($class)  
  65.     {  
  66.         // the current namespace prefix  
  67.         $prefix = $class;  
  68.   
  69.         // work backwards through the namespace names of the fully-qualified  
  70.         // class name to find a mapped file name  
  71.         while (false !== $pos = strrpos( $prefix'\\')) {  
  72.   
  73.             // retain the trailing namespace separator in the prefix  
  74.             $prefix = substr($class, 0,  $pos + 1);  
  75.   
  76.             // the rest is the relative class name  
  77.             $relative_class = substr($class $pos + 1);  
  78.   
  79.             // try to load a mapped file for the prefix and relative class  
  80.             $mapped_file = $this->loadMappedFile($prefix $relative_class);  
  81.             if ($mapped_file) {  
  82.                 return $mapped_file;  
  83.             }  
  84.   
  85.             // remove the trailing namespace separator for the next iteration  
  86.             // of strrpos()  
  87.             $prefix = rtrim($prefix'\\');     
  88.         }  
  89.   
  90.         // never found a mapped file  
  91.         return false;  
  92.     }  
  93.   
  94.     /** 
  95.      * Load the mapped file for a namespace prefix and relative class. 
  96.      *  
  97.      * @param string $prefix The namespace prefix. 
  98.      * @param string $relative_class The relative class name. 
  99.      * @return mixed Boolean false if no mapped file can be loaded, or the 
  100.      * name of the mapped file that was loaded. 
  101.      */  
  102.     protected function loadMappedFile($prefix$relative_class)  
  103.     {  
  104.         // are there any base directories for this namespace prefix?  
  105.         if (isset($this->prefixes[$prefix]) === false) {  
  106.             return false;  
  107.         }  
  108.   
  109.         // look through base directories for this namespace prefix  
  110.         foreach ($this->prefixes[$prefix as $base_dir) {  
  111.   
  112.             // replace the namespace prefix with the base directory,  
  113.             // replace namespace separators with directory separators  
  114.             // in the relative class name, append with .php  
  115.             $file = $base_dir  
  116.                   . str_replace('\\', DIRECTORY_SEPARATOR,  $relative_class)  
  117.                   . '.php';  
  118.             $file = $base_dir  
  119.                   . str_replace('\\', '/',  $relative_class)  
  120.                   . '.php';  
  121.   
  122.             // if the mapped file exists, require it  
  123.             if ($this->requireFile($file)) {  
  124.                 // yes, we're done  
  125.                 return $file;  
  126.             }  
  127.         }  
  128.   
  129.         // never found it  
  130.         return false;  
  131.     }  
  132.   
  133.     /** 
  134.      * If a file exists, require it from the file system. 
  135.      *  
  136.      * @param string $file The file to require. 
  137.      * @return bool True if the file exists, false if not. 
  138.      */  
  139.     protected function requireFile($file)  
  140.     {  
  141.         if (file_exists($file)) {  
  142.             require $file;  
  143.             return true;  
  144.         }  
  145.         return false;  
  146.     }  
  147. }  
<?php
namespace Example;

class Psr4AutoloaderClass
{
    /**
     * An associative array where the key is a namespace prefix and the value
     * is an array of base directories for classes in that namespace.
     *
     * @var array
     */
    protected $prefixes = array();

    /**
     * Register loader with SPL autoloader stack.
     * 
     * @return void
     */
    public function register()
    {
        spl_autoload_register(array($this, 'loadClass'));
    }

    /**
     * Adds a base directory for a namespace prefix.
     *
     * @param string $prefix The namespace prefix.
     * @param string $base_dir A base directory for class files in the
     * namespace.
     * @param bool $prepend If true, prepend the base directory to the stack
     * instead of appending it; this causes it to be searched first rather
     * than last.
     * @return void
     */
    public function addNamespace($prefix, $base_dir, $prepend = false)
    {
        // normalize namespace prefix
        $prefix = trim($prefix, '\\') . '\\';

        // normalize the base directory with a trailing separator
        $base_dir = rtrim($base_dir, '/') . DIRECTORY_SEPARATOR;
        $base_dir = rtrim($base_dir, DIRECTORY_SEPARATOR) . '/';

        // initialize the namespace prefix array
        if (isset($this->prefixes[$prefix]) === false) {
            $this->prefixes[$prefix] = array();
        }

        // retain the base directory for the namespace prefix
        if ($prepend) {
            array_unshift($this->prefixes[$prefix], $base_dir);
        } else {
            array_push($this->prefixes[$prefix], $base_dir);
        }
    }

    /**
     * Loads the class file for a given class name.
     *
     * @param string $class The fully-qualified class name.
     * @return mixed The mapped file name on success, or boolean false on
     * failure.
     */
    public function loadClass($class)
    {
        // the current namespace prefix
        $prefix = $class;

        // work backwards through the namespace names of the fully-qualified
        // class name to find a mapped file name
        while (false !== $pos = strrpos($prefix, '\\')) {

            // retain the trailing namespace separator in the prefix
            $prefix = substr($class, 0, $pos + 1);

            // the rest is the relative class name
            $relative_class = substr($class, $pos + 1);

            // try to load a mapped file for the prefix and relative class
            $mapped_file = $this->loadMappedFile($prefix, $relative_class);
            if ($mapped_file) {
                return $mapped_file;
            }

            // remove the trailing namespace separator for the next iteration
            // of strrpos()
            $prefix = rtrim($prefix, '\\');   
        }

        // never found a mapped file
        return false;
    }

    /**
     * Load the mapped file for a namespace prefix and relative class.
     * 
     * @param string $prefix The namespace prefix.
     * @param string $relative_class The relative class name.
     * @return mixed Boolean false if no mapped file can be loaded, or the
     * name of the mapped file that was loaded.
     */
    protected function loadMappedFile($prefix, $relative_class)
    {
        // are there any base directories for this namespace prefix?
        if (isset($this->prefixes[$prefix]) === false) {
            return false;
        }

        // look through base directories for this namespace prefix
        foreach ($this->prefixes[$prefix] as $base_dir) {

            // replace the namespace prefix with the base directory,
            // replace namespace separators with directory separators
            // in the relative class name, append with .php
            $file = $base_dir
                  . str_replace('\\', DIRECTORY_SEPARATOR, $relative_class)
                  . '.php';
            $file = $base_dir
                  . str_replace('\\', '/', $relative_class)
                  . '.php';

            // if the mapped file exists, require it
            if ($this->requireFile($file)) {
                // yes, we're done
                return $file;
            }
        }

        // never found it
        return false;
    }

    /**
     * If a file exists, require it from the file system.
     * 
     * @param string $file The file to require.
     * @return bool True if the file exists, false if not.
     */
    protected function requireFile($file)
    {
        if (file_exists($file)) {
            require $file;
            return true;
        }
        return false;
    }
}
单元测试代码:

  1. <?php  
  2. namespace Example\Tests;  
  3.   
  4. class MockPsr4AutoloaderClass extends Psr4AutoloaderClass  
  5. {  
  6.     protected $files = array();  
  7.   
  8.     public function setFiles(array $files)  
  9.     {  
  10.         $this->files = $files;  
  11.     }  
  12.   
  13.     protected function requireFile($file)  
  14.     {  
  15.         return in_array($file$this->files);  
  16.     }  
  17. }  
  18.   
  19. class Psr4AutoloaderClassTest extends \PHPUnit_Framework_TestCase  
  20. {  
  21.     protected $loader;  
  22.   
  23.     protected function setUp()  
  24.     {  
  25.         $this->loader = new MockPsr4AutoloaderClass;  
  26.   
  27.         $this->loader->setFiles(array(  
  28.             '/vendor/foo.bar/src/ClassName.php',  
  29.             '/vendor/foo.bar/src/DoomClassName.php',  
  30.             '/vendor/foo.bar/tests/ClassNameTest.php',  
  31.             '/vendor/foo.bardoom/src/ClassName.php',  
  32.             '/vendor/foo.bar.baz.dib/src/ClassName.php',  
  33.             '/vendor/foo.bar.baz.dib.zim.gir/src/ClassName.php',  
  34.         ));  
  35.   
  36.         $this->loader->addNamespace(  
  37.             'Foo\Bar',  
  38.             '/vendor/foo.bar/src'  
  39.         );  
  40.   
  41.         $this->loader->addNamespace(  
  42.             'Foo\Bar',  
  43.             '/vendor/foo.bar/tests'  
  44.         );  
  45.   
  46.         $this->loader->addNamespace(  
  47.             'Foo\BarDoom',  
  48.             '/vendor/foo.bardoom/src'  
  49.         );  
  50.   
  51.         $this->loader->addNamespace(  
  52.             'Foo\Bar\Baz\Dib',  
  53.             '/vendor/foo.bar.baz.dib/src'  
  54.         );  
  55.   
  56.         $this->loader->addNamespace(  
  57.             'Foo\Bar\Baz\Dib\Zim\Gir',  
  58.             '/vendor/foo.bar.baz.dib.zim.gir/src'  
  59.         );  
  60.     }  
  61.   
  62.     public function testExistingFile()  
  63.     {  
  64.         $actual = $this->loader->loadClass('Foo\Bar\ClassName');  
  65.         $expect = '/vendor/foo.bar/src/ClassName.php';  
  66.         $this->assertSame($expect$actual);  
  67.   
  68.         $actual = $this->loader->loadClass('Foo\Bar\ClassNameTest');  
  69.         $expect = '/vendor/foo.bar/tests/ClassNameTest.php';  
  70.         $this->assertSame($expect$actual);  
  71.     }  
  72.   
  73.     public function testMissingFile()  
  74.     {  
  75.         $actual = $this->loader->loadClass('No_Vendor\No_Package\NoClass');  
  76.         $this->assertFalse($actual);  
  77.     }  
  78.   
  79.     public function testDeepFile()  
  80.     {  
  81.         $actual = $this->loader->loadClass('Foo\Bar\Baz\Dib\Zim\Gir\ClassName');  
  82.         $expect = '/vendor/foo.bar.baz.dib.zim.gir/src/ClassName.php';  
  83.         $this->assertSame($expect$actual);  
  84.     }  
  85.   
  86.     public function testConfusion()  
  87.     {  
  88.         $actual = $this->loader->loadClass('Foo\Bar\DoomClassName');  
  89.         $expect = '/vendor/foo.bar/src/DoomClassName.php';  
  90.         $this->assertSame($expect$actual);  
  91.   
  92.         $actual = $this->loader->loadClass('Foo\BarDoom\ClassName');  
  93.         $expect = '/vendor/foo.bardoom/src/ClassName.php';  
  94.         $this->assertSame($expect$actual);  
  95.     }  
  96. }  
<?php
namespace Example\Tests;

class MockPsr4AutoloaderClass extends Psr4AutoloaderClass
{
    protected $files = array();

    public function setFiles(array $files)
    {
        $this->files = $files;
    }

    protected function requireFile($file)
    {
        return in_array($file, $this->files);
    }
}

class Psr4AutoloaderClassTest extends \PHPUnit_Framework_TestCase
{
    protected $loader;

    protected function setUp()
    {
        $this->loader = new MockPsr4AutoloaderClass;

        $this->loader->setFiles(array(
            '/vendor/foo.bar/src/ClassName.php',
            '/vendor/foo.bar/src/DoomClassName.php',
            '/vendor/foo.bar/tests/ClassNameTest.php',
            '/vendor/foo.bardoom/src/ClassName.php',
            '/vendor/foo.bar.baz.dib/src/ClassName.php',
            '/vendor/foo.bar.baz.dib.zim.gir/src/ClassName.php',
        ));

        $this->loader->addNamespace(
            'Foo\Bar',
            '/vendor/foo.bar/src'
        );

        $this->loader->addNamespace(
            'Foo\Bar',
            '/vendor/foo.bar/tests'
        );

        $this->loader->addNamespace(
            'Foo\BarDoom',
            '/vendor/foo.bardoom/src'
        );

        $this->loader->addNamespace(
            'Foo\Bar\Baz\Dib',
            '/vendor/foo.bar.baz.dib/src'
        );

        $this->loader->addNamespace(
            'Foo\Bar\Baz\Dib\Zim\Gir',
            '/vendor/foo.bar.baz.dib.zim.gir/src'
        );
    }

    public function testExistingFile()
    {
        $actual = $this->loader->loadClass('Foo\Bar\ClassName');
        $expect = '/vendor/foo.bar/src/ClassName.php';
        $this->assertSame($expect, $actual);

        $actual = $this->loader->loadClass('Foo\Bar\ClassNameTest');
        $expect = '/vendor/foo.bar/tests/ClassNameTest.php';
        $this->assertSame($expect, $actual);
    }

    public function testMissingFile()
    {
        $actual = $this->loader->loadClass('No_Vendor\No_Package\NoClass');
        $this->assertFalse($actual);
    }

    public function testDeepFile()
    {
        $actual = $this->loader->loadClass('Foo\Bar\Baz\Dib\Zim\Gir\ClassName');
        $expect = '/vendor/foo.bar.baz.dib.zim.gir/src/ClassName.php';
        $this->assertSame($expect, $actual);
    }

    public function testConfusion()
    {
        $actual = $this->loader->loadClass('Foo\Bar\DoomClassName');
        $expect = '/vendor/foo.bar/src/DoomClassName.php';
        $this->assertSame($expect, $actual);

        $actual = $this->loader->loadClass('Foo\BarDoom\ClassName');
        $expect = '/vendor/foo.bardoom/src/ClassName.php';
        $this->assertSame($expect, $actual);
    }
}

完。