《深入理解 PHP》笔记

《深入理解 PHP》笔记

编程那点事 随便写写 PHP 5271 字 / 11 分钟

今天把两个月之前借的书——《深入理解 PHP:高级技巧、面向对象与核心技术》给看完了。 也算是有不少收获的,不仅温故熟悉了一下面向对象,也学到了很多新东西。 故写篇文章总结记录一下上面个人感觉比较重要的点,作为以后开发过程中的备忘。然后明天去图书馆把这本书给还了。

使用 printf() 和 sprintf()

PHP 是使用 C 编写的一门解释型语言,因此会发现 C 中的很多库函数其实在 PHP 中也会出现相类似的。其实我有个人生梦想,就是学好 C 然后去看 PHP 内核 不过不同的是,在 PHP 里就很舒服了,不需要再引入什么头文件。 比如 printf() 函数,其实就是 C 中的格式化字符输出,甚至连类型说明符都是一样! 比如说这样:

printf("I am %d years old.", 18);

sprintf(),同样也是对字符串进行格式化,但是与 printf() 不同的是,它并不是将字符串打印出来,而是返回字符串。 之前做的爬虫中有需要循环枚举学号,其中会考虑最后两位数因为位数不够需要补零,就可以用 printf() 来解决。

for($i = 1; $i < 50; $i++){
	echo('182714' . sprintf("%02d", $i));
}

同时,在拼凑比较复杂的 SQL 命令时也可以使用 sprintf() 使得代码更加清晰。

Apache .htaccess 重写

虽然现在自己的 VPS 上都是用的 Nginx,但是还是记一下 Apache 的 .htaccess 文件吧。个人感觉 Apache 的这种方式比 Nginx 写在站点的配置文件里面要科学的多。

设置目录可以重写

<Directory "/www/wwwroot">
	AllowOverride All
<Directory>

设置重写规则

<IfModule mod_rewrite.c>
#打开重写引擎
RewriteEngine on

#设置重写的根目录
RewriteBase /www/

#index.php 重写规则
RewriteRule ^(about|contact|this|that|search)/?$ index.php?p=$1

</IfModule>

这里重点解读一下那段 index.php 重写规则。 ^这个符号表明这个匹配模式匹配 URL 的开始,从 RewriteBase 的值开始算起。然后匹配的是括号中的那些词。 之后的斜线/后面跟着一个问号,这表明这个问号可存在可不存在,因为我们的 URL 可能会有 www.example.com/aboutwww.example.com/about/ 这两种形式。

如果匹配成功后,就会被重定向到index.php?p=X 中,其中的$1就是前面的$的值。

调整浏览器缓存

只需要用 header() 函数加请求头就行了,调整的缓存包括 Web 浏览器的缓存和代理服务器的缓存。 有四种标头:

  • Last-Modified (最后修改时间)
  • Expires (过期时间)
  • Pragma (编译提示)
  • Cache-Control (缓存控制) 使用方法:
header('Expires: Mon, 3 Dec 2018 00:00:00 GMT');
header('Pragma: no-cache');		//禁止对页面的缓存
header('Cache-Control: no-cache');

高级面向对象

instanceof

可以使用 instanceof 关键词来查看一个对象是不是输入一个类的类型:

if($obj instanceof Someclass){ ...

构造函数的调用

PHP 总是调用刚刚被实例化的类的构造函数。这意味着创建一个子类的对象的时候,父类的构造函数并不会被自动调用。 正是如此,在 CodeIgniter 框架中,继承于 CI_Controller CI_Model 的控制器、模型,在其构造函数中都需要使用 parent::__construct() 来先调用父类的构造函数。

方法的重写

在子类中与父类的方法具有完全相同名称以及参数数量的方法会变重写。 因此我觉得其实也可以把子类的 __construct() 构造函数的声明看作是对父类构造函数的重写。

final

被定义为 final 的方法不能被任何子类所重写:

final function myFunc(){...}

__toString() 方法

__toString 方法是当类的对象被用做字符串 String 类型时而会自动调用的。 类被用作字符串时的情况有很多。比如 echo() 一个类就是。

抽象类和方法

抽象类仅包含了对类中变量以及方法的声明,并不会涉及其详细的过程。因此而“抽象”。 定义抽象类或者方法只需要使用 abstract 关键词。

abstract class Pet{
	abstract public function getName();
}

书上的这个例子:我们只会需要继承于 Pet 这个父类的其它子类,而并不会去实例化一个 Pet 对象。因此,Pet 可以被声明成一个抽象类。 抽象类是无法被实例化的。 更严谨的说法:一个抽象类定义了接口:这个基类(父类)的继承类如何被调用。然后子类会负责定义这些接口的真正实现。

接口与抽象类的区别

创建一个接口的方式:

interface iSomething(){
	public function someFunction($var);
}

要将一个类与一个接口相关联,需要使用 implements 操作符:

class Someclass implements iSomething(){}

而抽象类与接口的区别其实十分的微妙。一个抽象类是会被扩展为一个特定的类。 而一个接口不能被一个类所继承,接口更多的像是一个类所具有的方法的规则或者合约。 接口规定了类的方法。总而言之,接口与类之间并不存在继承关系。

复制和克隆对象

复制对象

在 PHP 中使用等号进行两个变量之间的赋值时,PHP 只是创建了被复制对象的一个引用,而并不是产生一个一模一样的新对象。

$a = new SomeClass();
$a->val = 1;
$b = $a;
$b->val =2;

echo($a->val);		//2

可以看到,对于$b修改的同时$a也会被修改,这就是复制。PHP 这样做是考虑到性能的因素,而不去复制多个副本,仅仅传递引用。

而如果要真正分开两个对象,使其相互独立,就需要创建一个对象的克隆:

$a = new SomeClass();
$a->val = 1;
$b = clone $a;
$b->val =2;

echo($a->val);		//1

这里使用了 clone,PHP 会执行所谓的“影子复制”。 值得一提的是,当对象被克隆时,它的__clone() 方法会被自动调用。

traits

从 PHP 5.4 起,PHP 加入了 traits。这个东西感觉应该挺少见的。 这是用来解决像 PHP 这样的面向对象语言中只支持单继承的问题。真的是长见识了 我们可以使用 trait 关键字来创建一个 trait:

trait tSomeTrait{
	function someFunction(){
		//Do something
	}
}

这其实跟声明一个类挺相似的。之后呢,在一个类中,我们可以使用use来向类中加入一个 trait:

class SomeClass{
	use tSomeTrait;
}

之后这个SomeClass的类中就有了 someFunction 这个方法了!我们可以直接就在它实例化的对象上进行调用。 因为use关键字可以随意使用,因此也就实现了多继承。

参数的类型提示

这应该是会被我经常忽略的一个点,因为不使用类型提示,PHP 也不会怎么样。 但是之前看杭电助手学长写的代码中有用,所以也在督促自己今后写项目中要慢慢用上。 其实概念很简单:

class SomeClass{
	function doThis(OtherClass $var){
		
	}
}

我们在这里限制了传入的参数的类型为OtherClass,之后调用函数中,如果我们传入的参数类型不正确,PHP 会直接报致命错误。 这在一定程度上其实也可以避免一些很难缠的 bug 的产生。同时,在 VS Code 等 IDE 的代码提示中也会提示出所要传入参数的类型。

命名空间

因为之前一直都是用 CodeIgniter 框架进行开发,所以完全没接触过命名空间。(CodeIgniter 4 中将会像 Laravel、ThinkPHP 等框架一样大量使用命名空间) 这并不是什么比较难的概念,只是我之前一直没用过,所以记录一下。 命名空间使用用来解决类、接口等重名的问题的。只要指定了各种输入哪个命名空间,就不会弄混。

使用命名空间

namespace MyNamespace\Company{
	
}

调用时:

$hr = new \MyNamespcae\Company\Foo();

这样就不会因为有两个相同名字的 Foo 类而弄混报错了。

单例模式

其实设计模式这一章我看完后也是云里雾里的,前几个还好理解,到后面就不太清楚其优点到底在哪。嘛,以后肯定还是要再加深学习一下的,毕竟也算是程序员软技能之一。那我就只能记录一下自我感觉理解的比较好的单例模式吧。

我十分佩服想出单例模式实现方法的人。 比如 CodeIgniter 框架中的数据库类,就是一个单例模式的例子。整个应用里面只有一个数据库示例,就是 $this->db。因为对于数据库的连接操作等只需要一个对象来负责就行了。 实现单例模式就显得十分巧妙了:

private functon __construct(){ }
private function __clone(){ }

是不是很妙?可能因为思维定式,我一直很自然的就会把构造函数当做 public 公开的,完全没想过将其声明成 private 的情况——这使得这个类无法在外部被实例化,因为实例化就是在尝试调用一个私有方法然后会报致命错误。同样对__clone()方法也是同样的声明,使得无法被克隆成两个独立的对象。 而类本身会提供静态方法来供外部调用。静态变量,静态方法是属于类本身的,这就保证了唯一性。同时还可以用静态变量来判断类是否初始化了。

PDO

嘛,第一次接触 PDO,也算是入了个门。之前部长问的那个问题让我感觉其实自己连门都没入 自我感觉又是一种与数据库进行交互的方式,这里就只聊聊比较熟悉的 MySQL 数据库。 PHP 的官方文档中记载的是 MySQL 数据库的连接是有过程化风格与面向对象风格两种。平时我是习惯用过程化风格。

//过程化风格
$conn = mysqli_connect('localhost', 'root', 'root', 'my_db');
$query = mysqli_query($conn, 'SELELCT * FROM `Users`');

//面向对象风格
$sql = new mysqli('localhost', 'root', 'root', 'my_db');
$sql->query('SELELCT * FROM `Users`');

然后 PDO 又是一种新的方式:

$pdo = new PDO('mysql:dbname=my_db;host=localhost', 'root', 'root');

$num = $pdo->exec('DELETE FROM `Users`');		//简单的查询,会返回影响到的行数
$data = $pdo->query('SELECT * FROM `Users`');		//会返回查询的结果

PDO 好就好在它可以连接其它类型的数据库,比如之前我那个菜鸡框架项目 Cube 里使用到的 SQLite 也是可以的。

$pdo = new PDO('selite:/www/database/db.sq3');

而关闭与数据库的连接只需要使用 unset() 销毁这个对象就行。 需要主要的是,第一个参数提供的是 DSN (Data Source Name),用来指明数据库。

PHP标准库 (SPL)

这也是托 C 语言的福,PHP 中也有了像 C 一样强大~~(可能吧)~~的标准库。 其实也还是有不少很有意思的功能的。

文件处理

SPL 中有一些处理文件、目录的类。它们都是以 Spl 开头的,我稍不留神就会看成 sql,哈哈哈。

获取文件信息 SplFIleInfo()

$file = new SplFIleInfo('test.php');

$file->getExtension();		//扩展名
$file->getSize();		//文件大小
$file->getRealPath();		//文件绝对路径

写入文件 SplFileObject

$fp = new SplFileObject('data.txt', 'w');
$fp->fwrite('Hello!');

这个 SplFileObject()并没有 close() 的方法来关闭。当使用 unset() 来删除对象时会自动关闭。

迭代器

之前看到 mian 学长在推特上面发了他写 C++ 时迭代器的一些事。当时从字面意思上将迭代器理解成一个类似 for 循环的东西。 现在想想……还是太天真。 迭代器 是一种设计模式。它的作用是使复杂数据结构的组件都可以使用循环来访问。 比较简单是使用foreach()来对数据进行迭代,然后访问其中的值。 其实我感觉更像是你定义了一种访问数据的方法。 只需要确定了 SPL 的 Iterator 接口中的下列方法,就可以对数据进行迭代了。

  • current() 返回当前数据项
  • key() 返回当前数据项在数据中位置(比如数组的下标)
  • next() 切换到下一个数据
  • rewind() 重置键值或者位置
  • valid() 返回一个布尔的变量,表明当前的键是否指向数据值,即有可能会溢出啊什么的

嘛,我其实也还不太懂,写一个小的 DEMO 吧:请再一次原谅我的恶趣味 这其实只是用迭代器来遍历一个简单的一维数组,其实用for循环就行了,我并不是小题大做,而只是给个演示的思路,以便日后自己快速回忆。

class MyWife implements Iterator{
	private wife = [];
	private $_position = 0;		//设置当前的位置为0.
	
	public function add($_wife){
		$this->wife[] = $_wife;
	}
	
	//返回当前数据的方法
	function current(){
		return $this->wife[$this->_position];
	}
	
	function key(){
		return $this->_position;
	}
	
	function next(){
		$this->_position++;
	}
	
	//回到原点
	function rewind(){
		$this->_position = 0;
	}
	
	function valid(){
		return (isset($this->wife[$this->_position]));
	}
}

之后就可以使用 foreach() 来进行迭代了!

$wife = new MyWife();
$wife->add('Mashiro');
$wife->add('Asuna');
$wife->add('Alice');
$wife->add('Emiria');
//嗯,没毛病...

foreach($wife as $w){
	echo($w);
}

数据结构(栈)

SPL 也可以涌来时间数据结构,然而数据结构这一块我并没有学过,所以只能拿唯一知道一点的“栈”来做个例子。 嘛,LIFO,后进先出。使用 SPL 会比用数组来实现好很多。

$wife = new SplStack();
$wife->push('Asuna');
$wife->push('Alice');
$wife->push('Mashiro');
$wife->pust('Emiria');		//被我一个个给“推”了,嘻嘻。

$wife->pop();		//Emiria

同时还有先进先出(FIFO)的列表,也可以使用SplQueue来实现。

cURL

cURL 最初我是在 Google Chrome 的开发者面板里面接触的,当时把 Connect 面板中的请求的 cURL 复制到 bash 中访问。 之后在做数字杭电的模拟登录时也大量用到了 cURL。可以说,PHP 中只要一牵扯到 POST 请求,那就 cURL 没的跑了。 其实 cURL 也就是设置好各种参数,然后再 curl_exec() 即可。

$curl = curl_init('www.example.com');
curl_setopt($curl, CURLOPT_POST, 1);	//使用 POST 方式请求
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);		//支持重定向
curl_setopt($curl, CURLOPT_POSTFIELDS, 'name=john&pass=123');		//POST请求的数据,之前在这里踩过坑

