MySQL中的索引学习

# 什么是索引?

在没有学数据库之前,大多数人对索引还是比较陌生(可能学完之后还是比较陌生:),今天我想告诉大家索引很有用,而且面试的时候都会问到。那么索引是什么呢?

Read More

PHP中的static关键字

PHP和Java中都会有static这个关键字,用法也类似,当问及PHP中的static用法是,很容易想出static可以声明类属性或方法为静态,静态属性和方法都是属于类的,静态属性不能通过对象访问,但静态方法可以通过对象访问。没错,是这样的,但是在PHP中static还有另外的用处哦。

先从static变量的作用域开始

PHP中static变量只存在于本地函数中,但是当程序执行完本函数后,static变量还会一直存在,考虑如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
function test()
{
$a = 0;
echo $a . "\n";
$a++;
}

// 都会输出0
for ($i=0; $i<5; $i++) {
test();
}

在每次调用这个函数的时候,函数都会将$a变量置0,再输出,尽管每次输出后,变量$a都加1了,为了每次都能将$a的值保存起来,我们可以将它声明为static

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
function test()
{
static $a = 0;
echo $a . "\n";
$a++;
}

// 输出
// 0
// 1
// 2
// 3
// 4
for ($i=0; $i<5; $i++) {
test();
}

现在,$a只被初始化了一次,每次调用test()函数时,$a都会加1。

在递归函数中,同样可以使用静态变量,我们可以设置一个$count静态变量的函数运行计数器,保存运行的次数,当$count到10时,就退出递归函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
function test()
{
static $count = 0;

$count++;
echo $count;
if ($count < 10) {
test();
}
}

test();

静态变量初始化时只能是确定的一个值,不能是函数的返回值,下面的代码中,将sqrt()函数的结果赋值给静态变量$a会报错:

1
2
3
4
5
6
7
8
9
function foo(){
static $a = 0; // correct
static $a = 1+2; // correct (as of PHP 5.6)
static $a = sqrt(121); // wrong (as it is a function)

echo $a;
}

foo();

程序没有运行前,再phpstorm中就已经其实不能用表达式初始化静态变量:

程序运行时也会报如下错误:

声明类属性或方法为静态

声明类属性或方法为静态,就可以不实例化类而直接访问。静态属性不能通过一个类已实例化的对象来访问(但静态方法可以)。为了兼容 PHP 4,如果没有指定访问控制,属性和方法默认为公有。由于静态方法不需要通过对象即可调用,所以伪变量 $this 在静态方法中不可用。静态属性不可以由对象通过 -> 操作符来访问。用静态方式调用一个非静态方法会导致一个 E_STRICT 级别的错误。就像其它所有的 PHP 静态变量一样,静态属性只能被初始化为文字或常量,不能使用表达式。所以可以把静态属性初始化为整数或数组,但不能初始化为另一个变量或函数返回值,也不能指向一个对象。自 PHP 5.3.0 起,可以用一个变量来动态调用类。但该变量的值不能为关键字 self,parent 或 static。如下是静态属性的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
class Foo
{
public static $my_static = 'foo';

public function staticValue() {
return self::$my_static;
}
}

class Bar extends Foo
{
public function fooStatic() {
return parent::$my_static;
}
}


print Foo::$my_static . "\n";

$foo = new Foo();
print $foo->staticValue() . "\n";
print $foo->my_static . "\n"; // 对象不能使用->符号调用静态变量

print $foo::$my_static . "\n";
$classname = 'Foo';
print $classname::$my_static . "\n"; // 5.3开始可以使用变量调用类

print Bar::$my_static . "\n";
$bar = new Bar();
print $bar->fooStatic() . "\n";

运行结果如下:

作为静态变量,还可以在多个对象之间共享数据,创建好几个对象的时候,因为每次都是new的,所以创建的对象都不同,如果想让多个对象实例共享同一个变量,就可以用到静态变量。假设要编写一个类来跟踪网页浏览的人数,肯定不希望每次实例化该类时都把访问者数量置0,只是就可以将该属性设置为static

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Visitor
{
private static $visitors = 0;

public function __construct()
{
self::$visitors++;
}

public static function getVisitors()
{
return self::$visitors;
}
}

