Динамический SELECT

Оригинал этой статьи был написан мною же в июне 2019 года на моем личном сайте. Потихоньку весь материал будет переезжать на этот сайт.
Уже давным-давно встроенные в PHP функции mysql_ для работы с СУБД MySQL были объявлены устаревшими, а начиная с PHP 7.0.0 их и вовсе исключили. Теперь нужно использовать либо функции mysqli_, либо использовать расширение PDO (Объекты данных PHP), встроенное в PHP.
Для тех, кто, в общем-то, понимает, о чём пойдет речь дальше, для тех, кому лень читать всё, а также для тех, кто просто хочет найти файлы с кодом готового решения, я предлагаю сразу же скачать подготовленный архив.
Взглянуть на рабочий пример всего того, что будет описано ниже, можете здесь.

Почему именно через PDO.

В одной хорошей книге был дан совет программировать на уровне интерфейса, а не на уровне реализации. Значит нужно придерживаться объектно-ориентированных приемов написания собственного кода. А это значит, что использование PDO в наших PHP-сценариях для соединения с СУБД MySQL предпочтительнее.

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

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

Подключаемся к MySQL через PDO.

Строка подключения может выглядеть так:


		
<?php
$dbh = new PDO( 'mysql:host=localhost;dbname=demo_pdo-mysql;charset=utf8;', 'demo', 'sadhiJ7slv!a' );
?>

Создаем собственный класс для подключения к СУБД

Лучше помещать любые обращения к СУБД в блоки try - catch, чтобы избегать прерывания выполнения кода сценария в случае возникновения ошибки, перехватывая исключения. Но вернемся к этому позже.

PDO — это класс. Значит мы можем унаследовать от него все доступные свойства и методы, чтобы использовать их в своих классах. Напишем свой собственный класс для подключения к СУБД MySQL, чтобы подключаться к нему удобным для нас способом.

Подключение к Базе данных может выглядеть так.


		
<?php
// Подключаемся к СУБД MySQL через вызов конструктора собственного класса
// Этот класс наследует свойства и методы от класса PDO.
$dbh = new DB();
?>

А может выглядеть так:


		
<?php
$params = array(
	'driver' => 'mysql',
	'host' => 'localhost',
	'dbname' => 'demo_pdo-mysql',
	'charset' => 'utf8',
	'username' => 'demo',
	'password' => 'sadhiJ7slv!a'
);

/**
 *  Подключаемся к СУБД MySQL через вызов конструктора собственного класса
 *  Этот класс наследует свойства и методы от класса PDO.
 *  
 *  При вызове конструктора передаём параметры подключения в виде массива,
 *  с которыми внутри функции-конструктора нашего класса что-то уже делается
 */
$dbh = new DB( $params );
?>
    

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

В этой статье я не останавлиюваюсь на разъяснениях и описаниях таких понятий, как: классы в PHP, методы классов в PHP, свойства классов в PHP, наследование в PHP, функция-конструктор класса в PHP, функция-деструктор класса в PHP и других подобных этим. Принимаем за данность то, что если вы пришли к необходимости ознакомления с этой статьёй, озвученные выше базовые знания и понятия у вас есть.

Данные с параметрами подключения можно хранить в защищенном статическом свойстве класса. Но что произойдет, если сделать следующее?


		
<?php
/**
 *  Подключаемся к СУБД MySQL через вызов конструктора собственного класса
 *  Этот класс наследует свойства и методы от класса PDO.
 *  
 *  При вызове конструктора наш пользовательский класс устанавливает соединение 
 *  с СУБД MySQL через те параметры, которые содержатся внутри класса 
 *  в статическом свойстве.
 */
$dbh_1 = new DB();
$dbh_2 = new DB();
$dbh_3 = new DB();
$dbh_4 = new DB();
?>
    

Будут созданы четыре идентичных экземпляра одного и того же класса. А зачем это нужно? Ну, в общем-то, если вам именно такое поведение PHP-сценария не нужно, то оно и не нужно. Как защититься от такого? Легко! Нужно создавать экзепляр класса, используя следующую конструкцию.


		
<?php
/**
 *  Подключаемся к СУБД MySQL через вызов конструктора собственного класса
 *  Этот класс наследует свойства и методы от класса PDO.
 *  
 *  При вызове конструктора наш пользовательский класс устанавливает соединение 
 *  с СУБД MySQL через те параметры, которые содержатся внутри класса 
 *  в статическом свойстве.
 */
$dbh = DB::instance();
?>

Статический метод instance() вернет вызову либо уже существующее соединение с СУБД, либо при отсутствии такого установит соединение и опять-таки вернет его вызову.

Ну и к этому моменту у вас, скорее всего, назрел вопрос... а что вообще происходит и как это сделано? Давайте представим, как может выглядеть наш пользовательский класс DB, с помощью которого мы через PDO подключаемся к MySQL описанным выше способом.


		
<?php
/**
 *  Пользовательский класс для создания подключения к СУБД MySQL,
 *  наследующий свойства и методы встроенного в PHP класса PDO
 */
