也就是闲时为了写文章而写的一篇关于 Pimple 源码的阅读笔记。
Pimple 代码有两种编码方式,一种是以 PHP 编写的,另一种是以 C 扩展编写的方式,当然个人能力有限呀,也就看看第一种了。

Pimple 链接
官网 WebSite
GitHub - Pimple
Pimple 中文版文档

前提知识

ArrayAccess(数组式访问)接口

提供像访问数组一样访问对象的能力的接口。

http://php.net/manual/zh/class.arrayaccess.php

一个 Class 只要实现以下规定的 4 个接口,就可以是像操作数组一样操作 Object 了。

1
2
3
4
5
6
7
ArrayAccess {
/* 方法 */
abstract public boolean offsetExists ( mixed $offset )
abstract public mixed offsetGet ( mixed $offset )
abstract public void offsetSet ( mixed $offset , mixed $value )
abstract public void offsetUnset ( mixed $offset )
}

伪代码如下

1
2
3
4
5
6
7
8
9
10
11
12

class A implements \ArrayAccess {
// 实现了 4 个接口
}

$a = new A();

// 可以这么操作
$a['x'] = 'x'; // 对应 offsetSet
echo $a['x']; // 对应 offsetGet
var_dump(isset($a['x'])); // 对应 offsetExists
unset($a['x']); // 对应 offsetUnset

特别说明,只支持上面四种操作,千万别以为实现了 ArrayAccess,就可以使用 foreach 了,要实现循环 = 迭代,要实现 Iterator(迭代器)接口,其实 PHP 定义了很多 预定义接口 有空可以看看。

SPL - SplObjectStorage

SPL

SPL 是 Standard PHP Library(PHP标准库)的缩写,一组旨在解决标准问题的接口和类的集合。SPL 提供了一套标准的数据结构,一组遍历对象的迭代器,一组接口,一组标准的异常,一系列用于处理文件的类,提供了一组函数,具体可以查看文档。

SplObjectStorage

SplObjectStorage 是 SPL 标准库中的数据结构对象容器,用来存储一组对象,特别是当你需要唯一标识对象的时候 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SplObjectStorage implements Countable , Iterator , Serializable , ArrayAccess {

/*
* 向 SplObjectStorage 添加一个 object,$data 是可选参数
* 因为 SplObjectStorage 实现了 ArrayAccess 的接口,所以可以通过数组的形式访问,这里相当于设置 object 为数组的 key ,data 是对应的 value,默认 data 是 null
*/
public void attach ( object $object [, mixed $data = NULL ] )

/*
* 检查 SplObjectStorage 是否包含 object ,相当于 isset 判断
*/
public bool contains ( object $object )

/*
* 从 SplObjectStorage 移除 object ,相当于 unset
*/
public void detach ( object $object )
// 其他接口定义可以自行查看文档

}

SplObjectStorage 实现了 Countable、Iterator、Serializable、ArrayAccess 四个接口,可实现统计、迭代、序列化、数组式访问等功能,其中 Iterator 和 ArrayAccess 在上面已经介绍过了。

魔术方法 __invoke()

__invoke() 当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。

看一个例子吧,一目了然。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class CallableClass
{
function __invoke($x) {
var_dump($x);
}
}
$obj = new CallableClass;
$obj(5);
var_dump(is_callable($obj));

//output

//int(5)
//bool(true)

读源码

目录接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pimple
├── CHANGELOG
├── LICENSE
├── README.rst
├── composer.json
├── ext // C 扩展,不展开
│   └── pimple
├── phpunit.xml.dist
└── src
└── Pimple
├── Container.php
├── Exception // 异常类定义,不展开
├── Psr11
│   ├── Container.php
│   └── ServiceLocator.php
├── ServiceIterator.php
├── ServiceProviderInterface.php
└── Tests // 测试文件,不展开

PS, Markdown 写目录格式真是麻烦,后来找了一个工具 tree 可以直接生成结构。