curl_exec($curl);		//发射!!
curl_close();

cron 任务

cron 是 UNIX 服务器上的一种任务,也就是按时自动执行任务。这个宝塔面板里有,就是那个任务计划,其实原理就是使用到了 cron。 运行的指令是保存在名为 crontab 的文件中,从上到下一个个运行。 每一行个格式如下:

30 22 * * * wget -q https://wuhan5.cc

每一行由制表符隔开六个域。前五个依次表示:分钟、小时、天、月、一周中的第几天(0 表示周日)。 甚至还可以使用连字符,比如 1-6,或者逗号分隔符,1,3,5。

查看 crontab 内容

crontab -l

PHP 命令行界面

这是我十分感兴趣的一部分,之前在洛谷上做题时,用得就是 PHP。写好的代码是在命令行里进行评测的。因此当时也接触了一点 PHP 命令行当中的输入输出。

执行一段代码

php -r 'echo "Hello!";'

注意这需要是完整的一行 PHP 代码,行末尾需要加分号,头部不需要加<?php

进入可交互界面

php -a

进入的就是像 Python 一样的交互界面,可以用来临时测试一些小的代码。

运行命令行脚本

php -f "myscript.php"

这里的f是文件 file,这样就好记一些了。如果不是在当前目录下的话,可以 cd 过去。