class DB extends PDO {
	
	/**
	 *  Параметры подключения к СУБД
	 */
	public static $_aParams = array(
							
							'driver' => 'mysql',				// Используемый драйвер для соединения
							'host' => 'localhost',				// Адрес СУБД
							'dbname' => 'demo_pdo-mysql',			// Имя базы данных MySQL
							'charset' => 'utf8',				// Кодировка соединения
							'username' => 'demo',				// Имя пользователя MySQL
							'password' => 'sadhiJ7slv!a',			// Пароль пользователя
							'params' => array(					// Дополнительные параметры подключения
		
								PDO::ATTR_PERSISTENT => true, 	// Использовать постоянные подключения
								
							)
							
	);
	
	/**
	 *  Защищенное статическое свойство, в котором будет храниться установленное подключение к СУБД
	 */
	protected static $_dbh = NULL;
	
	/**
	 *  Защищенное статическое свойство, в котором хранится статус наличия созданного экзепляра объекта нашего класса
	 */
	protected static $_instance = FALSE;
	
	/**
		*  @brief Конструктор класса подключения к СУБД
	 *  
	 *  @details Частный метод, который не даст создать экземпляр класса стандартным способом
	 *  в целях защиты от создания нескольких идентичных экземпляров нашего класса
	 */
	private function __construct() {
		
		// Нет необходимости нагружать функцию-конструктор лишними действиями
		// Установку соединения доверим защищенному методу класса 
		$this->_connect();
		
		++self::$count;
	}
	
	/**
		*  @brief Защищенный метод для установки подключения к СУБД
	 */
	protected function _connect() {
		
		// Используя имеющиеся параметры подключения подключимся к СУБД
		$dsn = self::$_aParams[ 'driver' ] . ':host=' .
				self::$_aParams[ 'host' ] . ';dbname=' . 
				self::$_aParams[ 'dbname' ] . ';charset=' . 
				self::$_aParams[ 'charset' ];
		
		/**
		 *  Устанавливаем подключение с СУБД
		 *
		 * Строку с параметрами сервера мы сформировали выше
		 * Теперь добавляем еще имя пользователя, пароль пользователя 
		 * и дополнителные параметры подключения
		 *
		 * Используется именно такая конструкция: parent::__construct(),
		 * которая вызывает конструктор родительского класса PDO
		 */ 
		parent::__construct( $dsn, self::$_aParams[ 'username' ], self::$_aParams[ 'password' ], self::$_aParams[ 'params' ]  );
		
		// В дальнейшем используем именно псевдопеременную $this, так как 
		// наш класс унаследовал все допустимые свойства и методы от класса 
		// PDO, и эта псевдопеременная ссылается именно на свойства и методы 
		// класса PDO, если они не определены в нашем классе
		
		// Устанавливаем режим обработки ошибок,
		// используя для этого метод setAttribute() объекта PDO
		$this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
		
		// Устанавливаем режим выборки по умолчанию для объекта запроса,
		// используя для этого метод setAttribute() объекта PDO
		$this->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC );
		
		// После того, как будут выполнены все действия 
		// установить значение TRUE для свойства, в котором хранится информация
		// о существовании созданного объекта нашего класса
		self::$_instance = TRUE;
		
		// Сохраняем в статическое свойство экземпляр объекта, подключенный к СУБД
		self::$_dbh = $this;
		
	}
	
	/**
		*  @brief статический метод получения экзепляра объекта класса подключения к СУБД
	 *  
	 *  @return свойство, в котором хранится подключение к СУБД
	 *  
	 */
	public static function instance() {
		
		// Если ранее был создан экземпляр объекта этого класса
		if ( self::$_instance !== FALSE ) {
			
			// Вернуть этот экземпляр объекта
			return self::$_dbh;
			
		}
		
		// Если такой экзепляр ещё не существует, создать его
		return new self();
		
	}
	
	/**
	 *  Статическое защищенное свойство, которое будет увеличиваться на 1
	 *  при каждом создании экземпляра объекта нашего класса. Служит только
	 *  для того, чтобы вы могли проверить, действительно ли нельзя создать
	 *  несколько экземпляров объектов нашего класса
	 */
	protected static $count = 0;
	
	/**
		*  @brief Получение значений счетчика экземпляров объекта класса
	 *  
	 *  @return число созданных экземпляров
	 */
	public static function getCount() {
		
		return self::$count;
		
	}
	
}
?>

Обратите внимание на так названный «счётчик» внутри нашего класса. Вы можете попытаться создать несколько экземпляров объекта класса PDO подключения к СУБД MySQL, а затем проверить значение счётчика, вызвав статический метод DB::getCount(). Наример, с помощью такого PHP-кода.


		
<?php
$dbh = DB::instance();
$dbh1 = DB::instance();
$dbh2 = DB::instance();
$dbh3 = DB::instance();

print DB::getCount();	// Выведет на экран 1

$dbh4 = new DB();		// Вызовет фатальную ошибку, так как конструктор класса нельзя вызывать таким образом
?>
    