// 实例化
$visitor1 = new Visitor();
echo Visitor::getVisitors() . "\n"; // 1

$visitor2 = new Visitor();
echo Visitor::getVisitors() . "\n"; // 2

延迟静态绑定

PHP中的static关键字除了上述比较熟知的作用外,还可以作为延迟静态绑定使用,这是在5.3版本后才加入的功能。

先看如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
abstract class DomainObject
{

}

class User extends DomainObject
{
public static function create()
{
return new User();
}
}

class Document extends DomainObject
{
public static function create()
{
return new Document();
}
}

首先创建了一个抽象类,然后创建了两个子类UserDocument分别继承自DomainObject抽象类,这个代码运行起来完全没问题,而且能很好的工作,如果你是一位懒惰的程序猿,看到这样重复的代码你会很恼火,尤其是重复代码比较多的时候,就会想着如何重构它。每个DomainObject子类都有一个相同的create()函数,我们试着把它放入父类当中去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
abstract class DomainObject
{
public static function create()
{
return new self();
}
}

class User extends DomainObject
{
}

class Document extends DomainObject
{
}

Document::create();

很明显phpstorm会提示出错,我们试着运行会得到以下错误:

在上面的例子中,self对该类所起的作用与$this对对象所起的作用不完全相同,self被解析为定义create()DomainObject,而不是解析为调用selfDocument类。在PHP 5.3中延迟静态绑定的概念,最明显的标志就是使用static关键字,它指向的是被调用的类而不是包含类。上面的代码我们可以这么改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
abstract class DomainObject
{
public static function create()
{
return new static();
}
}

class User extends DomainObject
{
}

class Document extends DomainObject
{
}

print_r(Document::create());

输出:

static关键字不仅可以用于实例化,和selfparent一样,static还可以作为静态方法调用的标识符,甚至是从非静态上下文中调用。例如为DomainObject引入组的概念,默认组为default,可以用static为继承层次结构的某些子类重写组,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
abstract class DomainObject
{
private $group;
public function __construct()
{
$this->group = static::getGroup();
}

public static function create()
{
return new static();
}

public static function getGroup()
{
return "default";
}
}

class User extends DomainObject
{

}

class Document extends DomainObject
{
public static function getGroup()
{
return "document";
}
}

class SpreadSheet extends Document
{

}

print_r(User::create());
print_r(SpreadSheet::create());

代码中,DomainObject的构造函数使用static调用静态方法getGroup(),设置默认组为default,在Document中重写了getGroup()方法,重新设置了组,下面是输出结果:

PHP静态绑定的一个应用

该例子来自简书:https://www.jianshu.com/p/25a78620fa5c

需求

做的某项目有一个“转账”的功能,但是转账的类型有很多种,对应每种转账需要的参数也不同,举个例子一种转账是由系统转账给用户,那么就只有接收方和金额两个参数,另一种转账是用户之间的转账且支持留言,那么就有发送方接收方金额和留言四个参数。当然最简单的思路就是采用四个参数,对于第一种转账将不用的两个参数留空,这种方法的问题在于,考虑到未来可能增加的新的转账类型,可能会引入新的参数,那么代码很可能需要推倒重来,有没有更优雅的解决方式呢?

一个例子

其实Laravel里就有实现类似需求的例子,那就是查询构造器(Query Builder),它的一个使用的例子如下:

1
2
3
4
5
$users = DB::table('users')
->select(DB::raw('count(*) as user_count, status'))
->where('status', '<>', 1)
->groupBy('status')
->get();

这个方法和我们的需求就很像了,对于查询这一功能,传入哪些参数是未知的,例如某次具体的查询,可能需要调用groupBy也可能调用orderBy,也可能两者需要同时调用或者都不调用。一个思路就是针对每一个参数都写一个方法,需要时则调用,不需要时则不调用。

解决方案

