解决PHP中htmlspecialchars返回null的问题

在开发中,经常需要对用户传过来的数据进行过滤,来阻止一些用户的恶意输入,在PHP中常用到htmlspecialchars()htmlentities()strip_tags()函数来处理。

今天在使用htmlspecialchars()做特殊字符的转换时,一直返回null,当我换成htmlentities()也是同样的问题,查官方手册才发现原来是编码的问题。

函数声明:

问题主要处在第三个参数上,对于第三个参数官方是这么说的:

这个参数的作用是设置转换字符时采用的编码,在PHP 5.4 和5.5中使用了UTF-8作为默认编码,而在PHP 5.4之前使用ISO-8859-1作为默认编码,从PHP 5.6开始就使用PHP配置文件中的default_charset参数作为默认编码。encoding参数支持的字符集:

从PHP的更新日志也能看到这个参数的变化:

一般我们是这么使用的:

1
2
3
$str='<a href="test.html">\'测试页面\'</a><script>alert(213)</script>';
echo htmlspecialchars($str);
// 输出:&lt;a href=&quot;test.html&quot;&gt;'测试页面'&lt;/a&gt;&lt;script&gt;alert(213)&lt;/script&gt;

今天在使用的时候,结果一直返回null,其实就是编码的问题,这个时候就需要用到第三个参数了:

1
echo htmlspecialchars($string, ENT_COMPAT,'ISO-8859-1', true);

同样,也适用于htmlentities

1
echo htmlentities($string, ENT_COMPAT,'ISO-8859-1', true);

MySQL中的索引学习

什么是索引?

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

其实索引这个东西我们很早就接触过了,想象一下当初学习《现代汉语词典》的时候,老师是如何教我们使用的,就可以理解什么是索引了。《现代汉语词典》有将近1800页,里面的汉字大概有1.3万多个,我们是如何在这么多的汉字中找到某个字呢?老师大概是这么教我们的:词典提供了“音节表”,“音节表”将所有汉子的汉语拼音编入其中,并且“音节表”按’a’到’z’的顺序排序,故而我们可以轻松的在音节表中找到某个字所对应的页数。其实不光是词典,我们看的书大部分都有目录,而这个目录就相当于索引的作用,如下是三本书的目录:

《禅与摩托车维修艺术》的目录

《黑客与画家》的目录

《计算机程序的构造和解释》的目录

通过上面三本书的目录的对比,很明显第一本书的目录写的很简单,第二本书稍详细一点,而第三本书的目录最详细,目录写得越详细,读者在阅读的时候就越方便。

不管是词典中的音节表还是书的目录,它们实际都是索引的一种存在形式,都是为了能够提高查找的效率。

数据库中索引的本质

本质上,索引其实是数据库表中字段值的复制,该字段称为索引的关键字。

索引在计算机中的存储

在学习数据结构时,曾讲到文件的概念,而数据库中所有的数据都是以文件的形式保存的计算机中的,所以文件系统是数据库系统的基础。而数据库在操作这些文件时,都是先将文件中的数据加载到内存,然后再以某种数据结构来操作这些数据,通常的数据结构可以是AVL树B树B+树等。数据库中的往往是要存大量的数据的,而所有的数据不可能一次性全部加载到内存中,这时候就需要索引与分块加载来提高查询的速度,不过一般初学者不会体验到索引带来的高效,因为他们的电脑内存足够大完全可以一次性加载完所有数据,这也导致初学者忽略了对索引的仔细学习。如下是MyISAM存储引擎的索引示意图:

MyISAM存储引擎teacher表的主索引及普通索引

更多关于数据库索引背后的数据结构和算法原理,请查看:https://www.cnblogs.com/tgycoder/p/5410057.html

MySQL中的索引

在MySQL中,共有4类索引:主键索引、唯一索引、常规索引和全文索引。

主键索引(Primary key)

主键索引是关系数据库中最常见的索引类型。它根据主键自身的唯一性来唯一标识每条记录。因此该键必须是表中的唯一值。创建主键索引很简单,如下:

每个表只能有一个自增字段,该字段必须指定为主键。此外,任何指定为主键的字段不能是NULL,即使没有声明为NOT NULL,MySQL会自己设置。

唯一索引(Unique)

与主键索引一样,唯一索引可以防止创建重复的值。不同之处在于每个表只能有一个主键索引,但可以有多个唯一索引。修改上面的sql语句来增加唯一索引:

如前面所讲,表中可以指定多个唯一字段,我们现在将name字段也设置为唯一:

还可以指定多列唯一索引,比如上面的表结构中,允许用户插入重复的url,甚至可以插入重复的name值,但不希望出现重复的name和url的组合。可以创建多列唯一索引来强制这种约束,如下:

常规索引(Index)

可能经常需要对数据库的搜索效率进行优化,以便能够根据并非主键甚至并非唯一的列获取数据行。为此,最有效的方法是采用某种方式索引列,是的数据库尽可能快的查找想要的值。这就称为常规索引。

单列常规索引

如果表中的某个列经常需要被查询到,就应该使用单列常规索引。假如,一张员工表you 4列:主键id,名字,姓氏和电子邮箱,若果大多数的查询操作都会针对员工的姓氏或者电子邮箱,因为员工的姓氏可以有重复的,而电子邮箱必须唯一,我们就可以给姓氏创建一个常规索引,电子邮箱创建一个唯一索引,如下:

同时,MySQL还提供了创建局部字段索引的特性,方式是:将某一字段的前N的字符作为索引,相比于单列常规索引,局部索引需要更少的磁盘空间,同时由于索引的数据短,因此效率会高一点。修改前面的示例,因为通常lastname的前5个字符就足以确保获取到唯一的数据,修改如下:

多列常规索引

一般查询时,通常需要包含多列,我们可以指定一些经常被查询的列为多列常规索引。MySQL的多列常规索引方法基于一种最左前缀(leftmost prefixing)的策略。最左前缀指出包含列A、B和C的任何多列索引都可以提高涉及如下列组合的查询的性能:

  • A、B、C
  • A、B
  • A

创建多列索引:

上图中创建了三个索引,第一个是主键索引,第二个是电子邮箱的唯一索引,第三个就是多列索引,包括两列:lastname和firstname。

全文索引

当字段类型为char、varchar或text时,我们可以使用全文索引。在MySQL中,对大量自然语言做全文搜索时,它提供了一种数据获取的机制,并生成最符合用户需要的结果。比如在对这样“Nginx is the world’s most popular web server”的字符串进行搜索时,单词is和the对结果几乎起不了任何作用,MySQL在搜索时,将文本分解为单词,默认忽略少于4个字符的单词。创建全文索引:

创建全文索引的方式与创建其他索引没什么区别,但基于全文索引的查询却有一点不同。在查询时,select语句需要使用两个特殊的MySQL函数match()againet()。查询方式如下:

Boolean全文搜索

Boolean全文搜索对查询提供了更加细的控制允许显示地标识候选结果中应当或不应当出现哪些词(MySQL默认会忽略少于4个字符的关键词)。以下是关于Boolean操作符的描述:

+ 前导加号确保后面的单词出现在每个结果记录中
- 前导减号确保后面的单词不出现在任何结果记录中
* 结尾处的星号允许接受关键字变体,只要该变体以星号前面的单词所制定的字符串开头
“ “ 外围的双引号确保结果记录包含所包围的字符串,要严格按照输入时的形式出现
< > 前导的大于号和小于号分别用于增加和减少后面单词的搜索级别相关度
( ) 小括号用于将单词分组为子表达式

如下是几个简单的例子:

  1. 返回包含Nginx,但不包含manual的记录:

  1. 返回包含单词Nginx,但不包含Apache或Tomcat的记录:

  1. 返回包含web和scripting或者php和scripting的记录,但web scripting的搜索级别低于php scripting:

注意,由于MySQL默认会忽略少于4个字符的单词,所以执行上面的SQL命令时,应该先修改ft_min_word_len参数。

索引选取的注意事项

  1. 只对WHEREORDER BY子句中需要的列添加索引,过多的索引会占用很多硬盘空间,在修改数据时还会降低性能,因为每次修改数据时都需要更新索引。
  2. 如果创建如INDEX(firstname, lastname)的索引时,不要再创建INDEX(firstname),因为MySQL的索引采用最左前缀策略。
  3. 需要索引的列要不为空(NOT NULL),保证构建索引时不存储NULL值。
  4. 可以开启慢查询日志,通过对慢查询日志的分析来优化索引。

参考:
《PHP与MySQL程序设计(第四版)》

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编码。

副作用:
一个PHP文件可以定义符号(比如类、函数、常量等),或者执行只有唯一副作用的操作(比如输出结果、处理数据等),但是不能同时做这两件事,尽量是一个PHP文件的功能单一。在操作的时候尽量把变量、类、函数的声明分开,通过includerequire文件的方式来使用。