Код нашего класса достаточно большой, вынесем его в отдельный файл с названием db.php.

Обработка исключения в собственном классе кодключения через PDO

Ранее в предыдущей статье я утверждал, что лучше использовать конструкции для перехвата исключений, чтобы избежать прерывания исполнения кода PHP-сценария, но в приведенном примере кода нашего класса DB вы ничего такого не увидели. Пора исправить это. Добавим к коду в файле db.php в место, где происходит подключение к СУБД MySQL через вызов конструктора класса PDO, конструкцию try — catch.

Эта конструкция будет перехватывать исключения типа PDOException, но такого типа исключений мы добавим свой собственный класс MyPDOException, в котором определим отображение информации об ошибке, если она появится.

Согласно соответствующему разделу официальной документации PHP все объекты, «выбрасывающиеся» с помощью выражения throw, должны наследовать родительский интерфейс Throwable, например будучи наследниками подкласса Exception. Так и сделаем с нашим классом MyPDOException. Но сначала внесём изменения в код в файле db.php


		
<?php
/**
 * файл db.php
 *
 * Пользовательский класс для создания подключения к СУБД MySQL,
 *  наследующий свойства и методы встроенного в PHP класса PDO
 */
class DB extends PDO {
	
	/**
	 *  Параметры подключения к СУБД
	 */
	public static $_aParams = array(
							
							'driver' => 'mysql',				// Используемый драйвер для соединения
							'host' => 'localhost',				// Адрес СУБД
							'dbname' => 'demo_pdo-mysql',		// Имя базы данных MySQL
							'charset' => 'utf8',				// Кодировка соединения
							'username' => 'demo',				// Имя пользователя MySQL
							'password' => 'sadhiJ7slv!a1',		// Пароль пользователя
							'params' => array(					// Дополнительные параметры подключения
		
								PDO::ATTR_PERSISTENT => true, 	// Использовать постоянные подключения
								
							)
							
	);
	
	/**
	 *  Защищенное статическое свойство, в котором будет храниться установленное подключение к СУБД
	 */
	protected static $_dbh = NULL;
	
	/**
	 *  Защищенное статическое свойство, в котором хранится статус наличия созданного экзепляра объекта нашего класса
	 */
	protected static $_instance = FALSE;
	
	/**
		*  @brief Конструктор класса подключения к СУБД
	 *  
	 *  @details Частный метод, который не даст создать экземпляр класса стандартным способом
	 *  в целях защиты от создания нескольких идентичных экземпляров нашего класса
	 */
	private function __construct() {
		
		// Нет необходимости нагружать функцию-конструктор лишними действиями
		// Установку соединения доверим защищенному методу класса 
		$this->_connect();
		
		++self::$count;
	}
	
	/**
		*  @brief Защищенный метод для установки подключения к СУБД
	 */
	protected function _connect() {
		
		// Используя имеющиеся параметры подключения подключимся к СУБД
		$dsn = self::$_aParams[ 'driver' ] . ':host=' .
				self::$_aParams[ 'host' ] . ';dbname=' . 
				self::$_aParams[ 'dbname' ] . ';charset=' . 
				self::$_aParams[ 'charset' ];
		
		/**
		 *  Устанавливаем подключение с СУБД
		 *
		 * Строку с параметрами сервера мы сформировали выше
		 * Теперь добавляем еще имя пользователя, пароль пользователя 
		 * и дополнителные параметры подключения
		 *
		 * Используется именно такая конструкция: parent::__construct(),
		 * которая вызывает конструктор родительского класса PDO
		 */ 
		// Пытаемся установить соединение с СУБД
		try {
		
			parent::__construct( $dsn, self::$_aParams[ 'username' ], self::$_aParams[ 'password' ], self::$_aParams[ 'params' ]  );
		
		}
		// Если соединение установить не удастся, будет выброшено исключение типа PDOException,
		// которое мы перехватим
		catch ( PDOException $e ) {
			
			// Передадим на обработку исключение нашему классу
			MyPDOException::instance( $e );
			
			// Так как подключиться к СУБД не удалось, в дальнейшем выполнении кода
			// сценария нет никакого смысла
			exit();
			
		}
		
		// Дальнейший код будет исполнен в случае успешной установки
		// соединения с СУБД
		
		// Ниже используем именно псевдопеременную $this, так как 
		// наш класс унаследовал все допустимые свойства и методы от класса 
		// PDO, и это псевдопеременная ссылается именно на свойства и методы 
		// класса PDO, если они не определены в нашем классе
		
		// Устанавливаем режим обработки ошибок,
		// используя для этого метод setAttribute() объекта PDO
		$this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
		
		// Устанавливаем режим выборки по умолчанию для объекта запроса,
		// используя для этого метод setAttribute() объекта PDO
		$this->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC );
		
		// После того, как будут выполнены все действия 
		// установить значение TRUE для свойства, в котором хранится информация
		// о существовании созданного объекта нашего класса
		self::$_instance = TRUE;
		