整体的解决思路是写两个类,一个叫Transfer,一个叫Builder,每个参数对应的方法写在Builder里,由Transfer去调用Builder构造我们需要的转账类型,完成相关操作。这样当需求更新时(要增加新的参数时),只要在Builder里添加相应的方法即可,而不用改动现有代码。下面先贴一下对应的代码再做详细解释。
Transfer类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Transfer
{
public function __call($method, $parameters)
{
$builder = new Builder();
return call_user_func_array([$builder, $method], $parameters);
}

public static function __callStatic($method, $parameters)
{
$instance = new static;
return call_user_func_array([$instance, $method], $parameters);
}
}

Builder类实际上只涉及到具体的功能实现,就贴部分代码意思意思看看就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Builder
{
protected $from = 0; // 0 represents system
protected $to = 0;
protected $amount = 0;
protected $comments = '';
protected $related = [];

public function from($user)
{
if ($user instanceof User) {
$this->from = $user->getAuthIdentifier();
} elseif (is_int($user)) {
$this->from = $user;
} else {
throw new InvalidArgumentException(sprintf('%s excepts $user parameter to be \App\User or integer, %s given.', __METHOD__, gettype($user)));
}
return $this;
}
public function to($user){...}
public function amount(int $amount){...}
public function comments($comments){...}
public function related(int $type, int $id, $extra = null){...}
public function transfer(){...}
}

具体调用Transfer功能的代码:

1
Transfer::from($sender)->to($receiver)->amount($amount)->comments($comments)->related($related_type, $related_id, $related_extra)->transfer();

下面我们来走一遍调用Transfer的流程来看看。首先调用了Transfer类中的静态方法from,然而Transfer中并不存在这个静态方法,则会自动调用__callStatic()这个魔术方法。这个方法首先实例化了一个static对象。注意这里的static是一个类名,new出来的$instance是属于static这个类的一个实例化对象,有点拗口然后返回时调用了call_user_func_array这个方法,这个方法具体可以参考php的手册,实际上它完成了类似$instance->method($parameters)这样的操作,放到我们当前的情境下实际执行了$transfer_instance->from($user)这样的操作。

然后发觉Transfer中并不存在这个动态方法,于是又会自动调用__call()这个魔术方法。这个方法首先创建了一个Builder类的实例,之后调用call_user_func_array这个方法,实际上相当于执行了$builder->from($user)方法,然后终于得Builder类里找到了这个from()方法,注意它的返回值是$this。

然后当前这个Builder这个对象继续调用to方法,发觉又不存在又去调用__call()这个魔术方法,之后的过程同上,反复调用Builder中的方法把所有需要的参数都处理过以后最后调用了transfer()方法最终完成转账操作。

参考:

http://www.php.net/manual/zh/language.oop5.static.php
https://www.jianshu.com/p/25a78620fa5c

欢迎关注我的公众号:

什么是PSR规范?

什么是PSR?

PSR是PHP Standards Recommendation的简称,这个是php-fig组织制定的一套规范。至今,php-fig已经发布了五个规范:

  • PSR-0:自动加载标准,2014-10-21该标准已经被废弃,使用PSR-4替代,不再细讲
  • PSR-1:基本的编码风格
  • PSR-2:编码风格(更严格)
  • PSR-3:日志记录器接口
  • PSR-4:自动加载

PSR-1

PHP标签:
PHP代码必须放在<?php ?>标签或<?= ?>标签中。

编码:
PHP文件必须使用无BOMUTF-8编码。

Read More

面向对象的再思考

为什么要再次思考

临近毕业,自己投了很多简历,也参加了好多轮的面试,不论结果如何,自己也从中收获了很多,发现了自己的诸多不足。大多数的面试都会提到面向对象的问题,虽然自己上了很多面向对象编程的课,学习了几门面向对象的语言,但当坐在面试官面前面对面讨论面向对象问题时,自己的无知立马显现出来,所以我觉得有必要再次思考什么是面向对象(Object Oriented)。

什么是对象?

