Только для читателей Lifeexample возможно открыть интернет-магазин на Moguta.CMS со скидкой в 15%

<<< XSS уязвимость и защита от XSS || Бесплатная CMS для интернет магазина >>>

PHP Singleton (Синглтон)

26.07.2012
PHP Singleton (Синглтон)

Здравствуйте, уважаемые читатели блога LifeExample, сегодня мы будем разбираться с очередным шаблоном программирования в PHP — Синглтоном (Singleton).

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

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

Концепция шаблона Singleton

Страшное слово синглтон ассоциируемое у меня, по названию, c мелафоном из сериала «Гостья из будущего» (1984 г.), совершенно не имеет никакого сходства с машиной времени.

Singleton – это обычный php класс, в логику работы которого включена проверка на эксклюзивность его создания. Т.е. объект класса, построенный по шаблону синглтона, может быть создан лишь один раз. Все будущие попытки обратиться к его методу или свойству, создав новый объект, будут нейтрализованы логикой его работы и перенаправлены на уже имеющийся экземпляр.

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

Представим, что у нас есть класс, инициализирующий какие либо настройки. Для инициализации он может считывать информацию из текстовых файлов и/или таблиц БД.

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

В ходе работы системы, проведение таких операций может потребоваться очень часто, и каждый раз будет создаваться новый экземпляр класса, а соответственно будет дублироваться работа по сбору настроек из БД и файлов, что не очень лестно скажется на работе всей системы.

Как вариант можно создать глобальный объект и пользоваться только им, но ничто не защитит его от повторного создания, и пришедший в команду новый неопытный разработчик, пренебрегая соглашением об использовании глобального объекта, будет создавать дубликат столько раз, сколько ему захочется.

Решением этой проблемы как раз и занимается шаблон Singleton.

Обычный пример PHP Singleton

Давайте ознакомимся с концепцией и классическим примером реализации php синглтона. Взгляните на этот код:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
 class someClass {
    protected static $_instance;

        private function __construct() {        
    }

    public static function getInstance() {
        if (self::$_instance === null) {
            self::$_instance = new self;  
        }
 
        return self::$_instance;
    }
 
    private function __clone() {
    }

    private function __wakeup() {
    }    
}
?>

В данном коде представлен некоторый класс, в логике работы которого, реализован паттерн – синглтон.

Первым делом объявляется защищенная статическая переменная $_instance, назначением которой является хранение в себе единственного экземпляра данного класса.

Конструктор в синглтоне защищен от публичного вызова, а следовательно исключает дублирование объектов.

Другими словами, если попытаться создать объект (PHP Singleton) класса стандартным образом:

1
obj= new someClass();

то мы получим сообщение об ошибке, в котором сказано, что конструктор доступен только изнутри класса.

1
Call to private someClass::__construct() from invalid context in…

Для того чтобы создать объект нам нужно вызвать статический метод getInstance(); таким образом:

1
someClass::getInstance();

Единственной точкой входа в класс является статическая функция getInstance() цель которой – создать первый и единственный экземпляр класса, записав его в переменную $_instance. При вызове getInstance() функция создает экземпляр класса, а следовательно задействует механизм конструктора.

Неотъемлемой частью синглтона в PHP является объявление приватных магических методов __clone() и _wakeup() , предотвращающих возможные дублирования объекта.

На этом классическое описание модели, во многих ресурсах сети заканчивается. Но остается вопрос, как пользоваться синглтоном в PHP, а главное зачем?

Пока не попробуете на практике, смысла не уловите, поэтому я настоятельно советую вам потратить еще 10 минут своего свободного времени, и посмотреть как можно на практике использовать паттерн Singleton в PHP.

PHP Singleton на примере класса для работы с БД

Если кто-то забыл, или не знает основ для работы с базами данных в php, читайте статью PHP работа с базой данных (Часть 1)

Самым подходящим примером будет маленькая система для работы с БД. Под маленькой системой я подразумеваю три файла :

  • index.php — точка входа в систему
  • class.db.php — singleton для работы с БД
  • class.order.php — некоторый вымышленный класс внутри которого идет работа с БД с помощью синглтона

