PHP 类与对象、访问控制

概述

PHP 5 完全重写了对象模型,从而使得自 PHP 5 开始,PHP 具备了完整的面向对象编程能力。面向对象编程(即 Object Oriented Programming,简称 OOP)是一种计算机编程架构,和基于函数构建程序(也被称作函数式编程)不同,面向对象编程的思想是在程序中包含各种独立而又相互调用的对象,每一个对象都应该能够接受数据、处理数据(通常通过对象方法实现)并将数据传达给其它对象,当我们下达指令时,不再是调用函数,而是指定对象的方法。因此,在面向对象编程中,对象是程序的基本单元,一个对象包含了数据和操作数据的函数。

面向对象编程中最核心的概念就是类(Class)和对象(Object),类是对象的抽象模板,而对象是类的具体实例,比如「Laravel 精品课」是一个课程,那么课程就是一个类,而「Laravel 精品课」是这个类的一个实例,或者更直观一些,我们把学生看作一个类,那么具体的某个学生,比如张三、李四就是学生类的实例。对象包含的数据称之为类属性(Property),操作数据的函数称之为类方法(Method)。

我们还可以从另一个角度来看类和对象,以 PHP 为例,基本数据类型包括整型、浮点型、字符串、布尔类型、数组,对于整型这个类型而言,1、2、3、4、5 这些具体的数字就是它的实例(对象),我们也可以把自定义的类看作一个数据类型,只是这个类型是基本类型之外的真实世界复杂类型的映射,比如学生、课程、汽车、飞机、学校、公司、项目等等,对于学生这个类型而言,每一个个体的学生就是它的实例(对象)。

编写自定义类

准备工作:在 php_learning 目录下创建 oop 子目录存放本章节代码。

所以要创建对象,需要先定义对应的类,我们以汽车为例,编写一个自定义的类 Car,在 php_learning/oop/class.php 中编写这个类:

-w905

可以看到,在 PHP 中,类通过关键字 class 进行声明,然后紧跟着类名 Car(通常我们通过首字母大写来定义类名),然后另起一行,通过一对花括号定义类的具体属性和方法。目前,该类是一个空实现,没有定义任何属性和方法。

但是我们已然可以通过 PHP 内置的 class_exists 方法判断该类是否存在:

if (class_exists('Car')) {
    echo "class Car exists." . PHP_EOL;
} else {
    echo "class Car not exists." . PHP_EOL;
}

类属性

接下来,我们可以为这个类定义一些属性,这些属性可以是变量,也可以是常量:

<?php

/**
 * Class Car
 */
class Car
{
    const WHEELS = 4;   // 汽车都是4个轮子
    var $seats;         // 座位
    var $doors;         // 门
    var $engine;        // 发动机
    var $brand;         // 品牌
}

这里我们通过 var 来定义变量属性,通过 const 来定义常量属性,由于汽车都是4个轮子,所以我们通过常量 WHEELS 来定义(大写、无 $ 前缀),而座位数、门、发动机、品牌都是可变的,所以通过变量进行定义。

类方法

有了属性之后,可以通过方法进行设置和获取,以 $brand 为例,在 PhpStorm 中,可以通过如下方式快速为其生成设置(Setters)和获取(Getters)方法:在 Car 类的花括号中,右键->从下拉菜单选择 Generate(或者通过对应快捷键呼出窗口):

-w387

在弹出窗口选择「Getters and Setters...」:

-w229

选择指定属性生成设置和获取方法:

-w567

点击「OK」,就可以在类中生成对应的 Setters 和 Getters 方法了:

<?php

/**
 * Class Car
 */
class Car
{
    const WHEELS = 4;   // 汽车都是4个轮子
    var $seats;         // 座位
    var $doors;         // 门
    var $engine;        // 发动机
    var $brand;         // 品牌

    /**
     * @return mixed
     */
    public function getBrand()
    {
        return $this->brand;
    }

    /**
     * @param mixed $brand
     */
    public function setBrand($brand): void
    {
        $this->brand = $brand;
    }
}

除此之外,还可以编写其他自定义方法,比如汽车的最基本功能 —— 开车,我们为此定义一个 drive 方法:

/**
 * 开车
 */
public function drive()
{
    echo "1.启动引擎..." . PHP_EOL;
    echo "2.挂D档..." . PHP_EOL;
    echo "3.放下手刹..." . PHP_EOL;
    echo "4.踩油门,出发..." . PHP_EOL;
}

与之对应的,还可以定义一个熄火方法 —— close

/**
 * 熄火
 */
public function close()
{
    echo "1.踩刹车..." . PHP_EOL;
    echo "2.挂P档..." . PHP_EOL;
    echo "3.拉起手刹..." . PHP_EOL;
    echo "4.关闭引擎..." . PHP_EOL;
}

实例化对象

有了这些基本的类属性和方法后,就可以基于这个类创建具体的对象并调用对象方法执行任务了,我们通常将基于类创建对象的过程称之为实例化,在 PHP 中,我们通过 new 关键字进行类的实例化:

$car = new Car();

然后就可以操作类属性或者调用类方法了,类常量值不可更改,只能访问,在类外面访问类常量,需要通过类名 + :: + 常量名的方式:

var_dump(Car::WHEELS);

由于常量是类级别的,无需实例化即可访问。而对于对象级别的类属性(变量类型),需要通过实例化后的对象才能访问,并且访问之前,需要先设置:

$car->seats = 5;
var_dump($car->seats);

当然,如果提供了 Setters/Getters 方法,可以通过这些方法进行设置/获取,从而屏蔽实现细节:

$car->setBrand("奔驰");
var_dump($car->getBrand());

要访问类方法,直接通过对象实例 + -> + 方法名即可:

$car->drive();
$car->close();

可以看到,在 PHP 中,对象级别的属性和方法,都是通过箭头符 -> 进行访问的。

上述所有代码的打印结果如下:

-w620

构造函数

上述对象实例化是通过 new Car() 来实现的,这段代码实际上调用了 Car 的缺省构造函数,构造函数的用途是在对象实例化过程中调用,用于对该对象进行一些初始化操作,因此,我们可以借助显示编写构造函数对 Car 对象属性进行初始化。

在 PhpStorm 中,要为某个类编写构造函数,依然可以通过模板代码来实现,在 Car 的花括号范围内,通过右键->从下拉菜单选择 Generate->在呼出窗口选择「Constructor」:

-w229

然后在呼出窗口选择要设置的属性字段(我这里全选):

-w486

点击「OK」即可生成对应的构造函数:

/**
 * Car constructor.
 * @param $seats
 * @param $doors
 * @param $engine
 * @param $brand
 */
public function __construct($seats, $doors, $engine, $brand)
{
    $this->seats = $seats;
    $this->doors = $doors;
    $this->engine = $engine;
    $this->brand = $brand;
}

也可以将其调整为如下的参数列表,处理品牌之外,其他参数均包含默认值:

public function __construct($brand, $seats = 5, $doors = 4, $engine = 1)
{
    $this->seats = $seats;
    $this->doors = $doors;
    $this->engine = $engine;
    $this->brand = $brand;
}

因为对于绝大多数汽车而言,都是 5 个座位、4 个门、1 个发动机,这里需要注意的是 $this 变量,它指向的是当前对象实例引用,可以用于在类内部调用对象级别属性和方法(类级别用 self:: 访问,后面讲静态属性和方法时会介绍),除了构造函数之外,普通类方法中也可以使用 $this

/**
 * 开车
 */
public function drive()
{
    echo "1.启动引擎..." . PHP_EOL;
    echo "2.挂D档..." . PHP_EOL;
    echo "3.放下手刹..." . PHP_EOL;
    echo "4.踩油门..." . PHP_EOL;
    printf("5.%s汽车已出发\n", $this->brand);
}

/**
 * 熄火
 */
public function close()
{
    echo "1.踩刹车..." . PHP_EOL;
    echo "2.挂P档..." . PHP_EOL;
    echo "3.拉起手刹..." . PHP_EOL;
    echo "4.关闭引擎..." . PHP_EOL;
    printf("5.%s汽车已熄火\n", $this->brand);
}

这样一来,我们就可以通过下面这段代码来初始化新的 Car 对象:

$car = new Car("奔驰");

然后再访问 $car 对象的属性和方法:

var_dump($car->getBrand());
var_dump($car->seats);
var_dump($car->doors);
var_dump($car->engine);
$car->drive();
$car->close();

打印结果如下:

-w517

访问控制

最后,我们来看看 PHP 中类属性和方法的访问控制,在 PHP 中,类属性和方法的访问控制作用域是当前类与继承类中,关于类的继承,学院君会在下篇教程中介绍。

具体来说,PHP 通过 public(公开)、protected(保护)、private(私有)关键字控制类属性和方法的可见性:

  • 对于通过 public 声明的属性和方法,在类以外和继承类中均可见;
  • 对于通过 protected 声明的属性和方法,仅在继承类(支持多个层级)中可见,在类以外不可见;
  • 对于通过 private 声明的属性和方法,仅在当前类内部可见,在继承类和类之外不可见。

所谓的「可见」与「不可见」,是一种形象的说法,实际含义是可访问可设置。我们前面定义的类方法都是通过 PhpStorm 自带模版生成的,默认都是 public 声明,对于构造函数来说,除了单例模式这种特殊场景,其他都是需要通过 public 声明,否则在类以外不可见影响对象实例化。对于操作属性的 Getters/Setters 方法通常用于从外部处理 private 类型属性,所以也需要声明为 public,其他的场景可以根据具体业务场景需求来。

我们之前通过 var 声明类属性,这是比较老的用法,是为了向后兼容 PHP 4,在 PHP 5 中,通过 var 声明的属性和方法统统被视作 public,所以我们在测试代码中可以从外部直接访问和设置,下面,我们将其都设置为 protected 类型,以便在当前类和继承类中可见,在类以外不可见,从而保护对应属性不被恶意修改:

protected $seats;         // 座位
protected $doors;         // 门
protected $engine;        // 发动机
protected $brand;         // 品牌

如果你不想某些属性在子类中被访问和修改,可以将其进一步限制为 private 类型,所有类方法也是同理,这里不再独立演示。

这个时候,在 PhpStorm 中,可以看到之前在类外部直接访问类属性的代码会报错:

-w626

提示类属性访问类型为 protected,不能从外部直接访问,直接通过 Setters/Getters 间接访问,如果要设置的话,请参照上述 brand 属性的设置。

上一篇: PHP 函数(下):匿名函数和作用域

下一篇: PHP 继承、封装与多态