面向对象中的对象(Object)是什么呢?面向对象程序设计(OOP)及时使用对象来进行程序设计,对象就是代表现实世界中可以明确分辨的一个实体,一个人、一辆车、一个圆、一次交易都可以看作是一个对象,不管是真实存在的还是虚拟的。一个对象通常都有自己的特征和行为。

  • 对象的特征(property),也可以称为属性(attribute),是一个数据域
  • 对象的行为(behavior),也可以称为动作(action),只这个对象可以去做什么

面向过程到面向对象的过渡

刚开始接触编程时,还是学的C语言(多么古老而又不可替代的语言啊),慢慢接触到了C++,Java,PHP等面向对象的语言,面向过程到面向对象的转换并非易事,首先就是思考问题的方式,落到实际行动就是面向过程首先关注这个函数怎么写,面向对象首先关注如何把要解决的问题抽象出来,编写父类、子类。

类与对象

面向对象中的类是一个模版、蓝本或者说是合约,用来定义一个对象的数据域是什么以及方法是做什么的。一个对象就是一个类的实例,可以从类中创建多个实例,类和对象之间的关系类似于生产玩具汽车的铸模和玩具汽车。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Car
{
public $branch = "default branch";
public $no = "A123";

function __construct($branch, $no) {
$this->branch = $branch;
$this->no = $no;
}

function run() {
echo "The car is running!";
}
}

$car1 = new Car("奔驰", "B111");
$car2 = new Car("宝马", "C222");

var_dump($car1);
var_dump($car2);

什么是面向对象?

在学习面向对象时,经常会谈到面向对象的三大特点:封装、继承和多态,正是由于这三大特点,面向对象的世界才如此的丰富。对于我来说,尽管接触了这么长时间的面向对象,但如果要我具体说一下这三大特点,我可能还要查资料才说的明白,所以还是有必要把整个学习的过程记录下来。

封装 Encapsulation

在讨论这三大特点时,往往最容易忽视的就是封装,而在开始学习的时候,初学者并没有太在意这个问题,因为刚开始接触的时候,不管是声明属性还是方法,直接public ...,因为这是最简单而且有效的,当然书上包括老师上课也讲了publicprotectedprivate的一些关系,但新手并不会对项目的工程化做太多的思考,代码跑起来就行了,这在阅读别人的代码时一眼就能看出些这个代码的人是新手还是老手,是否真正理解封装,善用访问修饰符。

public可以用啊,为什么还要使用其他的访问修饰符呢?public, protected, private的访问权限:

  • 任何地方都可以访问public属性和方法
  • 当前类才能访问private属性和方法,子类也不能访问
  • 当前类和子类都可以访问protected属性和方法,其他的外部代码都不能访问

先理解什么是封装?简单的说,封装就是通过众所周知的接口将用户与实际应用程序的内部工作原理分离。人都有好奇心,总喜欢朝着未知探索,喜欢把东西拆开,了解里面的小零件如何工作,虽然能得到精神上的满足,但有时深入了解事物内部的工作原理并不是必要的,我们每天都在使用计算机和手机,但很少有人真正了解它的工作原理,但即便不理解原理,也不想我们使用它们,因为接口隐藏了这些细节。

要怎么实现呢?最简单的方式就是给属性和方法添加访问修饰符(public, protected, private)。通过对客户端代码隐藏属性,就可以防止对对象的属性恶意修改了。其次还可以通过制作接口,把细节隐藏起来。

public这里不多讲,先来讲protected吧,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class Animal {
protected $name = "default name";

protected function say() {
echo "Name: " . $this->name . "\n";
echo "animal say\n";
}
}

class Dog extends Animal {

public function f1() {
$this->say();
}
}

$dog = new Dog();
$animal = new Animal();

echo $dog->f1();

echo $animal->name;
?>

可以看到Dog类继承自Animal类,在Dog类中可以获取到name属性,还能执行say()方法,结果中我们还看到,程序执行到23行就报错了,很明显,我们在外面定义了一个Animal变量,并试图访问protected修饰的name属性,导致程序出错。

再来看看private

1
2
3
4
5
6
7
8
9
<?php
class Foo
{
private $key = "This is a private value";
}

