ООП в PHP: Наследование

inheritanceИз первой статьи в этой рубрике можно было узнать, что при помощи механизма наследования один или несколько классов-наследников могут приобрести свойства и методы своего класса-предка. Такой подход позволит не дублировать код, что плохо по некоторым причинам. Во-первых, если код дублируется, то писать его приходится больше и дольше. Во-вторых, в случае, если в код нужно будет внести некоторые изменения, то делать это придётся в нескольких местах. В-третьих, если дублируется код, то дублируется и ошибка, если она была допущена, и исправлять её придётся в нескольких местах. Кроме того при помощи наследования реализуется полиморфизм – механизм, который позволит сделать ваш код более расширяемым.

Проблемы, которые поможет решить наследование

Перед тем, как говорить о синтаксисе, при помощи которого реализуется наследование, я приведу пример, где происходит дублирование кода. Этот пример поможет понять, как наследование помогает избавляться от дублированного кода.

Пусть при разработке сайта необходимо предусмотреть возможность наличие администраторов и обычных пользователей. Администратор отличается от обычного пользователя тем, что у него есть некоторый уровень прав доступа. Самым простым вариантам будет написать два класса: один для описания администраторов, другой для описания пользователей. Схематически они будут выглядеть примерно вот так:

class Admin {
  private $login;
  private $password;
  private $rights;
 
  public function __construct($login, $password, $rights) {
    $this->login = $login;
    $this->password = $password;
    $this->rights = $rights;
  }
 
  public function getLogin() {
    return $this->login;
  }
 
  public function getRights() {
    return $this->rights;
  }
}
class User {
  private $login;
  private $password;
 
  public function __construct($login, $password) {
    $this->login = $login;
    $this->password = $password;
  }
 
  public function getLogin() {
    return $this->login;
  }
}

Обратите внимание на то, что поля $login, $password и метод getLogin содержится в обоих классах, что не очень хорошо. Что бы избавиться от этого дублирования можно объединить эти два класса в один:

class User {
  private $login;
  private $password;
  private $rights;
 
  public function __construct($login, $password, $rights = 0) {
    $this->login = $login;
    $this->password = $password;
    $this->rights = $rights;
  }
 
  public function getLogin() {
    return $this->login;
  }
 
  public function getRights() {
    return $this->rights;
  }
 
  public function setRights($rights) {
    $this->rights = $rights;
  }
}

На первый взгляд это отличное решение – кода стало меньше и дублированных свойств и методов уже нет, но объекты, которые будут представлять обычных пользователей, будут содержать одно лишнее свойство $rights и лишние методы getRights и setRights. Вроде бы, в этом ничего страшного и нет, к тому же, возможно, вы скажете, что неплохо было бы предусмотреть возможность превращения обычного пользователя в админа, но наличие этих методов может ввести в заблуждение. Возвращённый методом getRights ноль может быть расценен двояко: как отсутствие админских прав вовсе или как нулевой уровень прав доступа админа. Так что и этот вариант далеко не идеален.

В этой ситуации необходимо сделать класс Admin наследником класса User. В этом случае методы и поля, которые будут описаны в классе User, передадутся классу Admin.

Синтаксис наследования

Унаследовать один класс от другого можно при помощи ключевого слова extends, которое можно перевести как «расширяет», что логично ведь класс-потомок, приобретая поля и методы своего класса-родителя, в то же время может содержать и свои собственные методы и поля, тем самым расширяя возможности своего класса-родителя.

Итак, класс-предок User схематично будет выглядеть вот так:

class User {
  private $login;
  private $password;
 
  public function __construct($login, $password) {
    $this->login = $login;
    $this->password = $password;	
  }
 
  public function getLogin() {
    return $this->login;
  }
}

Класс-наследник Admin в этом случае будет вот таким:

class Admin extends User {
  private $rights;
 
  public function __construct($login, $password, $rights) {
    $this->login = $login;
    $this->password = $password;	
    $this->rights = $rights;
  }
 
