深入浅出 PHP 对象

对象基础

类的创建

类是用于生成对象的代码模板。我们使用 class 关键字和一个任意的类名来声明类。约定类名首字母应该大写

class ShopProduct {
// 类体
}

对象的创建

对象是根据类中定义的模板所构造的数据。对象的创建其实就类的实例化,对象是由类定义的数据类型

通过 new 操作符来实例化一个类

# $product1和$product2 由同一个类生产的相同类型的不同对象
$product1 = new ShopProduct();
$product2 = new ShopProduct();

类的属性

类可以定义被称为属性的特定变量。属性也被称为成员变量,用来存放对象之间互不相同的数据。类的属性在声明和赋值前加一个代表可见性的关键字 public 或 protected 或 private(下文会讲明)

// public 关键字确保我们可以从对象之外访问属性。
class ShopProduct {
public $title = 'default product';
public $producerMainName = 'main name';
public $producerFirstName = 'first name';
public $price = 0;
}

我们通过使用->字符连接对象变量和属性名来访问属性变量

$product1 = new ShopProduct();
print $product1->title;

可见性为public的属性也可以直接赋值

$product1->title = 'change name';
print $product1->title;

方法

属性可以让对象存储数据,类方法则可以让对象执行任务。方法是在类中声明的特殊函数。方法和属性一样,接受限定词,包括可见性关键字,当然也可以省略,省略可见性关键字则方法默认为public

class ShopProduct {
public $title = 'default product';
public $producerMainName = 'main name';
public $producerFirstName = 'first name';
public $price = 0;
public function getProducer()
{
return "{$this->producerFirstName} {$this->producerMainName}";
}
}
$product1 = new ShopProduct();
print $product1->getProducer();

$this 伪变量指向一个对象实例,即指向调用方法的当前实例。

构造方法

创建对象时,构造方法(也称构造器)会被自动调用, 这使我们可以做一些初始化工作,例如初始化属性值,数据库连接等等。

class ShopProduct {
...
public function __construct($title, $firstName, $mainName, $price)
{
$this->title = $title;
$this->producerFirstName = $firstName;
$this->producerMainName = $mainName;
$this->price = $price;
}
...
}

参数和类型

PHP 是一种弱类型语言,即变量不需要声明为特定的数据类型。当然这不是说 PHP 没有类型的概念。字符串类型、整形、布尔型等这些都是基本类型。而定义一个类其实就是相当于定义了一种数据类型。

PHP 中定义方法和函数时不需要指定参数的数据类型。提供了很大的灵活性但也是一种隐患,可能会引起歧义。

我们可以通过一些类型检查函数类检查相应的数据类型

类型检查函数 类型 描述
is_bool() 布尔型 true 或 false
is_integer() 整形 整形
is_double() 双精度型 浮点数
is_string() 字符串 字符数据
is_object() 对象 对象
is_array() 数组 数组
is_resource() 资源 用于识别和处理外部资源的句柄(如数据库或文件)
is_null NULL 未分配的值

为了解决没有强制要求参数类型,PHP5中引入了类的类型提示(type hint)

public function method(ShopProduct $shopProduct)
{
// ...
}

这个方法只接受包含 ShopProduct 对象的$shopProduct 参数,当传入一个错误的对象时,将会报错。类提示只有在错误的对象被传递给方法时才会报错,即运行时报错。

类型提示不能用于强制规定参数为某种基本数据类型,例如字符串或整型,不过你可以强制规定数组作为参数

function setArray(array $store)
{
//...
}

继承

继承是从一个基类得到一个或多个派生类的机制。 继承自另一个类的类被称为该类的子类。子类将继承父类的特性,这些特性由属性和方法组成,子类可以增加父类之外的新功能,因此子类也被称为父类的“拓展”。
PHP 只支持继承自一个父类,也就是单继承,不能多继承

创建继承树的第一步是找到现有基类元素中不适合放在一起,或者不需要进行特殊处理的类方法。

要创建一个子类,必须使用 extends 关键字, 当子类没有定义构造方法时,所有在 实例化时对自动调用父类的构造方法。子类默认继承父类所有的 public 和 protected 方法,没有继承 private 方法。

当要引用父类的方法时,可以使用::而不是->, parent 关键字指向父类

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