使用命令行参数

<?php
echo($_SERVER['argc']);
var_dump($_SERVER['argv']);
?>

这里的$_SERVER['argc']是传入参数的个数。$_SERVER['argv']是一个数组,为传入的参数的值。 注意,其中脚本的名称本身就是第一个参数:$_SERVER['argv'][0]

接收输入

$input = fgets(STDIN);

直接使用fgets()获取文件内容,然后将文件设置成标准输入STDIN即可。这使我想到了前几天在 《C Primer Plus》上看到的输入输出那里,如果我们在命令行里把输入的流重定向到一个文件,不就可以读取文件内容了?如果我们把输出的流由屏幕重定向一个文件,不就可以读取文件内容了?这些在看到 PHP 接收输入这边又多了一层理解。

内置服务器

这个内置服务器是被各种强调了无数次:千万不要在生产环境上使用!!(第一次是在 CodeIgniter 的文档) 不过启动开起来真的很爽。

php -S localhost:8000

在终端中可以看到实时的请求情况。

单元测试

我还记得初中时,那是我还在写 ActionScript 的时候,当前同班的同学因为我不会单元测试,不会 Git 版本管理而嘲笑我。 在后来渐渐接触到 Jenkins 等持续集成服务后,才开始知道 PHPUnit。 但我一直觉得,我给你写好了期望的值,程序跑一遍看一下和这个值相不相等。这种测试并没有什么用啊。有点像 ACM 啊哈哈 这是我的一个误区——单元测试是为了保证未来加入了新的代码后,原来的代码还能正常工作而存在的。 确实,加了新功能导致之前的东西炸了的事十分常见。

PHPUnit 的单元测试是使用断言(assertion)来定义的。即会判断是不是符合这一种情况。

进行单元测试,首先要创建测试用例,这是一个继承于PHPUnit_Framework_TestCase类,所有的方法都必须test开头。

class SomeClassTest extends PHPUnit_Framework_TestCase{
	private $hello = true;
	function testSomething(){
		assertTrue($this->hello);
	}
}

运行测试

只需要在命令行中输入

phpunit Filename

即可进行单元测试。 后期可以去了解一下 PHPUnit 和 Jenkins 的对接。 确实很爽!

嘛,就差不多是这样啦,整理了也是挺久的。这篇文章当做一个备忘吧。 明天还要去把书给还了。