如下不符合规范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
// 改变设置
ini_set('error_reporting', E_ALL);

// 加载文件
include "file.php";

// 打印输出
echo "<html>\n";

// 声明
function foo()
{
// function body
}

符合规范如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
// 声明
function foo()
{
// function body
}

// 条件判断
if (! function_exists('bar')) {
function bar()
{
// function body
}
}

命名空间和类:
命名空间和类必须遵循PSR-4自动加载器标准。

类的名称:
每个类都有自己的命名空间,且都在顶级命名空间下,类名必须使用驼峰式(CamelCase)。
PHP 5.3 及以上,必须使用正式的命名空间,例如:

1
2
3
4
5
6
7
<?php
// PHP 5.3 及以后
namespace Vendor\Model;

class Foo
{
}

PHP 5.3一下应该使用Vendor_开头的伪命名空间约定,例如:

1
2
3
4
5
<?php
// PHP 5.3以下
class Vendor_Model_Foo
{
}

常量:
常量必须全部是用大写,并且使用下划线(_)分开。例如:

1
2
3
4
5
6
7
8
<?php
namespace Vendor\Model;

class Foo
{
const VERSION = '1.0';
const DATE_APPROVED = '2012-06-01';
}

类的方法:
类的方法必须使用小写字母开头的驼峰式(camelCase)命名。

PSR-2

PSR-2是对PSR-1的PHP的扩充。

贯彻PSR-1:
使用PSR-2代码标准之前要先贯彻PSR-1的代码标准。

文件和代码行:
PHP文件必须使用Unix风格的换行符(LF, linefeed),最后要有一个空行,仅包含PHP代码的文件而且不能使用PHP关闭标签?>,每行代码不应该超过80个字符,每行末尾不能有空格,每行只能有一条语句,可以在适当的地方添加空行提高代码的阅读性。

不加上?>关闭标签,可以避免意料之外的输出错误,如果加上关闭标签,且在关闭标签后有空行,那么空行会被当成输出,导致意想不到的错误。

缩进:
必须以4个空格为缩进,不能使用制表符(Tab键)缩进。

在不同的编辑器中,空格的渲染效果基本一致,而制表符的宽度各有差异。

关键字:
PHP的关键字必须使用小写,而且true, false, 和 null必须小写。

命名空间和use声明:
现在,namespace声明之后必须要有一个空行,而且use声明必须放在namespace之后,必须分别使用use引入命名空间,而且use后要有空行,例如:

1
2
3
4
5
6
7
8
<?php
namespace Vendor\Package;

use FooClass;
use BarClass as Bar;
use OtherVendor\OtherPackage\BazClass;

// ... additional PHP code ...

类的继承和实现:
extendsimplements关键字必须和类名在同一行,类、接口和Traits定义体的起始括号应该在类名之后新起一行,结束括号也必须新起一行,例如:

1
2
3
4
5
6
7
8
9
10
11
<?php
namespace Vendor\Package;

use FooClass;
use BarClass as Bar;
use OtherVendor\OtherPackage\BazClass;

class ClassName extends ParentClass implements \ArrayAccess, \Countable
{
// constants, properties, methods
}

如果implements后面后很多类导致一行很长,可以依次将需要的类另起新行并缩进4个空格,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
namespace Vendor\Package;

use FooClass;
use BarClass as Bar;
use OtherVendor\OtherPackage\BazClass;

class ClassName extends ParentClass implements
\ArrayAccess,
\Countable,
\Serializable
{
// constants, properties, methods
}

可见性:
类中的每个属性和方法都要声明可见性,有publicprivateprotected,不能使用var关键词来声明,老版本的PHP会在私有属性前加上_,一行只能声明一个属性,例如:

1
2
3
4
5
6
7
<?php
namespace Vendor\Package;

class ClassName
{
public $foo = null;
}

方法:
类中的所有方法也应该定义可见性,方法名后面不能有空格,方法体的括号位置和类定义体的括号位置一样,都要新起一行,结束括号也要新起一行。方法参数的起始圆括号之后没有空格,结束括号之前也没有空格,有多个参数是,每个参数的逗号后面加一个空格,例如:

1
2
3
4
5
6
7
8
9
10
<?php
namespace Vendor\Package;

