一直对psr0和4了解不是很透彻,虽然官方已经废弃了psr0,但是发现composer还是对psr0向下兼容,所以也花时间从composer的加载代码中了解了一下他们的区别,具体如下:
在composer中定义的NS,psr4必须以\结尾否则会抛出异常,psr0则不要求
psr0里面最后一个\之后的类名中,如果有下划线,则会转换成路径分隔符,如Name_Space_Test会转换成Name\Space\Test.php。在psr4中下划线不存在实际意义
psr0有更深的目录结构,比如定义了NS为 Foo\Bar=>vendor\foo\bar\src, use Foo\Bar\Tool\Request调用NS。 如果以psr0方式加载,实际的目录为vendor\foo\bar\src\Foo\Bar\Tool\Request.php 如果以psr4方式加载,实际目录为vendor\foo\bar\src\Tool\Request.php
介绍PSR-0之前,先来说说命名空间(NameSpace)和Autoloading吧。
namespace是PHP5.3版本加入的新特性,用来解决在编写类库或应用程序时创建可重用的代码如类或函数时碰到的两类问题:
用户编写的代码与PHP内部的类/函数/常量或第三方类/函数/常量之间的名字冲突。为很长的标识符名称(通常是为了缓解第一类问题而定义的)创建一个别名(或简短)的名称,提高源代码的可读性。php 命名空间中的元素使用了类似文件系统的原理。例如,类名可以通过三种方式引用:
非限定名称,或不包含前缀的类名称,例如 $a=new foo(); 或 foo::staticmethod();如果当前命名空间是 currentnamespace,foo 将被解析为 currentnamespace\foo。如果使用 foo 的代码是全局的,不包含在任何命名空间中的代码,则 foo 会被解析为foo。 警告:如果命名空间中的函数或常量未定义,则该非限定的函数名称或常量名称会被解析为全局函数名称或常量名称。详情参见 使用命名空间:后备全局函数名称/常量名称。限定名称,或包含前缀的名称,例如 $a = new subnamespace\foo(); 或 subnamespace\foo::staticmethod();。如果当前的命名空间是 currentnamespace,则 foo 会被解析为 currentnamespace\subnamespace\foo。如果使用 foo 的代码是全局的,不包含在任何命名空间中的代码,foo 会被解析为subnamespace\foo。完全限定名称,或包含了全局前缀操作符的名称,例如, $a = new \currentnamespace\foo(); 或 \currentnamespace\foo::staticmethod();。在这种情况下,foo 总是被解析为代码中的文字名(literal name)currentnamespace\foo。另外注意访问任意全局类、函数或常量,都可以使用完全限定名称,例如 \strlen() 或 \Exception 或 \INI_ALL。
use My\Full\Classname as Another, My\Full\NSname; $obj = new Another; // 实例化一个 My\Full\Classname 对象 $obj = new \Another; // 实例化一个Another对象 $obj = new Another\thing; // 实例化一个My\Full\Classname\thing对象 $obj = new \Another\thing; // 实例化一个Another\thing对象 $a = \strlen('hi'); // 调用全局函数strlen $b = \INI_ALL; // 访问全局常量 INI_ALL $c = new \Exception('error'); // 实例化全局类 Exception以上是namespace的简要介绍,对此不了解的同学还是把细读文档吧。在众多新的PHP项目中,namespace已经被普遍使用了。尤其是伴随Composer的流行,它已经是很有必要去了解的特性了。
很多开发者写面向对象的应用程序时对每个类的定义建立一个 PHP 源文件。一个很大的烦恼是不得不在每个脚本开头写一个长长的包含(require, include)文件列表(每个类一个文件)。
而Autoload,就是解决这个问题的,通过定义的一个或一系列autoload函数,它会在试图使用尚未被定义的类时自动调用。通过调用autoload函数,脚本引擎在 PHP 出错失败前有了最后一个机会加载所需的类。这个autoload函数可以是默认的__autoload(),如下:
function __autoload($class_name) { require_once $class_name . '.php'; } $obj = new MyClass();也可以采用更灵活的方式,通过spl_autoload_register()来定义我们自己的__autoload()函数:
function my_autoload($class_name) { require_once $class_name . '.php'; } spl_autoload_register("my_autoload"); $obj = new MyClass();以上代码将my_autoload()函数注册到__autoload栈中,从而取到__autoload()函数(注意__autoload()函数将不再起作用,但可以显式的将其注册到__autoload栈)。注意到刚才提到了__autoload栈,意味着我们可以注册多个autoload函数,根据注册的顺序依次加载(通过spl_autoload_register的第三个参数可以改变这一顺序)。
在这里我们展示了autoload函数最简单的例子,当然通过一些规则的设置,也可以胜任实际环境中许多复杂的情况。
但是如果我们的项目依赖某些其他的项目,本来大家在各自独立的加载规则下能顺利运行,但融合在一起可能就不妙了。那么能否有一种通用的加载规则来解决这个问题?
PSR是由PHP Framework Interoperability Group(PHP通用性框架小组)发布的一系列标准/规范,目前包括了PSR-0~PSR-4共4个,而PSR-0就是其中的自动加载标准(其后的PSR-4称为改进的自动加载的标准,是PSR-0的补充。PSR-0使用更广泛)。https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md
下面描述了具体互操作性的自动加载所必须的条件:
一个完全合格的namespace和class必须符合这样的结构 \* 每个namespace必须有一个顶层的namespace(”Vendor Name”提供者名字) 每个namespace可以有多个子namespace 当从文件系统中加载时,每个namespace的分隔符要转换成 DIRECTORY_SEPARATOR(操作系统路径分隔符) 在CLASS NAME(类名)中,每个下划线()符号要转换成DIRECTORY_SEPARATOR。在namespace中,下划线()符号是没有(特殊)意义的。 当从文件系统中载入时,合格的namespace和class一定是以 .php 结尾的 verdor name,namespaces,class名可以由大小写字母组合而成(大小写敏感的)
除此之外可能还会遵循这个规则:如果文件不存在则返回false。
将下划线转换成DIRECTORY_SEPARATOR实际上是出于兼容PHP5.3之前的版本的考虑
下面是一个按照如上标准进行自动加载的简单范例:
function autoload($className) { //这里的$className一般是用namespace的方式来引用的,文章开头已有介绍 //去除$className左边的'\' 这是PHP5.3的一个bug,详见https://bugs.php.net/50731 $className = ltrim($className, '\\'); $fileName = ''; $namespace = ''; //找到最后一个namespace分隔符的位置 if ($lastNsPos = strrpos($className, '\\')) { $namespace = substr($className, 0, $lastNsPos); $className = substr($className, $lastNsPos + 1); $fileName = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR; } $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php'; require $fileName; }下面有一个SplClassLoader 的实现范例,如果你遵循了如上的标准,可以用它来进行自动加载。这也是目前PHP5.3推荐的类加载标准。http://gist.github.com/221634
<?php /* * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * This software consists of voluntary contributions made by many individuals * and is licensed under the MIT license. For more information, see * <http://www.doctrine-project.org>. */ /** * SplClassLoader implementation that implements the technical interoperability * standards for PHP 5.3 namespaces and class names. * * http://groups.google.com/group/php-standards/web/psr-0-final-proposal?pli=1 * * // Example which loads classes for the Doctrine Common package in the * // Doctrine\Common namespace. * $classLoader = new SplClassLoader('Doctrine\Common', '/path/to/doctrine'); * $classLoader->register(); * * @license http://www.opensource.org/licenses/mit-license.html MIT License * @author Jonathan H. Wage <jonwage@gmail.com> * @author Roman S. Borschel <roman@code-factory.org> * @author Matthew Weier O'Phinney <matthew@zend.com> * @author Kris Wallsmith <kris.wallsmith@gmail.com> * @author Fabien Potencier <fabien.potencier@symfony-project.org> */ class SplClassLoader { private $_fileExtension = '.php'; private $_namespace; private $_includePath; private $_namespaceSeparator = '\\'; /** * Creates a new <tt>SplClassLoader</tt> that loads classes of the * specified namespace. * * @param string $ns The namespace to use. */ public function __construct($ns = null, $includePath = null) { $this->_namespace = $ns; $this->_includePath = $includePath; } /** * Sets the namespace separator used by classes in the namespace of this class loader. * * @param string $sep The separator to use. */ public function setNamespaceSeparator($sep) { $this->_namespaceSeparator = $sep; } /** * Gets the namespace seperator used by classes in the namespace of this class loader. * * @return void */ public function getNamespaceSeparator() { return $this->_namespaceSeparator; } /** * Sets the base include path for all class files in the namespace of this class loader. * * @param string $includePath */ public function setIncludePath($includePath) { $this->_includePath = $includePath; } /** * Gets the base include path for all class files in the namespace of this class loader. * * @return string $includePath */ public function getIncludePath() { return $this->_includePath; } /** * Sets the file extension of class files in the namespace of this class loader. * * @param string $fileExtension */ public function setFileExtension($fileExtension) { $this->_fileExtension = $fileExtension; } /** * Gets the file extension of class files in the namespace of this class loader. * * @return string $fileExtension */ public function getFileExtension() { return $this->_fileExtension; } /** * Installs this class loader on the SPL autoload stack. */ public function register() { spl_autoload_register(array($this, 'loadClass')); } /** * Uninstalls this class loader from the SPL autoloader stack. */ public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); } /** * Loads the given class or interface. * * @param string $className The name of the class to load. * @return void */ public function loadClass($className) { if (null === $this->_namespace || $this->_namespace.$this->_namespaceSeparator === substr($className, 0, strlen($this->_namespace.$this->_namespaceSeparator))) { $fileName = ''; $namespace = ''; if (false !== ($lastNsPos = strripos($className, $this->_namespaceSeparator))) { $namespace = substr($className, 0, $lastNsPos); $className = substr($className, $lastNsPos + 1); $fileName = str_replace($this->_namespaceSeparator, DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR; } $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . $this->_fileExtension; require ($this->_includePath !== null ? $this->_includePath . DIRECTORY_SEPARATOR : '') . $fileName; } } }看到这里你可能还有疑问,比如psr-0中给出的例子
\Symfony\Core\Request =>/path/to/project/lib/vendor/Symfony/Core/Request.php
在=>号之前是我们要去加载的类,而后面的部分是这个文件在文件系统中的路径(我们实际加载的是这个)。在加载Symfony\Core\Request时,并没有同时给出实际路径来,那么是如何得到这个映射的呢?在PSR-0标准中并没有对/path/to/project/lib路径的强制要求,而是可以有自己的实现。一般有两种方式,一种就是使用php配置中的include_path(上面给出的例子有使用这个方式来做),第二种就是自己去注册这种映射,可以选用其中一种,或者两种都兼容。下面就来看看在Symfony中是怎么做的?
<?php require_once '/path/to/src/Symfony/Component/ClassLoader/ClassLoader.php'; use Symfony\Component\ClassLoader\ClassLoader; $loader = new ClassLoader(); // to enable searching the include path (eg. for PEAR packages) $loader->setUseIncludePath(true); // ... register namespaces and prefixes here - see below $loader->register(); // register a single namespaces $loader->addPrefix('Symfony', __DIR__.'/vendor/symfony/symfony/src');此时就将 Symfony 这个namespace 注册到了一个真实的路径上,Symfony的子命名空间也可以使用这个路径了。
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标准的典型目录结构是这样的:
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的:
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可能就不是这样了。
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的实现:
<?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; } }单元测试代码:
<?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); } }