		// Сохраняем в статическое свойство экземпляр объекта, подключенный к СУБД
		self::$_dbh = $this;
		
	}
	
	/**
		*  @brief статический метод получения экзепляра объекта класса подключения к СУБД
	 *  
	 *  @return свойство, в котором хранится подключение к СУБД
	 *  
	 */
	public static function instance() {
		
		// Если ранее был создан экземпляр объекта этого класса
		if ( self::$_instance !== FALSE ) {
			
			// Вернуть этот экземпляр объекта
			return self::$_dbh;
			
		}
		
		// Если такой экзепляр ещё не существует, создать его
		return new self();
		
	}
	
	/**
	 *  Статическое защищенное свойство, которое будет увеличиваться на 1
	 *  при каждом создании экземпляра объекта нашего класса. Служит только
	 *  для того, чтобы вы могли проверить, действительно ли нельзя создать
	 *  несколько экземпляров объектов нашего класса
	 */
	protected static $count = 0;
	
	/**
		*  @brief Получение значений счетчика экземпляров объекта класса
	 *  
	 *  @return число созданных экземпляров
	 */
	public static function getCount() {
		
		return self::$count;
		
	}
	
}
?>

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


		
<?php
/**
 *  файл mypdoexception.php
 *  
 *  Пользовательский класс является потомком класса Exception
 *  Предназначен только лишь для вывода на экран сообщений 
 *  об ошибках при работе с СУБД
 *  
 */
class MyPDOException extends Exception {
	
	/**
	 *  Защищенное свойство, в котором будет храниться 
	 *  экземпляр объекта класса PDO
	 */
	protected $_pdoObject = NULL;
	
	/**
		*  @brief Частная (закрытая) функция-конструктор класса
	 *  
	 *  @param [in] $e Объект типа PDOException
	 *  @details отображает сообщение об ошибке
	 *  
	 */
	private function __construct( PDOException $e ) {
		
		// Сохраняем ссылку на объект типа PDOException
		$this->_pdoObject = $e;
		
		// Формируем строку сообщения об ошибке
		$this->_showMessage();
		
	}
	