$foo = new Foo;
echo $foo->key; // Error 类外不能访问private属性
?>
类外不能访问private属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Foo
{
private $key = "This is a private value";
}

class Bar extends Foo
{
public function f1() {
echo $this->key . '\n';
}
}

$bar = new Bar;
$bar->f1(); // Error 子类中也不能访问private属性
?>
子类中也不能访问private属性

看到子类Bar并没有从父类Foo继承private属性。

继承 Inheritance

继承是从一个基类得到一个或多个派生类的机制。通过定义一个从其他类继承过来的类,我们可以确保一个类拥有其自由的功能和父类的功能(除private方法或属性),另一种理解继承的思路是“搜索”,当我们调用一个变量或方法时,会先在当前调用的类中查找有没有这个属性或方法,如果没有就查找父类中的默认实现。

现在有CDProductBookProduct两种商品,我么分别用继承和不继承来对比下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php
class CDProduct {
public $playLength;
public $title;
public $mainName;
public $firstName;
public $price;

public function __construct($title, $mainName, $firstName, $price, $playLength) {
$this->title = $title;
$this->mainName = $mainName;
$this->firstName = $firstName;
$this->price = $price;
$this->playLength = $playLength;
}

public function getPlayLength() {
return $this->playLength;
}

public function getSummaryLine() {
$base = "{$this->title} ( {$this->mainName}, {$this->firstName})";
$base .= " playing time = {$this->playLength}";
return $base;
}

public function getProucer() {
return "{$this->firstName} {$this->mainName}";
}
}

class BookProduct {
public $numPages;
public $title;
public $mainName;
public $firstName;
public $price;

public function __construct($title, $mainName, $firstName, $price, $numPages) {
$this->title = $title;
$this->mainName = $mainName;
$this->firstName = $firstName;
$this->price = $price;
$this->numPages = $numPages;
}

public function getNumberOfPages() {
return $this->numPages;
}

public function getSummaryLine() {
$base = "{$this->title} ( {$this->mainName}, {$this->firstName})";
$base .= " page count = {$this->numPages}";
return $base;
}

public function getProucer() {
return "{$this->firstName} {$this->mainName}";
}
}
?>

可以看到,在没有使用继承的情况下,我们做了很多重复的劳动,每个产品都有getSummaryLine()方法,每个类中的getProducer()方法完全相同,他们的构造方法中用同样的方法设置了许多相同的属性,这样的代码看起来一点都不干净,即使我们我可以不停地复制、粘贴,仍然存在问题,不利于后期代码的重构。我们现在使用继承来优化它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<?php
class Product
{
private $title;
private $mainName;
private $firstName;
protected $price;

public function __construct($title, $firstName, $mainName, $price) {
$this->title = $title;
$this->firstName = $firstName;
$this->mainName = $mainName;
$this->price = $price;
}

public function getFirstName() {
return $this->firstName;
}

public function getMainName() {
return $this->mainName;
}

public function getTitle() {
return $this->title;
}

public function getPrice() {
return $this->price;
}

public function getProucer() {
return "{$this->firstName} {$this->mainName}";
}

public function getSummaryLine() {
$base = "{$this->title} ( {$this->mainName}, {$this->firstName})";
return $base;
}
}

class CDProduct extends Product {
private $playLength = 0;

public function __construct($title, $firstName, $mainName, $price, $playLength) {
parent::__construct($title, $firstName, $mainName, $price);
$this->playLength = $playLength;
}

public function getPlayLength() {
return $this->playLength;
}

public function getSummaryLine() {
$base = parent::getSummaryLine();
$base .= " : playing time = {$this->playLength}";
return $base;
}
}

class BookProduct extends Product {
private $numPages = 0;

public function __construct($title, $firstName, $mainName, $price, $numPages) {
parent::__construct($title, $firstName, $mainName, $price);
$this->numPages = $numPages;
}

public function getNumberOfPages() {
return $this->numPages;
}

public function getSummaryLine() {
$base = parent::getSummaryLine();
$base .= " : plage count = {$this->numPages}";
return $base;
}
}

多态 Polymorphism