  public function getRights() {
    return $this->rights;
  }
 
  public function setRights($rights) {
    $this->rights = $rights;
  }
}

Не забываем о том что, так как классы нужно хранить в отдельных файлах, необходимо не забыть подключить файл с классом-предком в файле с классом-наследником. Обычно это делается при помощи функции require_once что бы избежать повторного подключения одного и того же файла.

Теперь у экземпляров класса Admin можно вызывать методы, которые были реализованы в классе User:

$admin = new Admin('admin', 'veryLongAndDifficultPassword', 1);
echo $admin->getLogin(); // admin

Цепочка наследования может быть сколь угодно длинной и один класс может иметь сколь угодно много потомков, но предок у класса может быть только один, то есть в php не поддерживается множественное наследование, хотя его можно эмулировать, но об этом в следующих статьях.

Оператор instanceof

Этот оператор возвращает булево значение, показывающие относится ли объект к заданному классу или нет. Синтаксис этого оператора:

$object instanceof ClassName;

При этом этот оператор возвращает true даже если слева стоит экземпляр класса-наследника от класса, имя которого указанно справа.

$admin = new Admin('admin', 'veryLongAndDifficultPassword', 1);
var_dump($admin instanceof Admin); // bool(true)
 
$user = new User('true-coder', 'qwerty');
var_dump($admin instanceof User); // bool(true)
 
var_dump($user instanceof User); // bool(true)
 
var_dump($user instanceof Admin); // bool(false)

Оператор instanceof удобно применять, когда необходимо убедится в наличии у экземпляра необходимых данных или функционала, которые присущи экземпляром какого-нибудь класса. Чаще всего использование этого оператора можно избежать, если использовать уточнение типов в параметрах методов.

Уточнения типов объектов

Слаботипизированность php имеет множество плюсов и минусов и о них можно спорить вечно, но если умело использовать некоторые возможности этого языка можно забыть об отрицательных сторонах этого момента.
В php аргумент метода или функции может содержать всё что угодно, будь то строка, число или экземпляр класса и, хотя такая гибкость иногда полезна, она же может сыграть с вами злую шутку.

Пусть у нас есть класс, который отвечает за вывод информации о пользователях или админах на страницу c методом, который выводит логин.

class Writer {
  public function writeLogin($user) {
    echo "<strong>Login</strong>: {$user ->getLogin()}<br />";
  }
}

Всё будет прекрасно, когда в метод writeLogin будет передаваться экземпляр класса Admin или User, но если по ошибке передать в этот метод экземпляр другого класса или значение элементарного типа, то возникнет ошибка. Казалось бы, что ничего страшного в этом нет, но необходимый функционал экземпляра класса переданного методу в качестве параметра может использоваться где-то глубоко в условном операторе, и использоваться он только в день летнего солнцестояния. В таком случае отследить ошибку очень сложно и поэтому в таких случаях желательно указывать тип параметра метода.

class Writer {
  public function writeLogin(User $user) {
    echo "<strong>Login</strong>: {$user ->getLogin()}<br />";
  }
}

В этом случае, если методу передать что-то неподходящие, то произойдёт ошибка:

$writer = new Writer();
/*
 * Catchable fatal error: Argument 1 passed to Writer::writeLogin()
 * must be an instance of User, string given,…
 */
$writer->writeLogin("badValue");

Возникновение этой ошибки заставит проконтролировать тип передаваемого в метод значения, что возможно избавит в будущем от непредсказуемых ошибок. Стоит так же заметить, что передача экземпляра класса-наследника от класса, имя которого указано для уточнения типа, не вызовет ошибки. Это логично, особенно если учесть, что класс-наследник обладает всем тем функционалом, которым обладал его предок. Например, вот такой код ошибки не вызовет.

$writer = new Writer();
$writer->writeLogin(new Admin('true-coder', 'veryDifficultPassword', 1));

Переопределение методов

