Объектное программирование
Объектно-ориентированное программирование (ООП) на PHP
Объект - это набор специальных переменных - свойств и специальных функций - методов. То, что в процедурном программировании называлось переменной — в ООП называется свойство. То, что в процедурном программировании называлось функцией — в ООП называется методом класса. Созданные на основе класса объекты называются экземплярами класса или просто объекты.
Обращение из метода к свойствам только через служебное слово $this: $this->name; (обратите внимание на
отсутствие знака доллара перед name)
Обращение внутри метода к другому методу тоже через $this: $this->foo();
Для доступа к свойствам и методам объекта служит оператор "->":
$this->name;
(обратите внимание на отсутствие знака доллара перед name)
Обращение внутри метода к другому методу тоже через $this: $this->foo();
.
Объект создается с помощью оператора new на основании
шаблона, называемого классом.
Класс определяется ключевым словом class.
Пример 1
<html>
<head>
<title>Класс со свойством и методом</title>
</head>
<body>
<?php
class классN1
{
public $имя = "Маша"; // - это свойство класса доступное снаружи класса
private $Private_name; // - это свойство доступно только методам класса
protected $Protected_name; // это свойство доступно методам собственного класса, а также методам наследуемых классов
function Привет() // - это метод класса
{
echo "<H1>".$this->имя."! Привет!</H1>";
}
function Пока( $a )
{
$this->имя = $a;
echo "<H1>".$this->имя."! Пока!</H1>";
}
}
$obj = new классN1();
$obj->Привет();
$obj->имя = "Миша";
$obj->Привет();
$obj->Пока("Яша");
$obj->Привет();
?>
</body>
</html>
Модификаторы доступа в ООП:
- public — позволяет иметь доступ к свойствам и методам из любого места (глобальная область)
- protected — доступ к родительскому и наследуемому классу (область класса наследника)
- private — доступ только из класса, в котором объявлен сам элемент (область самого класса)
Метод по умолчанию — public. У свойств значения модификатора по умолчанию нет.
Константы класса в ООП
const NAME = 2;
Таким образом можно создавать константы и вне класса. Это именно константы класса, они не принадлежат ни одному объекту, они общие на все объекты, поэтому использование внутри метода:
function printname(){
echo self::NAME;
}
self — это сам класс!
Обращение вне класса (можно вызывать из глобальной области видимости без инициализации экземпляра класса):
echo OurClass::NAME;
this и self
Внутри класса использована специальная переменная this. Это указатель, с помощью которого объект может ссылаться на самого себя.
Для обращения к статическим методам используется self::
Методу Пока передан аргумент точно так же, как и обычной функции. При вызове этого метода объект меняет свое свойство имя.
Конструктор — это метод, который автоматически вызывается при создании нового объекта:
public function __construct(){}
.
При инициализации6 объекта через служебную конструкцию new, PHP ищет __construct
и если он есть, то
вызывается.
Также можно создать метод, имя которого совпадает с именем класса, - такой метод также будет считаться конструктором. Конструктор может принимать аргументы, что значительно упрощает работу с классами.
Пример 2
<html>
<head>
<title>Класс с конструктором</title>
</head>
<body>
<?
class классN2
{
private $имя; // - это свойство класса НЕ доступное снаружи класса
function __construct( $a="Кто-то там" )
{
$this->имя = $a;
}
function Привет()
{
echo "<H1>".$this->имя."! Привет!</H1>";
}
}
$obj0 = new классN2();
$obj1 = new классN2("Миша");
$obj2 = new классN2("Маша");
$obj0->Привет();
$obj1->Привет();
$obj2->Привет();
?>
</body>
</html>
Сложив все, изложенное выше, можно создать более осмысленный класс. Например, класс, который будет располагать данные в виде таблицы с поименнованными столбцами.
Пример 3
<html>
<head>
<title>Класс Table</title>
</head>
<body>
<?php
class Table
{
private $headers = [];
private $data = [];
function Table ( $headers )
{
$this->headers = $headers;
}
function addRow ( $row )
{
$tmp = [];
foreach ( $this->headers as $header )
{
if ( ! isset( $row[$header] )) $row[$header] = "";
$tmp[] = $row[$header];
}
array_push ( $this->data, $tmp );
}
function output ()
{
echo "<PRE><B>";
foreach ( $this->headers as $header ) echo "$header ";
echo "</B><BR>";
foreach ( $this->data as $y )
{
foreach ( $y as $x ) echo "$x ";
echo "<BR>";
}
echo "</PRE>";
}
}
$test = new Table (array("a","b","c"));
$test->addRow(array("a"=>1,"b"=>3,"c"=>2));
$test->addRow(array("b"=>1,"a"=>3));
$test->addRow(array("c"=>1,"b"=>3,"a"=>4));
$test->output();
?>
</body>
</html>
Свойства класса Table - массив имен столбцов таблицы и двумерный массив строк данных. Конструктор класса Table получает массив имен столбцов таблицы. Метод addRow добавляет в таблицу новую строку данных. Метод output выводит таблицу на экран.
Скрытые свойства и методы
Свойства и методы класса могут быть как открытыми (public), так и скрытыми (private). Скрытые свойства и методы недоступны извне класса, т.е. из сценария, в котором используется данный класс, или из другого класса.
Наследование
На основе существующих классов можно создавать новые, используя механизм наследования. Механизм наследования - это использование определенного ранее класса в качестве родительского. При этом набор свойств и методов родительского класса можно расширять. Имейте в виду, что производный класс имеет только одного родителя.
Чтобы создать новый класс, наследующий поведение существующего класса, надо использовать ключевое слово extends в его объявлении. Например:
class классN2 extends классN1 { ....... }
Здесь классN1 - родительский класс, классN2 - производный.
Если производный класс не содержит собственного конструктора, то при создании его объекта используется конструктор родительского класса. Если в производном класса существует собственный конструктор, то конструктор родительского класса не вызывается. При необходимости вызвать конструктор родительского класса это надо сделать явно. Например:
классN1::классN1();
Производный класс будет иметь все свойства и методы родительского класса. Но их можно и переопределить в производном классе.
Пример 4
<html>
<head>
<title>Переопределение метода родительского класса</title>
</head>
<body>
<?php
class классN3
{
public $имя = "Маша";
function Привет()
{
echo "<H1>".$this->имя."! Привет!</H1>";
}
}
class классN4 extends классN3
{
function Привет()
{
echo "<H1>".$this->имя."! Какая встреча!</H1>";
}
}
$obj = new классN4();
$obj->Привет();
?>
</body>
</html>
Метод Привет переопределен для производного класса. Свойство имя наследуется от родительского.
Начиная с 4-й версии PHP, в объекте производного класса можно вызвать метод родительского класса, который был переопределен.
Пример 5
<html>
<head>
<title>Вызов метода родительского класса</title>
</head>
<body>
<?php
class классN5
{
public $имя = "Маша";
function Привет()
{
echo "<H1>".$this->имя."! Привет!</H1>";
}
function Пока()
{
echo "<H1>".$this->имя.", пока!</H1>";
}
}
/**
* Class классN6
*/
class классN6 extends классN5
{
/**
*
*/
function Привет()
{
echo "<H1>".$this->имя."! Какая встреча!</H1>";
классN5::Привет();
}
}
$obj = new классN6();
$obj->Привет();
$obj->Пока();
?>
</body>
</html>
Итак, производный класс может наследовать, переопределять и дополнять свойства и методы другого класса.
В следующем примере создан класс HTMLTable, основанный на классе Table из примера 3. Новый класс формирует данные, сохраненные методом addRow родительского класса, и выводит их в HTML-таблицу. Свойства $cellpadding и $bgcolor дают возможность изменять соответствующие аргументы, при этом переменной $cellpadding присваивается значение по умолчанию, равное 2.
Пример 6
<html>
<head>
<title>Классы Table и HTMLTable</title>
</head>
<body>
<?php
class Tables
{
public $headers = [];
public $data = [];
function Tables( $headers )
{
$this->headers = $headers;
}
function addRow ( $row )
{
$tmp = [];
foreach ( $this->headers as $header )
{
if ( ! isset( $row[$header] )) $row[$header] = "";
$tmp[] = $row[$header];
}
array_push ( $this->data, $tmp );
}
function output ()
{
echo "<PRE><B>";
foreach ( $this->headers as $header ) echo "$header ";
echo "</B><BR>";
foreach ( $this->data as $y )
{
foreach ( $y as $x ) echo "$x ";
echo "<BR>";
}
echo "</PRE>";
}
}
class HTMLTable extends Tables
{
public $cellpadding = "2";
public $bgcolor;
function HTMLTable ( $headers, $bg="FFFFFF" )
{
Tables::Tables( $headers );
$this->bgcolor = $bg;
}
function setCellpadding ( $padding )
{
$this->cellpadding = $padding;
}
function output ()
{
echo "<table cellpadding='".$this->cellpadding."'><tr>";
foreach ( $this->headers as $header )
echo "<th bgcolor='".$this->bgcolor."'>".$header;
foreach ( $this->data as $y )
{
echo "<tr>";
foreach ( $y as $x )
echo "<td bgcolor='".$this->bgcolor."'>$x";
}
echo "</table>";
}
}
$test = new HTMLTable ( array("a","b","c"), "#00FFFF" );
$test->setCellpadding ( 7 );
$test->addRow(array("a"=>1,"b"=>3,"c"=>2));
$test->addRow(array("b"=>1,"a"=>3));
$test->addRow(array("c"=>1,"b"=>3,"a"=>4));
$test->output();
?>
</body>
</html>
Обратите внимание на то, что значение свойства сellpadding меняется с помощью отдельного метода setCellpadding. Конечно, значения свойств можно менять непосредственно, вне объекта:
$test->сellpadding = 7 ;
Но это считается дурным тоном, т.к. в сложных объектах при изменении одного из свойств могут изменяться и другие свойства.
Использовать или нет технику объектного программирования? С одной стороны, проект, интенсивно использующий объектную технику, может занимать слишком много ресурсов во время выполнения. С другой стороны, правильно организованный объектный подход значительно сократит время разработки и сделает программу более гибкой.
Удаление объектов
Удалить ранее созданный объект можно следующим образом:
unset($objName);
Ниже приведен пример, в котором объект класса Саг создается, а затем удаляется.
$myCar = new Car; unset($myCar);
После вызова функции unset() объект больше не существует. В РНР имеется специальный метод __destruct(), который автоматически вызывается при удалении объекта. Ниже приведен класс, содержащий этот метод.
class Bridge
{
function __destruct()
{
echo "Мост разрушен";
}
}
$bigBridge = new Bridge;
unset($bigBridge);
При создании объекта класса Bridge, а затем его удалении отобразится следующее сообщение:
Мост разрушен
Оно отображается вследствие вызова метода __destruct() при вызове функции unset(). При удалении объекта может потребоваться акрыть некоторые файлы или записать информацию в базу данных.
Копирование (клонирование) объекта
Клонирование объекта:
$a = clone $b;
Конструктор не вызывается при клонировании, вызывается магический метод __clone(){}
.
Он НЕ принимает аргументов и к нему нельзя обратиться как к методу.
Преобразование объекта в строку
Для конвертации объекта в строку, и обратно, используются следующие функции:
serialize() - принимает объект и возвращает строковое представление его класса и свойств;
unserialize() - принимает строку, созданную при помощи serialize(), и возвращает объект.
serialize() и unserialize() работают со всеми типами данных, но они не работают с ресурсами.
Специальные методы для обслуживания функций serialize() и unserialize():
__sleep() - вызывается строго перед тем, как объект сериализуется с помощью функции serialize().
Функция __sleep() должна будет вернуть список полей класса, которые функция serialize() включит в возвращаемую
строку.
Вы можете использовать это для того, чтобы исключить ненужные поля из строкового представления объекта.
Например:
public function __sleep() { // почистить
return array_keys( get_object_vars( $this ) );
}
__wakeup() - вызывается сразу после того, как объект десериализуется с помощью unserialize().
Абстрактный класс
Абстрактный класс - это класс, который не может быть реализован, то есть, вы не сможете создать объект класса, если он абстрактный. Вместо этого вы создаете дочерние классы от него и спокойно создаете объекты от этих дочерних классов. Абстрактные классы представляют собой шаблоны для создания классов.
abstract class Person {
private $firstName = "";
private $lastName = "";
public function setName( $firstName, $lastName ) {
$this->firstName = $firstName;
$this->lastName = $lastName;
}
public function getName() {
return "$this->firstName $this->lastName";
}
abstract public function showWelcomeMessage();
/* абстрактный метод showWelcomeMessage().
Так как он абстрактный, в нем нет ни строчки кода, это просто его объявление.
Любой дочерний класс обязан добавить и описать метод showWelcomeMessage() */
}
Интерфейс
Интерфейс - это шаблон, который задает поведение одного или более классов. Вот основные отличия между интерфейсами и абстрактными классами:
- Ни один метод не может быть описан в интерфейсе. Они все абстрактны. В абстрактном классе могут быть и не абстрактные методы.
- Интерфейс не может содержать полей - только методы.
- Класс имплементирует интерфейс, и класс наследует или расширяет другой класс.
- Класс может имплементировать несколько интерфейсов одновременно. Этот же класс может наследовать другой класс. Но у дочернего класса может быть только один супер-класс (абстрактный или нет).
interface MyInterface {
public function aMethod();
public function anotherMethod();
}
class MyClass implements MyInterface {
public function aMethod() {
// (имплементация метода)
}
public function anotherMethod() {
// (имплементация метода)
}
}
Методы-перехватчики (магические методы)
- __get($property) - вызывается при обращении к неопределенному свойству
- __set($property,$value) - вызывается, когда неопределенному свойству присваивается значение
- __unset($property) - вызывается, когда функция unset() вызывается для неопределенного свойства
- __isset($property) - вызывается, когда функция isset() вызывается для неопределенного свойства
- __call($method,$arg array) - вызывается при обращении к неопределенному методу
- __callStatic($method,$arg array) - вызывается при обращении к неопределенному статическому методу
- __toString() - Вызывается, если есть попытка вывести объект, как строку.
- __debugInfo() - В PHP 5.6 был добавлен новый магический метод, который позволяет менять свойства и значения объекта, когда он печатается с помощью функции var_dump(класс).
- __invoke() - для вызова объекта как функции. Пример
Пример использования необъявленных свойств класса
Где и зачем могут быть использованны методы-перехватчики?
Например есть у вас таблица в базе данных, называется user и есть в ней некие поля, например id, name, email, phone, password, avatar И Вы создали класс на для работы с юзерами, так его и назвали - User
Какие свойства будут у данного класса? Если вы сделаете такие же как в БД - id, name, email и так далее,
то получается что при каждом изменении базы данных - вам нужно менять код в классе User, как то не очень удобно.
Добавили вы например поле site - значит нужно его добавлять и в класс User, ну и так далее.
Используя же методы __get() и __set() Вы можете это всё автоматизировать.
У вас в классе User вообще не будет ни одного свойства из БД, у нас есть допустим только одно $data - мы туда
взяли, да и загрузили всё что есть в базе данных на данного пользователя.
А потом, когда программист что то запрашивает, например $user->email мы просто в __get() методе можете
посмотреть - если мы такую информацию загрузили из БД, и она лежит в $data['email'] - то вот мы её вам и
возвращаем.
А в __set() наоборот. Есть такое поле в БД? Значит присвоим ему новое значение.
/**
* Class User
* @property-read integer id текущего пользователя
* @property-write String site возвращает ссылку на сайт пользователя
*/
class User
{
private $data;
private $f_write=false;
public function __set($name, $value) {
$this->data[$name] = $value;
$this->f_write=true; // признак, что нужно сохранить данные
}
public function __get($name) {
if(empty($data)){
// читаем запись из БД в data
}
return $this->data[$name];
}
function __destruct()
{
if(!empty($data)&&$this->f_write){
// сохраняем изменения в БД
}
}
}
$user=new User();
$user->site='http://kdg.htmlweb.ru/'; //присваеваем переменной
echo $user->site; //выводим значение переменной
// записываем в БД. Можно это явно не делать, т.к. при окончании работы скрипта это поизойдет автоматически
unset($user);
Пример использование необъявленного свойства класса как элемент массива
Обратите внимание на то, что из __get возвращается ссылка:
class Foo {
private $data = [];
public function __set($name, $value) {
$this->data[$name] = $value;
}
public function & __get($name) {
return $this->data[$name];
}
}
$foo = new Foo();
$foo->bar[2] = 'lol';
var_dump($foo->bar);
Использоватние перехватчиков обращения к необъявленным методам класса
class OurClass
{
public function __call($name,array $params)
{
echo 'Вы хотели вызвать $Object->'.$name.', но его не существует,
и сейчас выполняется '.__METHOD__.'()';
return;
}
public static function __callStatic($name,array $params)
{
echo 'Вы хотели вызвать '.__CLASS__.'::'.$name.', но его не существует,
и сейчас выполняется '.__METHOD__.'()';
return;
}
}
$Object=new OurClass;
$Object->DynamicMethod();
OurClass::StaticMethod();
Пример обхода закрытых метов класса:
class _byCallStatic{
// Пример обхода "закрытых" методов класса,
// при использовании метода "__callStatic()" для вызова статического метода.
public static function __callStatic($_name, $_param) {
return call_user_func_array('static::'. $_name, $_param);
}
private static function _newCall(){ echo 'Method: '. __METHOD__; }
}
echo _byCallStatic::_newCall(114, 'Integer', 157); # Результат: Method: _byCallStatic::_newCall
Как вызвать через статический метод любой динамический:
/**
* Class o
* @method static void __f(int $a1 = 1)
*/
class o
{
public static function __callStatic($method, $args)
{
$class = get_called_class();
$obj = new $class($args[0]);
$method = substr($method, 2);
$pass = array_slice($args,1);
$reflection = new ReflectionMethod($obj, $method);
return $reflection->invokeArgs($obj, $pass);
}
public function f($a1 = 1) {
var_dump('oo', func_get_args());
}
}
class a extends o
{
public function f($a1 = 1, $a2 = 2) { var_dump('aa', $a1 ); }
}
class b extends o
{
public function f($b1 = 1) { var_dump('bb', $b1); }
}
a::__f(1,2,3);
b::__f(4,5,6);
Как использовать объект как функцию?
class Dog
{
private $name;
public function __construct($dogName = 'Тузик') {
$this->name = $dogName;
}
public static function __invoke() {
$args = func_get_args();
echo 'Собака получила: ' . implode(' и ', $args);
}
}
$dog = new Dog('Мухтар');
$dog('кость', 'поводок');
Как обращаться к объекту как к массиву?
Для этого необходимо создать такой объект который реализует интерфейс ArrayAccess из SPL. Следующий пример реализует объект доступ к данным которого можно получать как в стиле обращения к массиву, так и через получение свойств:
class MyArray implements ArrayAccess
{
protected $arr = array();
public function offsetSet($key, $value) {
$this->arr[$key] = $value;
}
public function offsetUnset($key) {
unset($this->arr[$key]);
}
public function offsetGet($key) {
return $this->arr[$key];
}
public function offsetExists($key) {
return isset($this->arr[$key]);
}
public function __get($key)
{
return $this->offsetGet($key);
}
public function __set($key, $val)
{
$this->offsetSet($key, $val);
}
}
$a = new MyArray();
$a['whoam'] = 'Я значение массива, или объекта? <br/ >';
echo $a['whoam'];
echo $a->whoam;
Автозагрузка классов
Файлы автозагружаемых классов обычно располагаются в общем месте, например в /include/class/. Имя файла формируется в формате ИМЯ_КЛАССА.php. Данный код необходимо подключить во все PHP-скрипты:
spl_autoload_register(function ($class_name) {
//echo "Autoload ".$class_name;
$file = $_SERVER['DOCUMENT_ROOT'] . "/include/class/" . strtolower($class_name) . '.php';
if (file_exists($file) == false) {
if($GLOBALS['DEBUG']) echo "Нет файла ".$file;
return false;
}
include_once($file);
return true;
});
Для автоподгрузки классов можно также использовать определение функции __autoload();
Обработка исключений в ООП
Для обработки некритических ошибок используются исключения(Exception).
try {
$a = 1;
$b = 0;
if($b == 0)
throw new Exception ("деление на ноль!");
$c = $a/$b;
} catch (Exception $e) {
echo $e->getMessage();
echo $e->getLine();
}
Exception — встроенный класс. Если попали в throw, то код ниже не выполняется и осуществляется переход к блоку catch.
Блок try-catch используется как в процедурном, так и в ООП программировании. Он используется для отлова ошибок — большой блок try с множеством throw и все отлавливаются в одном месте — блоке catch.
Exception можно наследовать, желательно при этом перезагрузить конструктор:
class MyException extends Exception {
function __construct($msg){
parent::__construct($msg);
}
}
Блоков catch может быть несколько — для каждого класса наследника Exception.