可见性

  • 在任何地方都可以访问 public 属性和方法
  • 只能再当前类才能访问 private 方法或属性,即使在子类中也不能访问
  • 可以在当前类或子类中访问 protected 属性或方法,其他外部代码无权访问

我们可以通过 private 来限制外部访问对象的属性,起到保护作用

class People {
private $age;
public function setAge($age)
{
$this->age = $age;
}
public function getAge()
{
return $this->age;
}
}

高级特性

静态方法和属性

我们不仅可以通过对象访问方法和属性,还可以通过类来访问它们,这样的方法和属性是“静态的”(static),必须用关键字 static 来声明

class StaticExample {
static public $aNum = 0;
static public function sayHello()
{
print 'hello';
}
}

静态方法是以类作为作用域的函数,静态方法不能访问这个类中的普通属性,因为这些属性属于一个对象而不是一个类,但可以访问静态属性。如果修改了一个静态属性,那个这个类中的所有实例都能访问到这个新值。

因为是通过类而不是实例访问静态元素,所以访问静态元素时不需要引用对象的变量,而是使用::来连接类名和属性或类名和方法。

print StaticExample::$aNum;
StaticExample::sayHello();

在类内部,可以使用 self 关键字来访问静态元素

class StaticExample {
static public $aNum = 0;
static public function sayHello()
{
self::$aNum++;
print self::$aNum;
}
}

为什么使用静态元素?

  • 不需要在对象间传递类的实例
  • 不需要将实例存放在全局变量中,就可以访问类中的方法。
  • 利用静态属性来设置值,该值可以被类中的所有对象使用。

常量属性

有些属性不能改变,错误和状态标识经常需要被硬编码进类中。虽然它们是公共的,但是客户端代码不能改变它们。

  • 常量属性用 const 关键字来声明
  • 按照惯例,只能用大写字母来命名常量
  • 常量属性值包含基本数据类型的值,不能将一个对象指派给常量。
  • 像静态属性一样,只能通过类而不能通过类的实例访问常量属性。
  • 引用常量时不需要用美元符号作为前导符
  • 给已经声明过的常量赋值会引起解析错误
class ClassName {
const OUT_OF_STACK = 1;
}
print ClassName::OUT_OF_STACK;

抽象类

抽象类不能被实例化,只能定义子类需要的方法(可以是普通方法或者抽象方法),子类通过继承它并且通过实现其中的抽象方法(必须实现或者把它们也声明为抽象方法,否则会出现解析错误),使抽象类具体化。
通过 abstract 关键字定义一个抽象类或声明抽象方法(其中不能有内容)

abstract class AbstractClass {
abstract public function method();
}
class Bar extends AbstractClass {
public function method()
{
// ...
}
}

接口

抽象类提供了具体实现的标准,而接口(interface)则是纯粹的模板。接口只能定义功能而不包含实现的内容。接口可以包含属性和方法声明,但是方法体为空。
定义一个接口:

interface Chargeable {
public function getPrice();
}

** 任何实现接口的类都要实现接口中定义的所有方法,否则该类必须声明为 abstract。一个类可以实现多个接口,接口可以有效地将不相关的类型联结起来。

实现一个接口也就意味着该类接受了接口的类型,只要知道一个对象的类型,我们就能知道它能做什么,也就是说实现接口表明的是实现接口的类的功能和类型。

class ShopProduct extends ParentClass implements Chargeable {
//...
public function getPrice()
{
// ...
}
}

延迟静态绑定

首先看看下面这个例子

abstract class DomainObject {
public static function create()
{
return new self();
}
}
class User extends DomainObject {}
class Document extends DomainObject {}
Document::create();

结果是:

PHP Fatal error: Cannot instantiate abstract class DomainObject in...

为什么是这样的结果呢?
实际上,self 对该类所起的作用和$this对象所起的作用不完全相同。self 指的不是调用上下文,指的是解析上下文。 因此 self 被解析为定义 create()的 DomainObject, 而不是解析为调用 self 的 Doucment 类。

为了解决这个问题,PHP5.3中引入引入了延迟静态绑定的概念, 该特性最明显的标志就是新关键字 static,指向被调用的类, 因此上例子中把 self 改为static就可以达到想要的效果。

异常

