愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

PHP基础总结——面向对象

一、类

PHP5重写了对象模型,提高了性能且引入了更多特性,其中新特性包括访问控制抽象类final类和方法魔术方法接口对象复制类型约束。PHP对待对象的方式是引用,即每个变量都持有的是对象的引用,而非拷贝

1、类的创建,如下基本形式。类可以拥有常量变量(称为属性)函数(称为方法)

class {
    // 常量、属性声明
    // 方法声明
}

2、类的实例化,使用new关键字,如$instance = new SomeClass();,也可以使用字符串的方式来实例化一个类(动态特性,但是当使用到命名空间的时候,需要使用完整名称),如:

$className = "A";
$p = new $className(); // 相当于 new A();

在类的内部,有一个$this指针,来代表创建出的对象
3、对类的赋值,是引用赋值,而不是拷贝。如果需要拷贝赋值,则应当使用克隆
4、可以使用ClassName::class来获得一个类的类名(包含命名空间),如Apple::class返回“NS\Apple”
5、定义构造函数可以使用function __construct(){ }。请注意,当子类中定义了构造函数后,将不会调用父类的构造函数,如果要执行父类构造函数,需要在子类的构造函数中加上parent::__construct();如果子类没有定义构造函数,则会继承父类构造函数(除非父类构造函数定义为private),而且会得到执行
6、定义析构函数使用function __destruct()
7、访问控制,使用public(公共访问,类的内外均可访问),private(私有访问,类本身可以访问),protected(保护访问,类本身及子类内可以访问)
8、范围解析操作符(::),用于访问静态成员、类常量,还可以用于覆盖类中的属性和方法。类外访问静态成员和类常量的时候,要使用类名,类内使用self::。覆盖父类的方法,默认不会再执行父类的方法,如果需要执行,则需要子类自己指定,如:

class A {
    protected function foo() {
        echo "Hello, ";
    }
}

class B extends A {
    public function foo() {
        parent::foo(); // 要再使用父类的方法,需要子类使用`parent::`指明
        echo "world!";
    }
}

$p = new B();
$p->foo();

9、使用static声明静态属性、静态方法。静态属性和静态方法属于类自身,只能通过::来访问,外部使用类名::前缀,内部使用self::
10、对象的比较:

  • 使用==的比较规则:两个对象的属性和属性值都相等,且是同一个类的实例,则返回true
  • 使用===的比较规则:两个对象的引用地址相等,引用向同一块内存单元

11、可以使用serialize序列化来保存一个对象,但是保存的仅仅是类名属性值,而在反序列化的时候,需要使用unserialize,然后同时引入类名对应的类,便可继续使用,如:

// save.php
require_once "SomeClass.php";
$p = new SomeClass();
$p->setSomeVar('5');
file_put_contents("fileSave", serialize($p));

// use.php
require_once "SomeClass.php";
$p = file_get_contents("fileSave");
$p = unserialize($p);

echo $p->getSomeVar(); // 5

二、属性

1)属性以publicprivateprotected开头,接着一个普通变量而成,如public $name;,为了向后兼容,也支持var方式,当时一般不建议再使用var。属性可以有默认值,但必须是 常量值(也可以是nowdoc)
2)访问属性:类的成员方法内部使用$this->property形式访问非静态属性,使用self::$property形式访问静态属性。类外使用$obj->property访问public的非静态属性

三、继承

1、PHP不支持多重继承,一个类只能继承一个基类,继承使用extends关键字
2、PHP支持重写,使用和父类一样的属性名或者方法名,即可重写属性或者方法。但如果定义方法的时候,使用了final,则该方法不能被重写。可以使用parent::来访问被覆盖的方法或者属性

四、常量

1、类和接口中可以声明常量值(也可以是nowdoc),表示固定不变的数值,用const关键字声明,无需$符,如:

class MyClass {
    const CONST_VAR = 'xxx';
}

2、从PHP5.3.0开始,可以用一个变量来动态调用类,如:

$className = "MyClass";
$className::CONST_VAR;

五、自动加载

可以使用spl_autoload_register()来注册一个自动加载函数,当使用尚未定义的类或者接口的时候,将得到一次处理,加载得到所需类的机会。如:

spl_autoload_register(function($className) {
    require_once $className . ".php";
});

$obj = new A;

六、抽象类