Первым делом создадим index.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
error_reporting(E_ALL);
require 'class.db.php';

// константы для подключени к БД

define('HOST', 'localhost'); //сервер
define('USER', 'root'); //пользователь
define('PASSWORD', ''); //пароль
define('NAME_BD', 'test');//база   
 
DB::getInstance(); // инициализация экземпляра класса дл работы с БД
//свободное использование класса

//вывод таблицы продуктов
$result=DB::query("SELECT id,product FROM `product`");
echo '<h2>Таблица продуктов:</h2> <table border="1">';
while($obj=DB::fetch_object($result)){
    echo '<tr><td>'.$obj->id.'</td><td>'.$obj->product.'</td><tr>';    
}
echo '</table>';

В коде index.php подключается файл class.db.php, содержащий реализацию синглтона, и производится инициализация констант для доступа к базе данных, а также инициализация объекта для работы с БД DB::getInstance();.

Примечание! Для более оптимизированной работы с синглтоном, можно внести вызов getInstance(); внутрь используемых классом публичных функций. Это поможет устранить необходимость явного определения экземпляра класса описанным выше методом DB::getInstance();

Далее следует пример работы с классом синглтона, в котором выводится содержимое таблицы product.

Чтобы это все заработало нужно создать файл class.db.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<?class DB {

        protected static $_instance;  //экземпляр объекта
 
        public static function getInstance() { // получить экземпляр данного класса
            if (self::$_instance === null) { // если экземпляр данного класса  не создан
                self::$_instance = new self;  // создаем экземпляр данного класса
            }
            return self::$_instance; // возвращаем экземпляр данного класса
        }
   
        private  function __construct() { // конструктор отрабатывает один раз при вызове DB::getInstance();
                echo "<br/><em>1.  Установка соединения с хостом...";
                //подключаемся к БД
                $this->connect = mysql_connect(HOST, USER, PASSWORD) or die("Невозможно установить соединение".mysql_error());
                // выбираем таблицу
                echo "<br/>2.  Выбор базы...";
                mysql_select_db(NAME_BD, $this->connect) or die ("Невозможно выбрать указанную базу".mysql_error());
                // устанавливаем кодировку таблицы
                echo "<br/>3.  Устанавливаем кодировку базы: ";
                $this->query('SET names "utf8"');  
                echo "<br/> Конструктор успешно открыл соединение с БД! и установил кодировку.</em>";
       
        }
 
        private function __clone() { //запрещаем клонирование объекта модификатором private
        }
       
        private function __wakeup() {//запрещаем клонирование объекта модификатором private
        }
   
        public static function query($sql) {
       
            $obj=self::$_instance;
       
            if(isset($obj->connect)){
                $obj->count_sql++;
                $start_time_sql = microtime(true);
                $result=mysql_query($sql)or die("<br/><span style='color:red'>Ошибка в SQL запросе:</span> ".mysql_error());
                $time_sql = microtime(true) - $start_time_sql;
                echo "<br/><br/><span style='color:blue'> <span style='color:green'># Запрос номер ".$obj->count_sql.": </span>".$sql."</span> <span style='color:green'>(".round($time_sql,4)." msec )</span>";            
               
                return $result;
            }
            return false;
        }  
   
        //возвращает запись в виде объекта
        public static function fetch_object($object)
        {
            return @mysql_fetch_object($object);
        }

        //возвращает запись в виде массива
        public static function fetch_array($object)
        {
            return @mysql_fetch_array($object);
        }
       
        //mysql_insert_id() возвращает ID,
        //сгенерированный колонкой с AUTO_INCREMENT последним запросом INSERT к серверу
        public static function insert_id()
        {
            return @mysql_insert_id();
        }
       
   
}

Сей класс является рабочей реализацией паттерна singleton в php, а также наглядно демонстрирует важность единственности объекта. Для тех кто не видит важности поясню: в конструкторе данного класса производится подключение к БД, если убрать логику синглтона, то нерадивые программисты смогут организовать тысячи бессмысленных подключений к БД .

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

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

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