class ClassName
{
public function fooBarBaz($arg1, &$arg2, $arg3 = [])
{
// method body
}
}

如果参数比较多,需要换行时,可以如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
namespace Vendor\Package;

class ClassName
{
public function aVeryLongMethodName(
ClassTypeHint $arg1,
&$arg2,
array $arg3 = []
) {
// method body
}
}

abstractfinalstatic
现在,abstractfinal必须在可见性修饰符之前,static声明必须放在可见性修饰符之后,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
namespace Vendor\Package;

abstract class ClassName
{
protected static $foo;

abstract protected function zim();

final public static function bar()
{
// method body
}
}

方法和函数的调用:
在调用方法和函数时,圆括号必须跟在函数名之后,函数的参数之间有一个空格:

1
2
3
4
<?php
bar();
$foo->bar($arg1);
Foo::bar($arg2, $arg3);

如果参数比较多,一行放不下时,如下处理:

1
2
3
4
5
6
<?php
$foo->bar(
$longArgument,
$longerArgument,
$muchLongerArgument
);

PHP的控制结构:
PHP的控制结构包括if、else、elseif、switch、case、while、do while、for、foreach、try和catch。如果这些关键词后面有一对原括号,开始括号前必须有一个空格,与方法和类的定义体不同,控制结构关键词后面的起始括号应该和控制结构关键词写在同一行,例如:

1
2
3
4
5
6
7
8
9
10
11
<?php
$gorilla = new \Animals\Gorilla;
$ibis = new \Animals\StrawNeckedIbis;

if ($gorilla->isWake() === true) {
do {
$gorilla->beatChest();
} while ($ibis->isAsleep() === true);

$ibis->flyAway();
}

PHP闭包函数:
闭包函数在声明时,function关键词后必须有一个空格,同时use关键词前后也必须有一个空格。起始大括号不需要另起新行,详细的如下代码:

1
2
3
4
5
6
7
8
<?php
$closureWithArgs = function ($arg1, $arg2) {
// body
};

$closureWithArgsAndVars = function ($arg1, $arg2) use ($var1, $var2) {
// body
};

闭包函数有多个参数时,处理方式和方法的参数一样:

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
<?php
$longArgs_noVars = function (
$longArgument,
$longerArgument,
$muchLongerArgument
) {
// body
};

$noArgs_longVars = function () use (
$longVar1,
$longerVar2,
$muchLongerVar3
) {
// body
};

$longArgs_longVars = function (
$longArgument,
$longerArgument,
$muchLongerArgument
) use (
$longVar1,
$longerVar2,
$muchLongerVar3
) {
// body
};

$longArgs_shortVars = function (
$longArgument,
$longerArgument,
$muchLongerArgument
) use ($var1) {
// body
};

$shortArgs_longVars = function ($arg) use (
$longVar1,
$longerVar2,
$muchLongerVar3
) {
// body
};

注意:以上规则同样适用于将闭包作为函数或方法的参数,如下:

1
2
3
4
5
6
7
8
<?php
$foo->bar(
$arg1,
function ($arg2) use ($var1) {
// body
},
$arg3
);

PSR-3

与PSR-1和PSR-2不同,PSR-3规定了一套通用的日志记录器接口(Psr\Log\LoggerInterface),为了符合PSR-3规范,框架必须实现该规范中的接口,这样可以更多的兼容第三方应用。PSR-3规范中包含了9个方法,每个方法都对应了RFC 5424协议的一个日志级别,而且都接受两个参数$message$context,如下:

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<?php

namespace Psr\Log;

/**
* Describes a logger instance
*
* The message MUST be a string or object implementing __toString().
*
* The message MAY contain placeholders in the form: {foo} where foo
* will be replaced by the context data in key "foo".
*
* The context array can contain arbitrary data, the only assumption that
* can be made by implementors is that if an Exception instance is given
* to produce a stack trace, it MUST be in a key named "exception".
*
* See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
* for the full interface specification.
*/
interface LoggerInterface
{
/**
* System is unusable.
*
* @param string $message
* @param array $context
* @return void
*/
public function emergency($message, array $context = array());

/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param array $context
* @return void
*/
public function alert($message, array $context = array());

/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
* @return void
*/
public function critical($message, array $context = array());

/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param array $context
* @return void
*/
public function error($message, array $context = array());

/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param array $context
* @return void
*/
public function warning($message, array $context = array());

/**
* Normal but significant events.
*
* @param string $message
* @param array $context
* @return void
*/
public function notice($message, array $context = array());

/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
* @return void
*/
public function info($message, array $context = array());

/**
* Detailed debug information.
*
* @param string $message
* @param array $context
* @return void
*/
public function debug($message, array $context = array());

/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
* @return void
*/
public function log($level, $message, array $context = array());
}