抽象类只能被继承,不能被实例化
1、抽象类采用abstract声明,如abstract class Demo { }
2、当子类继承一个抽象类的时候,必须实现抽象类中的所有抽象方法(抽象类中也可以有普通方法,普通方法可以不实现),而且方法的访问控制必须和父类一致或者更宽松(如protected的父类方法,子类中可以为protected或者public,不能为更严格的private)
3、抽象类中可以没有抽象方法
4、抽象方法不能有方法体,只能是类似于以下形式:

abstract class A {
    abstract protected function foo();
}

七、接口

1、接口中的所有方法必须是共有的(接口的特性),定义接口用interface关键字,如:

interface iTemplate {
    public function setVar($name, $var);
    public function getHtml($template);
}

2、实现接口使用implements,如class A implements B { },但是需要注意:

  • 可以实现多个接口,用,分隔,实现多个接口的时候,接口中的方法不能有重名

3、接口可以继承,使用extends,如:

interface A {
}

interface B {
}

interface C extends A, B {
}

继承后,加入现在有个类X实现接口C,则X应该实现接口A、B、C中的所有方法
4、接口中可以定义常量,如:

interface A {
    const FOO = "Hello";
}

class Demo implements A {
    function foo() {
        echo A::FOO;
    }
}

$p = new Demo();
$p->foo();

八、trait

trait是从PHP5.4起引入的新特性,是一种代码复用的方法,允许无需通过继承来复用代码。trait本身不能实例化,它只能是作为组件来组合功能,是一种水平上的组合

1、声明一个traittrait关键字,如:

trait Greet {
    public function SayHello() {
        echo "Hello";
    }
}

2、在类中使用一个trait,用use,可以同时使用多个trait,如:

class Demo {
    use SomeTrait1, SomeTrait2;
    /*
    也可以是:
    use SomeTrait1;
    use SomeTrait2;
    */
}

3、同名方法优先级顺序:父类方法 < Trait方法 < 类自身方法
4、Trait冲突方法的解决:使用insteadof指明使用哪个,使用as指定别名,如:

trait SayHello {
    public function hello() {
        echo "Hello ";
    }
}

trait SayHello2 {
    public function hello() {
        echo "Hello 2 ";
    }
}

trait SayHello3 {
    public function hello() {
        echo "Hello 3 ";
    }
}

class A {
    use SayHello, SayHello2, SayHello3 {
        SayHello::hello insteadof SayHello2, SayHello3;
        SayHello2::hello as speak;
    }
}

$p = new A();
$p->hello();     // 输出“Hello”
$p->speak();   // 输出“Hello 2”

5、as关键字还可以用来修改访问控制(但是这种情况下,原版的名字不能变),如:

trait SayHello {
    public function hello() {
        echo "Hello";
    }
}

class Demo {
    use SayHello {
        hello as protected; // 这时候hello方法变成了protected
    }
}

// 但如果是以下的情况:
class Demo {
    use SayHello {
        hello as protected hey;
        // 那么hello方法还是public,只是新增了protected的hey方法
    }
}

6、一个trait也可以使用其他的trait,如:

trait C {
    use A, B;
}

7、trait中可以包括抽象方法,来强制要求类定义该方法,如:

trait A {
    abstract public function foo();
}

8、trait中也可以包含静态的方法,也可以定义属性(但是定义了属性后,类中不能定义同名属性,除非类中定义的同名属性和trait中是同样的访问权限和同样的默认值)

九、匿名类

1、PHP7开始支持匿名类,匿名类具有和普通类几乎一致的功能,如:

callDemo(new class {
    public function foo() {
        echo "Hello";
    }
});

2、可以给构造函数传值,也可以继承、实现接口、使用trait,如:

callDemo(new class($params) extends Super implements SomeInterface {
    public function __construct($params) {

    }

    use SomeTrait;
});

3、可以让匿名的嵌套类访问嵌套类外的属性和方法(使用extends),但是访问private属性需要将属性传入构造器,如:

class Outer {
    private $prop = 1;
    protected $prop2 = 2;

    protected function func1() {
        return 3;
    }

    public function func2() {
        return new class($this->prop) extends Outer {
            private $prop3;

            public function __construct($prop) {
                $this->prop3 = $prop;
            }

            public function func3() {
                return $this->func1();
            }
        }
    }
}

4、声明的同一个匿名类,所创建的对象都是这个类的实例。匿名类的名称是引擎赋予的:

function anonymous_class() {
    return new class {};
}

get_class(annoymous_class()) === get_class(annoymous_class()); // true

十、重载

PHP提供的重载,是指动态地“创建”类属性和方法,通过魔术方法来实现