	/**
		*  @brief Защищенный метод для формирования строки сообщения об ошибке
	 *  
	 *  @param [in] $string уточненное сообщение об ошибке
	 *  
	 *  @details формирует строку с html-кодом
	 */
	protected function _showMessage( $string = NULL ) {
		
		// Начальный код строки
		$str = "<hr style='color:red' />";
		
		// Если было передано уточненное сообщение, добавляем его, иначе добавим сообщение по умолчанию
		$str .= ( !is_null( $string ) ) ? $string : "При выполнении сценария произошла ошибка:";
		
		// Добавляем сообщение об ошибке из объекта PDOException
		$str .= " " . $this->_pdoObject->getMessage() . "";
		
		// Добавляем информацию о строке файла, в которой произошла ошибка, и о самом файле
		$str .= "в строке " . $this->_pdoObject->getLine() . " файла " . $this->_pdoObject->getFile() . "";
		
		// Формируем блок информации со стеком вызовов
		$str .= "
Стек вызовов."; // Для каждого из таких элементов foreach ( $this->_pdoObject->getTrace() as $array ) { // Добавляем информацию о файле и номере строки в нем $str .= "
Файл: " . $array[ 'file' ] . " строка: " . $array[ 'line' ]; // Если есть информация о названии функции (метода), класса и операторе доступа, добавляем и их if ( !empty( $array[ 'function' ] ) && !empty( $array[ 'class' ] ) && !empty( $array[ 'type' ] ) ) { $str .= ", в вызове функции (метода): " . $array[ 'class' ] . $array[ 'type' ] . $array[ 'function' ] . "()"; } } // Закрываем сообщение об ошибке горизонтальной чертой $str .= "<hr style='color:red' />"; // Выводим сообщение на экран print $str; } /** * @brief Статический метод для получения экземпляра этого класса * * @param [in] $e Объект типа PDOException * @return экземпляр этого класса * */ public static function instance( PDOException $e ) { return new self( $e ); } } ?>

Хотелось бы проверить это всё... но как, да? Достаточно попытаться подключиться к MySQL с неверным паролем для пользователя. Код файла, в котором мы это проверим, будет выглядеть следующим образом.


		
<?php
// Подключаем файл с объявлением класса DB для работы с MySQL через PDO
require_once( 'db.php' );

// Подключаем файл с объявлением класса нашего обработчика исключений типа PDOException
require_once( 'mypdoexception.php' );

// Подключаемся к СУБД
$dbh = DB::instance( array( 'password' => '123456' ) );	
?>

Но для того, чтобы этот код дал какой-то эффект, нам нужно внести изменения в наш класс DB. На текущий момент статический метод instance() не собирается принимать какие-либо аргументы от вызова. Мы не станем намеренно менять пароль пользователя на неверный в статическом свойстве-массиве $_aParams. Зачем это делать? Лучше дадим возможность изменять параметры подключения «на лету». Внесем изменения лишь в метод instance() класса DB и в его конструктор.


		
<?php
/**
 * файл db.php
 *
 * Пользовательский класс для создания подключения к СУБД MySQL,
 *  наследующий свойства и методы встроенного в PHP класса PDO
 */
class DB extends PDO {
	
	/**
	 *  Параметры подключения к СУБД
	 */
	public static $_aParams = array(
							
							'driver' => 'mysql',				// Используемый драйвер для соединения
							'host' => 'localhost',				// Адрес СУБД
							'dbname' => 'demo_pdo-mysql',		// Имя базы данных MySQL
							'charset' => 'utf8',				// Кодировка соединения
							'username' => 'demo',				// Имя пользователя MySQL
							'password' => 'sadhiJ7slv!a',		// Пароль пользователя
							'params' => array(					// Дополнительные параметры подключения
		
								PDO::ATTR_PERSISTENT => true, 	// Использовать постоянные подключения
								
							)
							
	);
	
	/**
	 *  Защищенное статическое свойство, в котором будет храниться установленное подключение к СУБД
	 */
	protected static $_dbh = NULL;
	
	/**
	 *  Защищенное статическое свойство, в котором хранится статус наличия созданного экзепляра объекта нашего класса
	 */
	protected static $_instance = FALSE;
	
	/**
		*  @brief Конструктор класса подключения к СУБД
	 *  
	 *  @param [in] $aParams массив с параметрами подключения, изначально пустой
	 *
	 *  @details Частный метод, который не даст создать экземпляр класса стандартным способом
	 *  в целях защиты от создания нескольких идентичных экземпляров нашего класса
	 */
	private function __construct( $aParams = array() ) {
		
		// Если были переданы параметры подключения, сохраним их
		if( !empty( $aParams ) && is_array( $aParams ) ) {
		
			// Подобное действие переопределит значения массива, 
			// если в массиве $aParams найдутся идентичные ключи
			self::$_aParams = $aParams += self::$_aParams;
			
		}
		
		// Нет необходимости нагружать функцию-конструктор лишними действиями
		// Установку соединения доверим защищенному методу класса 
		$this->_connect();
		
		++self::$count;
	}
	
	/**
		*  @brief Защищенный метод для установки подключения к СУБД
	 */
	protected function _connect() {
		
		// Используя имеющиеся параметры подключения подключимся к СУБД
		$dsn = self::$_aParams[ 'driver' ] . ':host=' .
				self::$_aParams[ 'host' ] . ';dbname=' . 
				self::$_aParams[ 'dbname' ] . ';charset=' . 
				self::$_aParams[ 'charset' ];
		
		/**
		 *  Устанавливаем подключение с СУБД
		 *
		 * Строку с параметрами сервера мы сформировали выше
		 * Теперь добавляем еще имя пользователя, пароль пользователя 
		 * и дополнителные параметры подключения
		 *
		 * Используется именно такая конструкция: parent::__construct(),
		 * которая вызывает конструктор родительского класса PDO
		 */ 
		// Пытаемся установить соединение с СУБД
		try {
		
			parent::__construct( $dsn, self::$_aParams[ 'username' ], self::$_aParams[ 'password' ], self::$_aParams[ 'params' ]  );
		
		}
		// Если соединение установить не удастся, будет выброшено исключение типа PDOException,
		// которое мы перехватим
		catch ( PDOException $e ) {
			
			// Передадим на обработку исключение нашему классу
			MyPDOException::instance( $e );
			
			// Так как подключиться к СУБД не удалось, в дальнейшем выполнении кода
			// сценария нет никакого смысла
			exit();
			
		}
		
		// Дальнейший код будет исполнен в случае успешной установки
		// соединения с СУБД
		
		// Ниже используем именно псевдопеременную $this, так как 
		// наш класс унаследовал все допустимые свойства и методы от класса 
		// PDO, и это псевдопеременная ссылается именно на свойства и методы 
		// класса PDO, если они не определены в нашем классе
		
		// Устанавливаем режим обработки ошибок,
		// используя для этого метод setAttribute() объекта PDO
		$this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
		
		// Устанавливаем режим выборки по умолчанию для объекта запроса,
		// используя для этого метод setAttribute() объекта PDO
		$this->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC );
		
		// После того, как будут выполнены все действия 
		// установить значение TRUE для свойства, в котором хранится информация
		// о существовании созданного объекта нашего класса
		self::$_instance = TRUE;
		
		// Сохраняем в статическое свойство экземпляр объекта, подключенный к СУБД
		self::$_dbh = $this;
		
	}
	
	/**
		*  @brief статический метод получения экзепляра объекта класса подключения к СУБД
	 *  
	 *  @return свойство, в котором хранится подключение к СУБД
	 *  
	 */
	public static function instance() {
		
		// Если ранее был создан экземпляр объекта этого класса
		if ( self::$_instance !== FALSE ) {
			
			// Вернуть этот экземпляр объекта
			return self::$_dbh;
			
		}
		
		// Если методу были переданы какие-либо аргументы
		if ( func_num_args() > 0 ) {
			
			// Сохраним переданный аргумент в переменную
			// Нас интересует лишь первый аргумент, если он был передан
			$aParams = func_get_args()[ 0 ];
			
			// Параметры должны передаваться в виде массива и никак иначе
			if( is_array( $aParams ) ) {
				
				// Передаем параметры конструктору класса, создаем экземпляр класса
				return new self( $aParams );
				
			}
			
		}
		
		// Создаем экземпляр класса
		return new self();
		
		// Всё остальное будет проигнорировано
		
	}
	
	/**
	 *  Статическое защищенное свойство, которое будет увеличиваться на 1
	 *  при каждом создании экземпляра объекта нашего класса. Служит только
	 *  для того, чтобы вы могли проверить, действительно ли нельзя создать
	 *  несколько экземпляров объектов нашего класса
	 */
	protected static $count = 0;
	
	/**
		*  @brief Получение значений счетчика экземпляров объекта класса
	 *  
	 *  @return число созданных экземпляров
	 */
	public static function getCount() {
		
		return self::$count;
		
	}
	
}
?>

Обработка полей SELECT

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

Начиналось всё с того, чтобы мою же статью о динамических списках, информация в которые подгружается из базы данных MySQL, переделать на алгорит работы PHP — PDO — MySQL.

Вы скажете: «Так много действий, кода... и всё ради переделки?». Да, так много действий и кода. Это объектно-ориентированный подход к делу. Зато у нас теперь о многом голова болеть не будет. И вы потом поймете, что имеется в виду.

Файлов, в которые нам потребуется внести изменения, немного.