异常是从 PHP5内置的 Exception 类(或其子类)实例化得到的特殊对象。Exception 类用于存放和报告错误信息。
Exception 类的构造方法接受两个可选的参数:消息字符串和错误代码。

抛出异常

联合使用 throw 关键字和 Exception 对象来抛出异常。这会停止执行当前方法,并负责将错误返回给调用代码。

throw new Exception("something wrong");

处理异常

把可能会抛出异常的代码快放入 try 子句中,当抛出异常时程序控制权交 由 catch 子句。
catch子句可以定义多个,调用哪一个 catch 子句取决于抛出异常的类型和参数中类的类型提示。

// FileException 继承于Exception
// XMLException 继承于Exception
try {
//...
} catch (FileException $e) {
//...
} catch (XMLException $e) {
//...
} catch (Exception $e) {
// 后背捕捉器,正常情况不应该被调用
}

Final类和方法

final 关键字可以终止类的继承, final 类不能有子类,final 方法不能被覆写

final class Checkout {
// ...
}
class CheckoutBar {
final public function method()
{
// ...
}
}

拦截器

PHP提供了内置的拦截器方法,它可以“拦截”发送到未定义方法和属性的消息, 类似于__construct()方法,遇到合适的条件时会被调用。

方法 描述
__get($property) 访问未定义的属性时将被调用
__set($property, $value) 给未定义的属性赋值时被调用
__isset($property) 对未定义的属性调用 isset() 时调用
__unset($property) 对未定义的属性调用 unset() 时调用
__call($method, $arg_array) 调用未定义的方法时被调用

call()方法可能是最有用的拦截器方法,接受两个参数,一个是方法的名称,另一个是传递给要调用方法的所有参数(数组)。 cal()方法对于实现委托也很有用, 委托是指一个对象转发或者委托一个请求给另一个对象,被委托的一方替原先对象处理请求

析构方法

与构造方法对应的还有析构方法__destruct(), 析构方法只在对象被垃圾收集器收集前自动调用,可以利用这个方法进行最后必要的清理工作。

使用__clone()复制对象

PHP 提供了 clone 关键字来获得一个对象的副本

class CopyMe {}
$first = new CopyMe();
$second = clone $first;

通过clone关键字复制的对象与原对象一模一样,包括一些对象自身独特的标识符, 但我们不希望复制对象与原对象有冲突。
为了解决这个问题, 在对象调用clone时,我们希望可以控制复制什么,clone()方法就是为了达到这样的目的。
当在一个对象上调用 clone 关键字时,产生了新副本, 并且其
clone()方法会被自动调用。 注意:__clone()方法是在复制得到的对象上运行的

class Person {
private $id;
private $name
public function setId($id)
{
$this->id = $id;
}
function __clone()
{
$this->id = 0;
}
}

clone 只进行浅复制,这意味着在复制对象属性时只复制引用,并不复制引用的对象

class Account {
public $balance = 10;
}
class Person {
public $name = 'name';
public function __construct(Account $account)
{
$this->account = $account;
}
public function __clone()
{
}
}
$account = new Account();
$person = new Person($account);
$person1 = clone $person;
$person1->account->balance += 1;
$person1->name = 'yann';
print $person->account->balance; // 11
print $person1->account->balance; // 11
print $person->name; // name
print $person1->name; // yann

如果不希望对象属性在被复制之后被共享,那么可以显示地在__clone()方法中复制指向的对象。

function __clone() {
$this->account = clone $this->account;
}

定义对象的字符串值

通过实现自己的toString()方法,可以控制输出字符串的格式。toString()方法应当返回一个字符串值。当把对象传递给print或 echo 时,会自动调用这个方法,并用方法的返回内容来代替默认的输出内容。

class Person {
function __toString() {
$desc = 'There is some description';
return $desc;
}
}
$person = new Person();
print $person;

回调、匿名函数和闭包

所谓匿名函数也就是没有函数名的函数,常常可以用在回调中或者创建一个闭包。创建一个匿名函数

// 创建一个匿名函数并把函数赋值给一个变量
$anonymityVar = function ($argument) {
return $argument;
}
// 也可以让函数返回一个函数, 其实就是实现一个闭包
// & 引用$count变量而不是值
function method($name) {
$count = 0;
return function ($product) use ($name, &$count) {
// ...
}
}