Если в классе-потомке определить метод с именем, которое уже использовалось при объявлении другого метода в классе-предке, то такой метод будет называться переопределённым. Такой подход вполне себя оправдывает ведь классы, стоящие в одной цепочки наследования, описывают схожие множества объектов со схожим поведением, и для обозначения похожего действий таких объектов вполне естественно использовать одно и то же имя метода.

Вернёмся к классам Admin и User. И администратор, и пользователь могут авторизироваться, и для этого совершения этого действия можно использовать одно имя метода, например, authorize. Авторизация пользователя и администратора будет проходить немного по-разному: если для пользователя достаточно записать в сессию только логин, то для администратора необходимо записать в сессию ещё и уровень прав доступа.

Реализуем соответсвующие варианты метода authorize в классах User и Admin предполагая, что сессия уже инициализирована.

class Admin {
  private $rights;
 
  public function __construct($login, $password, $rights) {
    $this->login = $login;
    $this->password = $password;
    $this->rights = $rights;
  }
 
  public function authorize() {
    $_SESSION['login'] = $this->getLogin();
    $_SESSION['rights'] = $this->getRights();
  }
 
  public function getLogin() {
    return $this->login;
  }
 
  public function getRights() {
    return $this->rights;
  }
 
  public function setRights($rights) {
    $this->rights = $rights;
  }
}
 
class User {
  private $login;
  private $password;
  private $rights;
 
  public function __construct($login, $password, $rights = 0) {
    $this->login = $login;
    $this->password = $password;
    $this->rights = $rights;
  }
 
  public function getLogin() {
    return $this->login;
  }
 
  public function getRights() {
    return $this->rights;
  }
 
  public function setRights($rights) {
    $this->rights = $rights;
  }
 
  public function authorize() {
    $_SESSION['login'] = $this->getLogin();
  }
}

Теперь при написании кода, который будет авторизовать пользователей или администраторов не нужно будет знать, от экземпляра какого класса вызывается метод authorize.

Обращение к полям и методам класса-предка