  • index.php
  • functions.php
  • requests.php

Основное число изменений — подключение файлов с кодом наших двух классов: DB и MyPDOException. Ну и немного изменяется алгорит обработки результатов запросов к MySQL. Внимательно следите за комментариями внутри файлов. Файлы scripts.js и style.css вообще не претерпели изменений, как и таблицы с данными в MySQL.

Файл index.php


		
<?php
/**
 *  файл index.php
 *  Страница с html-формой, в которой находятся два поля Select.
 */

/** Обращайте внимание на комментарии, которые находятся в подобных блоках, 
 *  а не в блоках с двумя слэшами в начале строке. В подобных блоках мы пишем 
 *  о вносимых в код изменениях
 */
 
/***** Это фрагмент кода, который на сегодняшний момент устарел *********

// Подключаем файл для соединения с СУБД MySQL
require_once( 'database.php' );

/***** Это фрагмент кода, который на сегодняшний момент устарел *********/

// Подключаем файл, в котором будем объявлять пользовательские функции
/* Подключение к СУБД будет происходить непосредственно в функциях */
require_once( 'functions.php' );
?>
<!-- Пишем в рамках стандарта HTML5 -->
<!DOCTYPE html>
<html>
<head>
	<title>Выбор марки и модели автомобиля</title>
	<!-- Подключаем библиотеку jQuery -->
	<script src="//libs.raltek.ru/libs/jquery/1.8.3/js/jquery-1.8.3.js"></script>
	<!-- Подключаем таблицу стилей -->
	<link href="style.css" rel="stylesheet" type="text/css" />
	<!-- Подключаем JavaScript-файл с нашим сценарием, который и будет получать данные об автомобилях -->
	<script src="scripts.js"></script>
</head>
<body>
	<!-- Создаем контейнер-обертку для нашей формы -->
	<div id="car_producers_wrapper">
		<!-- Сама форма -->
		<form name="car_producers" id="car_producers" >
			<!-- Контейнер для поля выбора производителя -->
			<div class="row">
				<!-- Метка поля производителей автомобилей -->
				<label for="producer">Производитель автомобилей:</label>
				<!-- Раскрывающийся список производителей автомобилей -->
				<select id="producer">
					<option value="0">Выберите из списка</option>
					<?php					
					
					
					// Получаем перечень производителей в виде массива
					$aProducers = getProducers();
					
					// Для каждого элемента массива производителей автомобилей...
					foreach ( $aProducers as $aProducer ) {
						// Создаем свой элемент раскрывающегося списка
						print '<option value="' . $aProducer[ 'id' ] . '">' . $aProducer[ 'producer' ] . '</option>';
						
					}
					?>
				</select>
			</div>
			<!-- Контейнер для поля выбора модели автомобиля выбранного производителя -->
			<div class="row">
				<!-- Метка поля выбора марки автомобиля -->
				<label for="model">Марка автомобиля:</label>
				<!-- Раскрывающийся список выбора марки автомобиля выбранного производителя -->
				<!-- Изначально список пуст и неактивен -->
				<!-- Данные в нем появятся полсле выбора производителя -->
				<select id="model" disabled >
					<option value="0">Выберите из списка</option>
				</select>
			</div>
			