回调为什么有用?利用回调,可以在运行时将与组件的核心任务没有直接关系的功能插入到组件中。有了组件回调,你就赋予了其他人在你不知道的上下文中拓展你的代码的权利。

class Register {
private $callbacks;
function registerCallback($callback) {
if (!is_callable($callback)) {
throw new Exception("callback not callable");
}
$this->callbacks[] = $callback;
}
function doSomething($target) {
print "{$target->id}: processing\n";
foreach ($this->callback as $callback) {
call_user_func($callback, $target);
}
}
}
class Target {
public $id = 10;
}
$register = new Register();
$register->registerCallback($anonymityVar);
$register->doSomething(new Target());

对象工具

类函数与对象函数

查找类

class_exists() 函数接受表示类的字符串,检查并返回布尔值。如果类存在则返回 true,否则为 false

if (!class_exists('ClassName')) {
throw new Exception('class not exists!');
}

也可以使用get_declared_classes() 函数来获得脚本进程中定义的所有类的数组。

了解对象或类

PHP 允许类的类型提示限制方法的参数类型,但这不能用它来确定对象的类型。PHP 不允许限制从方法或函数返回的对象的类型。不过我们可以用 get_class() 来获取一个对象的类,返回字符串形式的类名

class Person{ }
$person = new Person();
echo get_class($person); // Person

有时我们想确定一个类的类型,该对象是否属于某一个类家族, 我们可以使用 instanceof 操作符,判断对象是否是给定类型的实例, 也可以判断一个类是否实现了接口 。

class Man extends Person{}
$man = new Man();
echo $man instanceof Man; // 1
echo $man instanceof Person; // 1

了解类中的方法

get_class_methods() 函数返回一个类中所有方法名的列表,传入一个类名作为参数, 该函数只显示可见性为 public 的方法

print_r(get_class_methods('Exception'));

Array
(
[0] => __construct
[1] => __wakeup
[2] => getMessage
[3] => getCode
[4] => getFile
[5] => getLine
[6] => getTrace
[7] => getPrevious
[8] => getTraceAsString
[9] => __toString
)

当然我们可以利用该函数来判断方法是否可调用

if (!in_array($method, get_class_methods($man))) {
throw new Exception('method not exists');
}

较之上面的做法,我们还有另外两种实现方式,is_callablemethod_exists

// 检查函数是否可调用
echo is_callable('functionName');
// 检查类方法是否可调用
echo is_callable(array($object, 'methodName'));
// 检查类方法中的函数是否存在
echo method_exists($object, 'methodName');

存在并不意味着可调用,method_exists()对于任何可见性都返回 true

了解类属性

类似于类方法, get_class_vars()接受类名作为参数,返回属性数组,属性名为键名,属性值为键值, 同样只显示 public 属性

class Man extends Person{
public $name = 'Yann';
private $age = 18;
}
print_r(get_class_vars('Man'));

Array
(
[name] => Yann
)

了解继承

get_parent_class() 可以查找父类,该函数接受一个对象或类名作为参数,如果父类存在,则返回字符串形式的父类名,否则返回 false

print get_parent_class('Man'); // Person

对应的有 is_subclass_of() 检测类是否是另一个类的派生类,

print is_subclass_of($man, 'Person'); // 1
print is_subclass_of('Man', 'Person'); // 1

is_subclass_of() 只会告诉你类的继承关系,而不会告诉你类是否实现了一个接口,我们可以使用 instanceof(见上文)

方法调用

我们可以使用字符串来动态调用方法

$man = new Man();
$method = 'methodName';
print $man->$method();

PHP还提供call_user_func() 方法来达到相同的目的.

function myFunction($name, $age)
{
print $name . $age;
return 1;
}
$returnVal = call_user_func('myFunction', 'Yann', 20);
print $returnVal; // 1

call_user_func_array() 比 call_user_func() 更好用, 它把目标方法所需的任何参数当做数组来接受, 这样在进行委托时,我们不需知道参数的具体数量就可以很用把参数传递。

class Person {
function myFunction($name, $age)
{
print $name . $age;
return 1;
}
}
$person = new Person();
call_user_func(array($person, 'myFunction'), 'Yann', 20);
call_user_func_array(array($person, 'myFunction'), ['Yann', 20]);

坚持原创技术分享