На этом описание класса предлагаю завершить, но если есть вопросы пишите в комментариях, отвечу.

Теперь файл index.php можно запускать. Ах да, конечно же вам понадобится импортировать дамп тестовой базы, приложенный в исходниках, или обращаться к своей БД :

Наследование Singleton в PHP

Мы уже имеем возможность обращаться к функциям синглтона, теперь попробуем сделать это в отдельном классе Order.

Многие новички делают не правильные выводы относительно данного шаблона программирования и пытаются сделать наследование дочерних классов от синглтона.

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

Если вы хотите наследовать некоторый класс от синглтона, то в самом синглтоне вам придется открыть публичный доступ к конструктору класса. В противном случае вы будете получать ошибку:

1
Call to private someClass::__construct() from invalid context in…

А открыв публичный доступ ломается вся защита.

Поэтому наследовать PHP Singleton, не рационально и бессмысленно, так как можно обращаться к классу синглтона из любой точки программы, инициализировав его единожды в index.php.

В следующем классе это наглядно продемонстрировано:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
//попробуем создать класс и внутри него работать с БД
class Order {
    public function getOrderList(){
   
       // $result=DB::query("INSERT `order` SET id=4,content=777");
        $result=DB::query("SELECT id,content FROM `order` where id=4");
       
        $html='<h2>Таблица заказов:</h2> <table border="1">';
        while($obj=DB::fetch_object($result)){
            $html.='<tr><td>'.$obj->id.'</td><td>'.$obj->content.'</td><tr>';      
        }
        $html.='</table>';
        return $html;
    }      
}

Создайте class.order.php и вставьте в него вышеприведенный код. В конец файла index.php допишите следующее:

1
2
3
4
5
6
7
8
require 'class.order.php';

$order = new  Order();
echo $order->getOrderList();

//попробуем создать новый экземпляр DB
echo "<span style='color:red'>";
$DB2 = new  DB();

Запустив приложение вы должны будете наблюдать такую картину:

Пример синглтона в php

Ошибка в конце страницы сгенерирована намеренно, для демонстрации защиты от дублирования PHP Singleton. Избавиться от ошибки можно, удалив последнюю строку:

1
$DB2 = new  DB();

На этом, я предлагаю завершить наше знакомство с очередным шаблоном программирования PHP Singleton‘ом, надеюсь, материал окажется вам полезен. Подписывайтесь на RSS обновления и не пропустите следующей статьи о другом не мало известном паттерне программирования Registry.

Чтобы не пропустить публикацию следующей статьи подписывайтесь на рассылку по E-mail или RSS ленту блога.

Нравится