		</form>
	</div>
</body>
</html>
    

Файл functions.php


		
<?php
/**
 *  файл functions.php
 *  Здесь размещены функции, получающие данные из MySQL для полей формы
 */

/**
 *  Подключаем файл с нашими объявлениями классов DB и MyPDOException
 */
require_once( 'db.php' );
require_once( 'mypdoexception.php' );
/**
 *  Функция для получения перечня производителей автомобилей
 */
function getProducers() {
	
	/** Более этот код не актуален
	 *  Мы будем подключаться к СУБД MySQL через класс DB
	 */
	 
	// Подключаемся к СУБД MySQL
	// connect();
	$dbh = DB::instance();
	
	// Выбираем всех производителей из таблицы
	$sql = "SELECT * FROM `producers` ORDER BY `producer`";
	
	/**
	 * Этот код более не актуален
	 */
	// Выполняем запрос
	// $query = mysql_query( $sql ) or die ( mysql_error() );
	
	// Поместим данные, которые будет возвращать функция, в массив
	// Пока что он будет пустым
	$array = array();
	
	// Инициализируем счетчик
	$i = 0;
	
	/** Это устаревшая часть кода **/
	/*while ( $row = mysql_fetch_assoc( $query ) ) {
		
		$array[ $i ][ 'id' ] = $row[ 'id' ];				// Идентификатор производителя
		$array[ $i ][ 'producer' ] = $row[ 'producer' ];	// Имя производителя
		
		// После каждой итерации цикла увеличиваем счетчик
		$i++;
		
	}*/
	
	try { 
	
		foreach( $dbh->query( $sql ) as $row ) {
			
			$array[ $i ][ 'id' ] = $row[ 'id' ];				// Идентификатор производителя
			$array[ $i ][ 'producer' ] = $row[ 'producer' ];	// Имя производителя
			
			// После каждой итерации цикла увеличиваем счетчик
			$i++;
			
		}
	
	}
	catch ( PDOException $e ) {
		
		MyPDOException::instance( $e );
		
	}
	
	// Возвращаем вызову функции массив с данными
	return $array;
	
}

// Функция, которая выбирает модели автомобилей по переданному
// ей идентификатору производителя
function getModels( array $array ) {
	
	// Сохраняем идентификатор производителя из переданного массива
	$sProducerId = htmlspecialchars( trim ( $array[ 'producer_id' ] ) );
	
	/** Это устаревшая часть кода **/
	// Подключаемся к MySQL
	// connect();
	
	$dbh = DB::instance();
	
	// Строка запроса из базы данных
	$sql = "SELECT `id`, `model` FROM `models` WHERE `producer_id` = '" . $sProducerId . "' ORDER BY `model`";
	
	/** Это устаревшая часть кода **/
	// Выполняем запрос
	// $query = mysql_query( $sql ) or die ( mysql_error() );
	
	// Поместим данные, которые будет возвращать функция, в массив
	// Пока что он будет пустым
	$array = array();
	
	// Инициализируем счетчик
	$i = 0;
	
	/** Это устаревшая часть кода **/
	/*while ( $row = mysql_fetch_assoc( $query ) ) {
		
		$array[ $i ][ 'id' ] = $row[ 'id' ];		// Идентификатор модели
		$array[ $i ][ 'model' ] = $row[ 'model' ];	// Наименование модели
		
		// После каждой итерации цикла увеличиваем счетчик
		$i++;
		
	}*/
	
	foreach( $dbh->query( $sql ) as $row ) {
		
		$array[ $i ][ 'id' ] = $row[ 'id' ];		// Идентификатор модели
		$array[ $i ][ 'model' ] = $row[ 'model' ];	// Наименование модели
		
		// После каждой итерации цикла увеличиваем счетчик
		$i++;
		
	}
	
	// Возвращаем вызову функции массив с данными
	return $array;
	
}
?>
    

Файл requests.php


		
<?php
/**
 *  файл requests.php
 *  Здесь обрабатываются AJAX-запросы
 */

/** Это устаревшая часть кода **/
// Подключаем файл для соединения с СУБД MySQL
// require_once( 'database.php' );

/**
 *  Подключаем файл с нашими объявлениями классов DB и MyPDOException
 */
require_once( 'db.php' );
require_once( 'mypdoexception.php' );

// Подключаем файл, в котором будем объявлять пользовательские функции
require_once( 'functions.php' );

/**
 *  Данные передаются методом POST
 *  Если массив POST, пустой, что-то идет не так
 *  Более того, переменная $_POST[ 'request' ] пустая, или её не существует,
 *  значит тоже что-то не так
 */
if ( empty( $_POST ) ) {
	
	die( "Массив \$_POST пустой" );
	
}
elseif ( empty( $_POST[ 'request' ] ) ) {
	
	die( "Не передан запрос" );
	
}
else {
	
	// Очищаем строку с типом запроса от лишних пробелов и защищаемся от возможных SQL-инъекций
	$request = htmlspecialchars( trim( $_POST[ 'request' ] ) );
	
	// Убираем тип запроса из массива $_POST
	unset( $_POST[ 'request' ] );
	
}

// В переменной $response будем возвращать данные AJAX-запросу
$response = NULL;

switch ( $request ) {
	case "getModels":
		
		$response = getModels( $_POST );
		
	break;
}

echo json_encode( $response );
?>
    

Файлы scripts.js и style.css не претерпели вообще никаких изменений.


		
/**
 *  файл scripts.js
 */
(function($) {

	// Включаем строгий режим ECMA-Script
	"use strict";
	
	/**
	 * В скрипте мы будем выполнять AJAX-запросы к СУБД MySQL
	 * Чтобы каждый раз не писать один и тот же код AJAX-запроса, создадим 
	 * свой метод request в объекте jQuery
	 */
	$.extend({
		request: function( options ) {
			
			// В методе request будут различные опции (настройки)
			// Это своего рода настройки по умолчанию, созданные
			// в объекте options
			options = $.extend({
				
				type: "POST",					// Метод передачи данных серверу
				url: "requests.php",			// Путь к файлу со сценарием обращения к СУБД
				data: null,						// Данные, которые мы будем передавать серверу
				async: false,					// Асинхронность выполнения AJAX-запроса
				dataType: "json",				// Тип данных, в котором они передаются
				before: null,					// Код, выполняемый перед AJAX-запросом
				error: function() {},			// Код, выполняемый в случае какой-либо ошибки при AJAX-запросе
				complete: options.callback,		// Код, выполняемый после AJAX-запроса	
				success: function( result ) {	// Код, выполняемый после получения ответа от сервера
					$.response.result = result;	// Помещаем ответ от сервера в отдельный объект
				},
				result: null,					// Результат работы
				callback: null					// Функция обратного вызова
				
			}, options );
			
			options.before = function() {
				alert( "ok before" );
			};
			
			// Тело AJAX-запроса
			$.ajax({
				
				type: options.type,
				url: options.url,
				data: options.data,
				async: options.async,
				dataType: options.dataType,
				before: options.before,
				error: options.error,
				complete: options.complete,
				success: options.success
				
			});
			
			return this;
			
		},
		// Объект, в котором хранится ответ от сервера, полученный через AJAX-запрос
		response: {
			result: {}
		}
	});
	
	jQuery(function() {
		
		/**
		 *  При выборе производителя нужно сделать многое
		 *  Сначала из списка моделей должны быть удалены все имеющиеся модели автомобилей
		 *  Затем поле выбора модели автомобиля должно стать неактивным
		 */
		 
		// Обработчик события выбора производителя
		$( '#producer' ).change(function() {
			
			var producer_id = $( this ).val();	// Идентификатор выбранного производителя
			
			
			
			// Отключаем поле, установив значения свойства disabled
			$( '#model' ).prop( 'disabled', true )
			
			// Находим и удаляем все возможные модели автомобилей из раскрывающегося списка
			.find( 'option:not( :first )' ).remove();
			
			
			
			// Если был выбран конкретный производитель
			if ( producer_id != 0 ) {
				
				// Создаем AJAX-запрос, который вернет нам перечень моделей для выбранной марки 
				$.request({
					
					data: "request=getModels&producer_id=" + producer_id,
					
				});
				// Успешный AJAX-запрос должен закончиться вставкой полученного перечня моделей 
				// в раскрывающийся список select#model
				// Результат AJAX-запроса мы сохраняли в отдельном объекте
				var i = 0, models = $.response.result;
				for ( i; i < models.length; i++ ) {
					
					$( '#model' ).append( '' + models[ i ].model + '' );
					
				}
				
				// Включаем поле со списком моделей
				$( '#model' ).prop( 'disabled', false );
				
			}
			
		}); // Обработчик события выбора производителя
	
	});
	
})(jQuery); // Используем немедленно вызываемую анонимную функцию
    

		
/********* Файл style.css ****************/
/**
 * Просто внешний вид 
 */
body {
	font-size:12pt;
	color:#333333;
	font-family: Tahoma, Verdana, Arial, Sans-Serif;
}
.row {
	margin-bottom:10px;
}
.row label {
	margin-bottom:5px;
	font-weight:bold;
	width:280px;
	display:block;
}
.row select {
	width:300px;
	font-size:1.1em;
	border:1px solid #aaaaaa;
	padding:3px 5px;
}
#car_producers_wrapper {
	width: 310px;
	margin: 100px auto 0 auto;
}
    

Работа над данным примером окончена. Напоминаю, что мы внесли изменения в алгоритм работы динамических списков (динамический Select-ов), данные в которые подгружались из MySQL, описанный в прежней статье. В связи с тем, что работа с функциями вида mysql_ исключена из PHP, тем, для кого тот пример реализации был полезен, приходилось самостоятельно решать проблему его неработоспособности, или уже использовать устаревшую версию PHP.

Теперь же код пригоден для использования в версиях PHP выше 5.5. Пользуйтесь!

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

Описанные выше файлы доступны для скачивания в виде архива.

Средний рейтинг статьи:

Комментарии


Эко-туризм

bkLen 0 0 0 22.04.2024 19:37:10 #

Сайт принадлежит ООО Группа Ралтэк. 2014 — 2024 гг