Container.php

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
class Container implements \ArrayAccess
{
private $values = array(); // 存储 value 的数组
private $factories; // 存储工厂方法的对象,是 SplObjectStorage 的实例
private $protected; // 存储保护方法的对象,是 SplObjectStorage 的实例

// 存储被冻结的服务,新设置一个 service 的时候,可以在还没有调用这个 service 的时候,覆盖原先设置,这时不算冻结
// 一旦调用了这个 service 之后,就会存入 $frozen 数组,如果这时还想重新覆盖这个 service 会报错,判断逻辑在 offsetSet 实现。
private $frozen = array();

private $raw = array(); // 存储 service 原始设置内容,用于 ::raw() 方法读取
private $keys = array(); // 存储 key

public function __construct(array $values = array())
{
$this->factories = new \SplObjectStorage();
$this->protected = new \SplObjectStorage();

foreach ($values as $key => $value) {
$this->offsetSet($key, $value);
}
}

public function offsetSet($id, $value){}
public function offsetGet($id){}
public function offsetExists($id){}
public function offsetUnset($id){}
public function factory($callable){}
public function protect($callable){}
public function raw($id){}
public function extend($id, $callable){}
public function keys(){}
public function register(ServiceProviderInterface $provider, array $values = array()){}
}

Container 实现了 ArrayAccess 接口,这就可以理解为什么可以通过数组的方式定义服务了。

重要的 function 分析

1、offsetSet、offsetExists、offsetUnset 主要实现 ArrayAccess 的接口很容易看懂
2、factory、protect 主要逻辑是判断传入的 $callable 是否有 __invoke ,如果有的话,通过 SplObjectStorage::attach,存储 object 中
3、raw 获取设置的原始内容
4、key 获取所有的 key
5、register() 注册一些通用的 service

6、offsetGet()

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
public function offsetGet($id)
{
if (!isset($this->keys[$id])) { // 如果没有设置过,报错
throw new UnknownIdentifierException($id);
}

if (
isset($this->raw[$id]) // raw 里已经有值,一般来说就是之前已经获取过一次实例,再次获取的时候,就返回相同的值
|| !\is_object($this->values[$id]) // 对应的 value 不是 object ,而是一个普通的值
|| isset($this->protected[$this->values[$id]]) // 存在于 protected 中
|| !\method_exists($this->values[$id], '__invoke') // 对应的 value 不是闭包
) {
return $this->values[$id]; // 返回 values 数组里的值
}

if (isset($this->factories[$this->values[$id]])) { // 如果工厂方法里面设置了相关方法
return $this->values[$id]($this); // 直接调用这个方法,传入参数($this),也就是匿名函数中可以访问当前实例的其他服务
}

$raw = $this->values[$id];
$val = $this->values[$id] = $raw($this); // 初始化一般的 service ,传入($this) ,以后再调用都获取相同的实例
$this->raw[$id] = $raw; // 把原始内容存入 raw 数组

$this->frozen[$id] = true; // 在初始化之后冻结这个 key ,不能被覆盖

return $val;
}

7、extend()

扩展一个 service,如果已经被冻结了,也不能被扩展。
与上文说的直接覆盖还是有区别的,直接覆盖就是完全不管之前定义的 service ,使用 extend 是可以在原始定义上做出修改

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
public function extend($id, $callable)
{
// ... 一些判断逻辑省略

// 如果是 protected 的 service 还不被支持 extend
if (isset($this->protected[$this->values[$id]])) {
@\trigger_error(\sprintf('How Pimple behaves when extending protected closures will be fixed in Pimple 4. Are you sure "%s" should be protected?', $id), \E_USER_DEPRECATED);
}

if (!\is_object($callable) || !\method_exists($callable, '__invoke')) {
throw new ExpectedInvokableException('Extension service definition is not a Closure or invokable object.');
}

$factory = $this->values[$id];

// 主要是这两行代码
$extended = function ($c) use ($callable, $factory) {
return $callable($factory($c), $c);
};

if (isset($this->factories[$factory])) {
$this->factories->detach($factory);
$this->factories->attach($extended);
}

return $this[$id] = $extended;
}

未完待续。

还有一篇,主要关于 PSR11 兼容性的。