多态是来自希腊语的一个术语,原来的意思是“有多种形态”。多态的三大特征:

  • 子类继承父类
  • 子类重写父类
  • 父类指向子类

多态实现的前提:必须是类与类之间要有关系,要么继承,要么实现,存在重写(override),其实就是抽象函数或接口。
多态的应用:父类对象的引用指向子类对象,其实本质上就是一个向上转型。
多态的好处:大大提高程序的扩展,增强系统的灵活性,降低模块间的耦合。

举个模型例子,一家公司有员工类(Employee),还有其子类:销售(Sales)、市场(Market)、工程师(Engineer)等。某一天老板招待所有的员工开了个短会,完了之后对所有的员工(Employee)说,大家回去工作吧。在这里我们可以认为老板调用了所有员工对象的continueToWork()方法,而不是对一个个员工细说做什么工作,比如对销售部说你去制定销售计划(调用makeSalePlan();),对市场部说你去制定产品的价格(调用makeProductPrice();)….这种逐个细说的方式并不是面向对象,而是面向个体。可以确定的是,员工类应该有一个方法continueToWork()。而员工如何实现他们工作的方法却只有精确到子类才能确定,因为不同的员工的工作方式是不一样的。因此,我们很多时候只需要关心对象的父类型,而忽略更精确的子类型,比如上面老板叫大家回去工作时,他对全体员工说的,主要指的是全体员工类型。
上述的UML图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php
abstract class Employee{
abstract function continueToWork();
}
class Sales extends Employee{
private function makeSalePlan(){
echo "make sale plan";
}

public function continueToWork(){
$this->makeSalePlan();
}
}

class Market extends Employee{
private function makeProductPrice(){
echo "make product price";
}

public function continueToWork(){
$this->makeProductPrice();
}
}

class Engineer extends Employee{
private function makeNewProduct(){
echo "make new product";
}

public function continueToWork(){
$this->makeNewProduct();
}
}

class Demo{
public function Work($employeeObj){
$employeeObj->continueToWork();
}
}
//调用
$obj = new Demo();
$obj->Work(new Sales());
$obj->Work(new Market());
$obj->Work(new Engineer());
?>

总结

面向对象能将相同的属性封装在一起,同时又能复用很多代码,同时针对不同情况,还能实现多种形态,这就是面向对象的强大之处,有了它我们可以更方便地写出易于管理的代码,但作为初学者,要深刻理解它的三大特点,灵活运用也并非易事。
上课时,老师稍微对一个知识多讲了几遍就觉得不耐烦,觉得自己已经懂了,但自己真正遇到的时候,又觉得自己什么都不懂,书到用时方恨少,学习不能浅尝辄止,蜻蜓点水,要静下心来学习。

PHP的自动加载机制

什么是自动加载?

​ 顾名思义,就是当我们在使用一个东西的时候,如果这个东西还不存在,于是我们所处的环境就自动帮我们把需要的那个东西拿过来供我们使用,通俗地讲就是这个意思。

PHP中的自动加载

​ 我们在开始接触PHP时,会先写一个PHP文件,当项目比较大了,就会写好多PHP文件,在不同的PHP文件中,如果要用到其他PHP文件中的内容,比如实例化其他文件中的类,就会使用到requireinclude等函数。

1
2
3
4
5
6
7
8
9
// Foo.php
class Foo{
...
}

// Bar.php
require 'Foo.php';

$foo = new Foo;

​ 这种方法看起来很好,确实在一些古老的PHP项目中确实是这样做的,比如ECShop,但一旦这个项目变大,并且还使用了面向对象技术,各种各样的类文件多到很难管理时,requireinclude等技术还方便吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
// index.php
require 'A.class.php';
require 'B.class.php';
require 'C.class.php';
require 'D.class.php';
require 'E.class.php';
...
require 'Z.class.php';

$a = new A;
$b = new B;
...
...

​ 这么多的require,你不会在一个现代PHP框架中见到。那么,在现代PHP中是怎么解决这种问题的呢?