Иногда бывает необходимо получить доступ к полю или методу класса-предка из класса-наследника. Использование методов, реализованных в классе-предке, иногда позволяет избавиться от дублирования кода. Например, обратите внимание на конструкторы классов Admin и User и заметьте, что в них дублируется код, присваивающий полям password и login переданные в параметрах конструктора значения. Чтобы избежать подобного дублирования можно вызвать конструктор класса-предка из конструктора класса-потомка (в php в отличии от java или C# это не происходит автоматически)

Чтобы обратится к полям или методам класса-предка используется ключевое слово parent. Конструктор класса Admin в итоге можно переписать вот так:

public function __construct($login, $password, $rights) {
  parent::__construct($login, $password);
  $this->rights = $rights;
}

Обращение к полям класса-предка происходит аналогичным образом:

class A {
  public $property = 'value';  
}
 
class B extends A{
  public function getProperty() {
    return $this->property;
  }
}
 
$b = new B();
 
echo "{$b->getProperty()}
"; // value

Обратите внимание, что к статическим полям класса-предка используя ключевое слово parent обращаться нельзя:

class A {
  public static $staticProperty = 'value';  
}
 
class B extends A{
  public function getProperty() {
    return parent::$property;
  }
}
 
$b = new B();
//Fatal error: Access to undeclared static property: A::$property...
echo "{$b->getProperty()}<br />";

Наследование и область видимости полей и методов

Ранее в этой рубрике я уже упоминал о возможности регулировать область видимости полей и методов класса с помощью спецификаторов доступа. Но рассмотрение спецификатора доступа protected возможно только в контексте темы наследования. Методы или поля, объявленные с этим спецификаторами доступны из классов-наследников.

Рассмотрим следующий пример. Пусть есть класс Rectangle, описывающий прямоугольник и его наследник RectangleDrawer, основной обязанностью которого является отрисовка прямоугольника. В методе draw этого класса, который и будет отрисовывать прямоугольник, происходит обращение к полям класса-предка.

class Rectangle {
  protected $width;
  protected $height;
  protected $x;
  protected $y;
 
  public function __construct($x, $y, $width, $height) {
    $this->x = $x;
    $this->y = $y;
    $this->width = $width;
    $this->height = $height;
  }
}
 
class RectangleDrawer {
  public function __construct() {
    parent:__construct();
  }
 
  public function draw($image, $thickness, $color) {
    $left = parent::$x;
    $top = parent::$y;
    $right = parent::$x + parent::$width;
    $bottom = parent::$y + parent::$height;
 
    imageSetThickness($image, $thickness);
    imageRectangle($image, $left, $top, $right, $bottom, $color);
 
    return $image;
  }
}

Обратите внимание, что попытка обратиться к полям класса Rectangle вне этой цепочки наследование вызовет ошибку:

$rect = new Rectangle(1, 2, 3, 4);
// Fatal error: Cannot access protected property Rectangle::$width
$rect->width;

Наследование и полиморфизм

Не смотря на слаботипизированность php о полиморфизме в нём всё-таки можно говорить в том смысле, что экземпляр подкласса может использоваться в любом месте, где используется его предок. Это естественно, поскольку класс-потомок в полной мере приобретает функционал своего предка. Подобная возможность позволяет повторно использовать код, оперирующий экземплярами классов-предков, для классов-потомков. Ну, например, для авторизации и админа, и простого пользователя нужна одна строчка:

$user->authorize();

при этом не важно содержится в переменной user экземпляр класса User или экземпляр его потомка Admin.
Приведу более яркий, хотя и более далёкий от практики пример. Пусть у нас есть целый зоопарк животных: слон, крокодил и обезьяна. Каждого из них описывает соответствующий класс: Elephant, Crocodile, Monkey. У этих классов общий предок – класс Animal (животное) в котором реализован единственный метод – eat (есть). Схематично это можно описать вот так:

class Animal {
  public function eat() {
    //...
  }
}
 
class Monkey extends Animal {
}
 
class Crocodile extends Animal {
}
 
class Elephant extends Animal {
}

Теперь можно покормить хоть целый массив животных, только нужно убедиться, что мы кормим именно животное.

$animals = array (
  new Monkey(),
  new Monkey(),
  new Monkey(),
  new Elephant(),
  new Crocodile(),
  new Elephant(),
);
 
foreach ($animals as $animal) {
  // Убеждаемся что это животное
  if ($animal instanceof Animal) {
    $animal->eat();
  }
}

Теперь, если нам вздумается добавить ещё одно животное, то мы просто унаследуем описывающий его класс от класса Animal, тем самым показав что оно может кушать, а для кормления нашего зоопарка легко можно будет использовать тот-же foreach, что и в предыдущем примере.

На этом всё! Успехов вам!

  • Подобная возможность позволяет повторно использовать код, оперирующий экземплярами классов-потомков, для классов предков.

    Поправьте, наоборот. Если метод оперирует потомком, по отношению к предку его вызывать нельзя. У предка может не быть нужных полей/методов банально

  • Александр, да вы правы. Ошибку исправил. Спасибо за замечание. Прошу прощения у тех, кого я ввёл в заблуждение.

  • Хорошии статьи у вас. Можно по больше про PHP если можно . Просто я его сейчас изучаю.

  • Дмитрий

    Хорошая статья, все понятно и доступно. Очень порадовал пример с зоопарком, побольше таких!))

  • Роман

    Отличные статьи, сожалею, что так поздно их нашел 🙂

  • Евгений

    Скажите, пожалуйста, в чем же всё-таки принципиальная разница между наследованием и полиморфизмом? Или полиморфизм является последствием наследования?

  • 123

    Автор вообще проверял свой код? как ты обращаешься к логину если у тебя его нет. (echo $admin->getLogin();) как ты обращаешься к переменным если они приватные были… ( $this->login = $login;
    $this->password = $password;)
    echo $admin->getLogin(); // admin — нихера тут у тебя не выведет.
    благодарные комменты к своей статье, походу, сам писал..