关于message参数:

$message必须是一个字符串或者是含有__toString()方法的对象,$message应该包含占位符,例如{placeholder_name},占位符由{、占位符名称和}组成,不能包含空格,占位符名称可以由A-Z, a-z, 0-9, _组成,第三方实现可以用$context参数来替换占位符,占位符名称必须$context数组的key对应。如下例子是使用$context中的值替换$message中的占位符:

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
<?php

/**
* Interpolates context values into the message placeholders.
*/
function interpolate($message, array $context = array())
{
// build a replacement array with braces around the context keys
$replace = array();
foreach ($context as $key => $val) {
// check that the value can be casted to string
if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) {
$replace['{' . $key . '}'] = $val;
}
}

// interpolate replacement values into the message and return
return strtr($message, $replace);
}

// a message with brace-delimited placeholder names
$message = "User {username} created";

// a context array of placeholder names => replacement values
$context = array('username' => 'bolivar');

// echoes "User bolivar created"
echo interpolate($message, $context);

关于context参数:

$context是一个数组参数,用于构造复杂的日志消息,$context中的值不能跑出任何PHP异常或错误。如果$context中包含Exception对象,则该对象的key必须exception

PSR-3日志记录器的使用

推荐使用monolog/monolog,这样可以让我们不需要浪费更多的时间在编写一个日志记录器了。Monolog组建完全实现了PSR-3接口,而且便于使用自定义的消息格式化程序和处理程序扩展功能,通过Monolog可以把日志消息写入文本文件、系统日志和数据库中,还能通过电子邮件发送,并且还支持Slack和远程服务器。如下展示了如何设置Monolog,并把日志消息写入文本文件:

1
2
3
4
5
6
7
8
9
10
11
use Monolog/Logger;
use Monolog/Handler/StreamHandler;

// 创建日志记录器
$log = new Logger('myApp');
$log->pushHandler(new StreamHandler('logs/development.log, Logger::DEBUG));
$log->pushHandler(new StreamHandler('logs/production.log', Logger::WARNING));

// 使用日志记录器
$log->debug("This is a debug message");
$log->warning("This is a warning message");

PSR-4

PSR-4规范描述了一个标准的自动加载器策略,指在运行时按需查找PHP类、接口或Traits。支持PSR-4自动加载器标准的PHP组建和框架,使用同一个自动加载器就能找到相关代码,然后将其载入PHP解释器。有了这个功能,就可以把现代PHP生态系统中很多客户操作的组件联系起来。

编写一个PSR-4自动加载器

PSR-4规范不要求改变代码的实现方式,只建议如何使用文件系统目录结构和PHP命名空间组织代码,PSR-4规范以来PHP命名空间和文件系统目录结构查找并加载PHP类、接口和Traits,这正是PSR-4的精髓所在。下面我们来自己手动实现一个PSR-4自动加载器:

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
<?php
/**
* 使用SPL组册这个自动加载函数后,遇到下述代码时这个函数会尝试 从/path/to/project/src/Baz/Qux.php文件中加载\Foo\Bar\Baz\Qux类:
* new \Foo\Bar\Baz\Qux;
* @param string $class 完全限定的类名。
* @return void
* /
spl_autoload_register(function ($class) {
// 项目的命名空间前缀
$prefix = 'Foo\\Bar\\';

// 目录前缀对应的根目录
$base_dir = __DIR__ . '/src/';

// 判断传入的类是否使用了这个命名空间前缀
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
// 没有使用,交给注册的下一个自动加载器处理
return;
}

// 获取去掉前缀后的类名
$relative_class = substr($class, $len);

// 把命名空间前缀替换成根目录,
// 在去掉前缀的类名中,把命名空间分隔符替换成目录分隔符,
// 然后在后面加上.php
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';

// 如果该文件存在,就将其导入
if (file_exists($file)) {
require $file;
}
});

这样我们就写好了一个PSR-4的自动加载器了,更多关于PHP的自动加载机器可以看我这篇文章

参考:

  1. 《Morden PHP中文版》
  2. https://www.php-fig.org

面向对象的再思考

为什么要再次思考

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

总结

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