​ 在PHP 5以后,PHP拥有了完善的面向对象部分,PHP中使用了autoload技术来解决在一个文件中include多个PHP脚本。

相关技术

_autoload

1
2
3
4
5
/**
* 用于加载未定义的类
* @param $class 需要加载的类名
*/
void __auto(string $class)

注意:此方法已经在PHP 7.2中标记为DEPRECATED,不建议继续使用

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//myClass.php
<?php
class myClass {
public function __construct() {
echo "myClass init'ed successfuly!!!";
}
}
?>

// index.php
<?php
function __autoload($classname) {
$filename = "./". $classname .".php";
include_once($filename);
}

$obj = new myClass(); // 输出:myClass init'ed successfuly!!!
?>

​ 在index.php中我们并没有指明把myClass.php加载进来,但是依然可以实例化myClass对象。

spl_autoload

1
2
3
4
5
6
/**
* __autoload()的默认实现
* @param $class_name 需要被实例化的小写类名或命名空间
* @param $file_extensions 文件扩展名,默认为.inc或.php
*/
void spl_autoload ( string $class_name [, string $file_extensions = spl_autoload_extensions() ] )

​ 该函数是__autoload()函数的默认实现,如果在调用spl_autoload_register()函数时没有设置任何参数,那么此函数将作为默认加载函数被注册,意思是说如果使用spl_autoload_register()来自动加载时,如果没有指定特殊的加载器,那么将通过spl_autoload()来加载。

​ 通常使用该函数时,还需要一些其他的函数配合使用,比如set_include_path()spl_autoload_extensions()等。

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// class/Foo.class.php
class Foo
{
public function __construct() {
echo "Foo init successfully!!!";
}
}

// index.php
set_include_path('class/');
spl_autoload_extensions('.class.php');
spl_autoload('Foo');

$foo = new Foo;

​ 可以看到,在spl_autoload()中不能对传过来的class_name做一些判断或修改,需要配合其他函数来设置,此时如果想要在自动加载时做一些判断修改,只能使用__autoload()或者下面的spl_autoload_register()

spl_autoload_register

1
2
3
4
5
6
7
8
/**
* 注册一个自动加载器,这个注册的加载器可以替换__autoload()
* @param $autoload_function 需要被注册的自动加载函数,如果没有指定,会将spl_autoload()函数注册
* @param $throw 是否允许函数抛出异常,默认为true
* @param $prepend 是否将这个函数加载器放在队列的最前面,默认为false
* @return bool
*/
bool spl_autoload_register([ callable $autoload_function [, bool $throw = TRUE [, bool $prepend = FALSE ]]])

​ 此函数可以可以注册一个带有spl __autoload队列的函数,并且将会启用这个队列。

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// class/Foo.class.php
class Foo
{
public function __construct() {
echo "Foo init successfully by spl_autoload_register!!!";
}
}

// index.php
// 自定义一个加载器,再加载器中可以对参数$class 做一些判断
function my_autoloader($class) {
include 'classes/' . $class . '.class.php';
}

spl_autoload_register('my_autoloader');
// 或者使用一个匿名函数作为加载器
spl_autoload_register(function ($class) {
include 'class/' . $class . '.class.php';
});

$foo = new Foo;

通过命令空间来注册:

1
2
3
4
5
6
7
8
9
10
11
12
// index.php
namespace Foobar;

class Foo {
static public function test($class) {
echo $class . " autoloaded by namespace";
}
}

spl_autoload_register(__NAMESPACE__ .'\Foo::test');

new AClass;

​ 学了这么多,我们知道了什么是自动加载技术。再来回到文章开头,那个不存在的东西就是我们将要实例化的类,我们所处的环境就是PHP这个环境,自动把没有的东西拿过来给我们用就是使用以上技术加载所需要的类文件,以供我们实例化所需。

Laravel中的自动加载

从Nginx的配置文件说起

官方建议的Nginx服务配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
server {
listen 80;
server_name example.com;
root /example.com/public;

add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";

index index.html index.htm index.php;

charset utf-8;

location / {
try_files $uri $uri/ /index.php?$query_string;
}

location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }

error_page 404 /index.php;