Комментарии

  • Дiмка

    Отлично, Марк!! Замечательная статья!! Ты прям читаешь мысли, хотел тебя просить доходчиво объяснить про паттерны программирования. Очень полезно!!

    • На здоровье, рад что понравилось. )

  • hunty

    Неплохая статья, начинаю как раз изучать паттерны — все доступно разжевано. Ошибок только много грамматических, немного раздражает ). Но это мелочи. Еще хотелось бы видеть примеры использования Singleton скажем с PDO или mysqli (так как поддержка mysql модуля будет прекращена), а так же вариант отдельного хранения настроек БД скажем БД в ini или xml, но никак не в index.php.
    Спасибо за внимание ) И за статью.

    • С грамматикой у меня проблемы. Поправил явные ошибки и опечатки, теперь по легче читать будет. Про PDO и mysqli? в планах, есть статья. Зачем хранить настройки для подключения к базе в INI или XML? Чем хуже php файл?

  • hunty

    Вот такой вот вопрос: Почему не используется конструктор в синглтоне? Например для задания\чтения настроек. Или синглтон не предусматривает каких либо свойств класса и дополнительных методов (в т.ч. в конструкторе)?

    • Почему не использую? В данном примере конструктор используется для установки соединения с базой и задания кодировки таблиц.

      >Или синглтон не предусматривает каких либо свойств класса и дополнительных методов (в т.ч. в конструкторе)?
      Синглтон, не исключает использование свойств и методов класса. В примере также демомнстрируется обращение к методу query().

  • Примечание! Для более оптимизированной работы с синглтоном, можно внести вызов getInstance(); внутрь используемых классом публичных функций. Это поможет устранить необходимость явного определения экземпляра класса описанным выше методом DB::getInstance();

  • Дмитрий

    Спасибо, наконец-то нашёл нормальное объяснение синглтон с наследованием.

  • Юрий

    Спасибо!
    Сегодня и я понял зачем он )

  • Георгий

    Спасибо, очень полезная статья!
    Маленькая только ошибочка в классе: вместо

    1
    $this->query('SET names "utf8"');

    надо

    1
    mysql_query('SET names "utf8"', $this->connect);

    — тогда кодировка работает.

  • styv

    Я вот не совсем понял с:
    — public function fetch_object
    — public function fetch_array
    — public function insert_id

    В index.php DB::fetch_object вызываются как статическая, а в самом классе они не являются таковыми.

    Почему так?
    Спасибо.

    • Это ошибка, конечно нужно объявлять функции статическими: static .
      Поправил пример.

  • Виктор

    Огромное спасибо за статью на человеческом языке. Так и паттерны учить можно )

  • Алексей

    Спасибо! Статья просто супер!

  • спасибо человеческое) побольше бы таких авторов и статей) удачи)

  • Сергей

    Спасибо большое, благодаря Вашей статьи наконецто произошел сдвиг с мертвой точки в обучении патернов.

    • Всегда рад, помочь. Кроме PHP Singleton (Синглтон) на блоге можно найти еще статьи по паттернам.

  • Виталий

    А почему в классе не определено свойство connect?

    1
    2
    3
    4
    5
    6
    private  function __construct() { // конструктор отрабатывает один раз при вызове DB::getInstance();
                    echo "<br/><em>1.  Установка соединения с хостом...";
                    //подключаемся к БД
                    $this->connect = mysql_connect(HOST, USER, PASSWORD) or die("Невозможно установить соединение".mysql_error());
                    // выбираем таблицу
    ....

    или вот еще пример

    1
    2
    3
    4
    ....
    if(isset($obj->connect)){
                    $obj->count_sql++;
    ....

    Вроде как также отсутствует объявление в классе count_sql.

  • Taras

    Как в Singleton можна реализовать переключение между базами

    • Можно реализовать. Создайте метод для этого внутри класса.

  • Selen

    Спасибо за статью!

    Вопрос: все public методы надо объявлять static?

    Добавление: если класс объявить final, то и наследовать
    будет нельзя.

  • Алексей

    Огромное спасибо! Очень полезная статья.Действительно почти нигде не встретишь вот такой красивый пример применения паттерна.

  • Vijit

    +1 к «Виталий 18.11.2013»
    У вас не объявлены свойства класса:

    $obj->connect
    $obj->count_sql

    Магических геттеров нет. Так как же это работает?

  • Саша

    Я че-то не пойму- зачем в методе __construct делать подключение к базе. Если нерадивый программист попытается создать новый объект- ошибка конечно выведется, но после подключения к базе. Или я не прав?

    • Смысл сингл тона в том чтобы не допустить нескольких подключений к базе. Код в теле конструктора будет отрабатывать только один раз. А при последующих попытках создать экземпляр класса будет игнорироваться.

  • index

    Марк, спасибо большое, очень ясное изложение мыслей!

  • Туран

    Простите,но вы так и не ответили на вопрос откуда взято это:
    $obj->count_sql.Спасибо

    • Alex

      Аналогично не понял как это работает

  • Сергей Рудаков

    Ошибочка в коде:
    $this->query(‘SET names «utf8″‘);

    Этот запрос выполнен не будет, ибо

    if(isset($obj->connect)){

    }
    Проверочку не пройдёт. Метод вызывается из конструктора и на момент вызова объект ещё создан не будет.
    В вашем скриншоте сообщение о выполнении запроса с кодировкой отсутствует)

  • Дима

    В синглтоне можно организовать что если $exp != null mysql_close() и вернуть уже пустую $exp ?

  • Оставить комментарий

    Подписаться на комментарии к этой статье по RSS

    Яндекс.Метрика