1、当调用未定义不可见的属性、方法时,重载方法会被调用,所有的重载方法应该被声明为public
2、魔术方法的参数不能通过引用传递,即function __set(&$var)是不允许的
3、属性重载:

  • public function __set($name, $value),给不可访问属性赋值时调用
    注意:__set()的返回值将被忽略,所以不能实现如$a = $obj->b = 8形式的链式调用
  • public function __get($name),访问不可访问属性时调用
  • public function __isset($name),对不可访问属性调用isset()或者empty()时调用
  • public function __unset($name),对不可访问属性调用unset()时调用
  • public function __set($name, $value),给不可访问属性赋值时调用

4、静态方法中,魔术方法不会被调用,所以这些方法都不能声明为static
5、方法重载:

  • public __call($name, $arguments) 调用不可访问方法时候,__call()会被调用
  • public __callStatic($name, $arguments) 静态上下文中调用不可访问方法的时候被调用,如Class::runSomeMethod()时,runSomeMethod不存在,则__callStatic()被调用

十一、遍历对象

1、可以使用foreach来遍历每个可见对象属性,如:

class Demo {
    private $a = 1;
    public $b = 2;
    public $c = 3;

    public function foo(){}
}

$p = new Demo();
foreach($p as $key => $val) {
    echo $key, "=>", $val, PHP_EOL;
}
/*
执行结果:
b=>2
c=>3
*/

class Demo {
    private $a = 1;
    public $b = 2;
    public $c = 3;

    public function foo(){
        foreach($this as $key => $val) {
            echo $key, "=>", $val, PHP_EOL;
        }
    }
}

$p = new Demo();
$p->foo();
/*
执行结果:
a=>1
b=>2
c=>3
*/

2、可以实现Iterator接口,来决定如何遍历以及每次遍历的时候哪些值可用,其实就是迭代器模式,Iterator接口如下:

interface Iterator {
    public function rewind();
    public function current();
    public function key();
    public function next();
    public function valid();
}

实例如:

class MyIterator implements Iterator {
    private $var = array();
    public function __construct($array) {
        if(is_array($array)) {
            $this->var = $array;
        }
    }

    public function rewind() {
        echo "Rewiding\n";
        reset($this->var);
    }

    public function current() {
        $var = current($this->var);
        echo "Current: {$var}\n";
        return $var;
    }

    public function key() {
        $var = key($this->var);
        echo "Key: {$var}\n";
        return $var;
    }

    public function next() {
        $var = next($this->var);
        echo "Next: {$var}\n";
        return $var;
    }

    public function valid() {
        $var = $this->current() !== false;
        echo "Valid: {$var}\n";

        return $var;
    }
}

$it = new MyIterator([1, 2, 3]);
foreach($it as $key => $val) {
    echo "[$key: $val]\n";
}

将输出:

Rewiding
Current: 1
Valid: 1
Current: 1
Key: 0
[0: 1]
Next: 2
Current: 2
Valid: 1
Current: 2
Key: 1
[1: 2]
Next: 3
Current: 3
Valid: 1
Current: 3
Key: 2
[2: 3]
Next: 
Current: 
Valid: 

可见,当一个对象实现了Iterator接口的时候,使用foreach就会按照实现的Iterator方法来遍历对象。foreach首先调用rewind()方法,将指针指向数组头部,然后判断是否达到数组末尾(调用valid()判断),然后调用current()获得值,调用key()获得键,然后使用next()迭代到下一个元素

十二、魔术方法

PHP中所有以__开头的方法名保留作魔术方法,除了之前介绍的魔术方法外,还有以下的魔术方法:

1、__sleep()和__wakeup()

__sleep() 用于确定serialize() 需要序列化哪些属性,如:

class Demo {
    public $a = 1;
    public $b = 2;

    function __sleep() {
        return array('a');
    }
}
$a = new Demo();
echo serialize($a); // 返回 O:4:"Demo":1:{s:1:"a";i:1;}

__wakeup() 则会在 unserialize() 调用前执行

2、__toString()

用于指明在将对象当做字符串的时候,应该这么处理,如:

class Demo {
    public $a = 1;
    public $b = 2;

    function __toString() {
        return "Hello !!";
    }
}

$a = new Demo();
echo $a; // 输出"Hello !!"

注意: 不能在__toString()中抛出异常,否则会引发致命错误

3、__invoke()

当尝试以函数的形式来使用对象的时候,该方法就会被调用,如:

class Demo {
    public $a = 1;
    public $b = 2;

    function __invoke($a, $b) {
        return $a+$b;
    }
}

$a = new Demo();
echo $a(3, 7); // 输出:10
4、__set_state()

调用var_export()的时候,会被执行,即:

class Demo {
    public $a = 1;
    public $b = 2;

    public static function __set_state($arr) {
        $obj = new Demo();
        $obj->a = $arr['a'];
        $obj->b = $arr['b'];
        return $obj;
    }
}

$a = new Demo();
$a->a = 10;
$a->b = 20;
var_export($a);
/*
输出:
Demo::__set_state(array(
   'a' => 10,
   'b' => 20,
))
*/
5、__debugInfo()

调用var_dump()的时候,会被调用。当没有定义这个方法的时候,默认情况下public、protected、private属性都将被展示出来

十三、final关键字

1、如果父类方法被声明为final,则子类无法重写该父类方法
2、如果一个类被声明为final,则该类无法被继承
3、属性不能被定义为final,只有类和方法才可以

十四、对象复制

通常情况下,直接使用=来复制对象,并没有实现对象的复制,而是将被复制对象的引用复制了一份而已。如下所示:

class A {
    public $arr = array(1, 2);
}

$p = new A();
$q = $p;

array_push($q->arr, 3); // 对$q->arr添加元素3

print_r($p->arr); // 为[1, 2, 3]
print_r($q->arr); // 为[1, 2, 3]

1、对象复制,使用clone关键字来完成(如果类中定义了__clone()魔术方法,将执行__clone()魔术方法):

class A {
    public $arr = array(1, 2);
}

$p = new A();
$q = clone $p;

array_push($q->arr, 3); // 对$q->arr添加元素3

print_r($p->arr); // 为[1, 2]
print_r($q->arr); // 为[1, 2, 3]

2、但是clone关键字实现的只是浅复制(只实现了一层的复制),对于类内部的对象,只是复制了引用地址,如下所示:

class A {
    public $a = 1;
}

class B {
    public $instance;
    function __construct() {
        $this->instance = new A();
    }
}

$p = new B();
$q = clone $p;

var_dump( $p->instance === $q->instance ); // bool(true)

如果要解决这个问题,则应当在类里使用__clone()魔术方法,来对类内部的对象也进行克隆,如:

class A {
    public $a = 1;
}

class B {
    public $instance;
    function __construct() {
        $this->instance = new A();
    }

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

$p = new B();
$q = clone $p;

var_dump( $p->instance === $q->instance ); // bool(false)

十五、类型约束

PHP5.1其引入了类型约束,用于限制参数的类型,类型约束可以用在方法里,也可以用在函数里
1、类型必须是对象(类的名字)、接口、数组或者callable(PHP5.4+引入)。如果传入的参数不符合类型约束的类型,那么就会报错
2、如果参数有个默认值NULL,那么传入NULL的时候不会报错(也可以不传参)

十六、后期静态绑定

后期绑定是PHP5.3引入的新特性,可以使得调用静态方法的时候,绑定的类取决于调用的类,由于在继承范围内引用静态调用的类
1、self::的限制:self只能获得定义它本身的类

class A {
    public static function who() {
        echo __CLASS__;
    }

    public static function test() {
        self::who();
    }
}

class B extends A {
    public static function who() {
        echo __CLASS__;
    }
}

B::test(); // 输出A

2、上面的例子,B类继承A类方法,得到的是A类的引用。现在,我们希望得到继承的方法,但是调用的方法中用到了自身引用的时候是子类自身,那么后期静态绑定就可以发挥作用了。使用static::就能够使得类的引用是运行时决定,它取决于调用它的类!!所以有:

class A {
    public static function who() {
        echo __CLASS__;
    }

    public static function test() {
        static::who();
    }
}

class B extends A {
    public static function who() {
        echo __CLASS__;
    }
}

B::test(); // 输出B

3、在非静态上下文中,使用static::不会得到预期的效果。因为static::仅仅只能用于静态对象。

class A {
    private function foo() {
        echo __CLASS__;
    }

    public function test() {
        $this->foo();
        static::foo();
    }
}

class B extends A {

}

class C extends A {
    private function foo() {

    }
}

// 此时调用者是$p1,而$p1所属的类是B,但是结果会输出“AA”
$p1 = new B();
$p1->test();

// 此时C重写了A的方法,类外不可调用私有方法,所以会报错
$p2 = new C();
$p2->test();