location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}

location ~ /\.(?!well-known).* {
deny all;
}
}

关于该配置为详细说明,就不在此说了,有兴趣的可以查看这篇文章:Laravel 5.5 官方推荐 Nginx 配置学习 。从配置文件中发现Laravel项目的入口文件是public/index.php,于是,就从该文件开始分析。

public/index.php文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader for
| our application. We just need to utilize it! We'll simply require it
| into the script here so that we don't have to worry about manual
| loading any of our classes later on. It feels nice to relax.
|
*/

require __DIR__.'/../bootstrap/autoload.php';

可以看到,在入口文件中引入了bootstrap/autoload.php这个文件,根据文件名可以大致猜出和自动加载有点关系,而且看注释,就是使用了Composer的自动加载,因为Laravel本身会从vendor目录引用很多第三方库,继续看bootstrap/autoload.php

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
|--------------------------------------------------------------------------
| Register The Composer Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
|
*/

require __DIR__.'/../vendor/autoload.php';

bootstrap/autoload.php文件中引入了vendor/autoload.phpvendor/里面保存的是第三方库,继续查看vendor/autoload.php

1
2
3
4
5
6
7
<?php

// autoload.php @generated by Composer

require_once __DIR__ . '/composer/autoload_real.php';

return ComposerAutoloaderInit9794f3f3ddfd00f32b75ea45e287b507::getLoader();

只有两行代码,一个是引入vendor/composer/autoload_real.php,最后调用ComposerAutoloaderInit9794f3f3ddfd00f32b75ea45e287b507::getLoader()返回,看到这里知道既然从这里返回,那么getLoader()方法里面肯定做了一些自动加载的事情。查看该方法。

这是一个静态方法,首先检查静态属性loader是否被实例化,如果已经实例化了则直接返回,否则继续执行下面的代码,很明显这是一个单例模式,Laravel项目中每次只保留一个loader。接下来就用到了spl_autoload_register()函数:

1
2
3
4
// 注册loadClassLoader加载器
spl_autoload_register(array('ComposerAutoloaderInit9794f3f3ddfd00f32b75ea45e287b507', 'loadClassLoader'), true, true);
// 创建一个loadClassLoader实例
self::$loader = $loader = new \Composer\Autoload\ClassLoader();

可以看到,先是注册了一个函数名叫loadClassLoader的加载器,再通过这个加载器开始类的加载。看一下这个加载器的实现:

1
2
3
4
5
6
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}

很简单,就是判断要加载的不是Composer\Autoload\ClassLoader这个类,如果是就引入该类,否则什么也不做,这就是自动加载。

我们来检验一下,分析的是否正确,修改loadClassLoader()函数如下:

1
2
3
4
5
6
7
8
9
10
11
public static function loadClassLoader($class)
{
// 引入Foo这个类
if ('Foo' === $class) {
echo 'Foo from loadClassLoader';
exit();
}
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}

getLoader()中添加一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static function getLoader()
{
// 单例模式
if (null !== self::$loader) {
return self::$loader;
}
// 注册loadClassLoader加载器
spl_autoload_register(array('ComposerAutoloaderInit9794f3f3ddfd00f32b75ea45e287b507', 'loadClassLoader'), true, true);
// 创建一个loadClassLoader实例
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
// 创建为定义的类的实例
$foo = new Foo;
...

运行结果如下:

运行结果和我们猜想的一样。

总结

​ 学习了PHP自动加载技术,发现其实一点也不神奇,主要就是依靠spl_autoload_register()spl_autoload()函数,底层还是使用了include的方式,所谓的自动,只是不需要我们每次都在PHP文件的顶部写很多include命令,只要把使用了自动加载函数的文件引入进来,当执行到未定义的类时,就会自动引入相关的类文件。为达到效果,其实也完全可以用一个循环来引入所需要的类,只是在循环引用前,需要把类名和类所在的文件做一个映射,但这样做完全没有使用自动加载函数来的优雅,而且还不灵活,还没有使用到命名空间。

参考:http://php.net/manual/en/language.oop5.autoload.php