ORM на php для MySQL, реальность (часть первая)

в 7:52, , рубрики: Doctrine ORM, mysql, orm, php

После долгих поисков интересующей меня библиотеки на php для связи с MySQL сел и написал свою, наиболее подходящую для использования в проектах. Данная тема займет небольшой цикл статей, который будет полезен не только профессиональным разработчикам веб-приложений, но и начинающим. Следует отметить, что представленная ниже ORM библиотека, которую, кстати, я назвал kitty, является результатом долгих мучений и не является обязательной библиотекой всех проектов.

Библиотека по моему видению должна иметь два файла (по крайней мере на начальных этапах):

  • файл библиотеки — kitty.php;
  • файл объектного изображения модели базы данных — modeldb.php.


Начнем с последнего. Файл изображения базы данных должен в себе содержать классы, по названию схожие с названием таблиц и содержать в себе поля в соответствии со столбцами таблиц. Т.е. если у нас есть таблица authors с полями idauthor,Name,Year (Идентификатор, ФИО, Годы жизни), то класс будет выглядеть следующим образом:

class authors extends kitty {
    public $idauthor;
    public $Name;
    public $Year;
}

Идентификатор должен следовать первым.

Обычно, класс изображения базы данных содержит в себе все таблицы, которые kitty генерирует автоматически, но об этом позже (все в данной статье не уместишь), и подключается после kitty. А теперь приступим к самому интересному, к нашей библиотеке.

Свойства класса kitty

Класс kitty является абстрактным классом и имеет в своем составе (по моему видению) два ключевых свойства:

	private static $db;	//Объект базы данных
	private static $stack;	//Стэк запросов

Экземпляр класса $db хранит в себе подключение к базе данных, используя улучшенных класс mysqli.
Экземпляр класса $stack хранит в себе стек запросов и результаты этих запросов, используя класс SplStack.
На этом свойства закончились, все лаконично и просто, теперь перейдем к сладкому.

Методы класса kitty

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

Статические методы

Ключевым статическим методом для соединения с базой данных является setup:

	static public function setup(mysqli $dbi,$enc = "utf8"){
		if (is_object($dbi)){
			self::$db = $dbi;
			self::$stack = new SplStack();	        //Стэк запросов
			return self::setEncoding($enc);	//Запрос на кодировку
		}else{
			throw new Exception("Параметр $dbi не является объектом mysqli", 1);
			return false;
		}
	}

В качестве параметра мы передаем экземпляр класса mysqli и кодировку, которая по умолчанию является utf8. При инициализации заносится экземпляр MySQLi и стек. Результатом ответа является запрос, т.е. проверка на корректность соединения. Строчка kitty::setup(new mysqli) является единственной настройкой библиотеки.
Кодировка устанавливается запросом setEncoding. Код метода представлен ниже:

	static function setEncoding($enc){
		$result = self::$db->query("SET NAMES '$enc'");		        //Отправляем запрос
		self::$stack->push("SET NAMES '$enc' [".($result ? "TRUE" : self::getError())."]");
		return $result ? true : false;							//Возвращаем ответ
	}

В случае возникновения ошибки, заносим в стек запрос и ошибку, и соответственно возвращаем false.
Функция получения ошибки очень лаконичная:

	static function getError(){
		return self::$db->error." [".self::$db->errno."]";
	}

Возвращаем текст ошибки (error) и код ошибки (errno).

Каждая, уважающая себя, ORM библиотека должна содержать экранирование (к.т.н., доц. Ковженкин В.С.)

Эту возможность реализует функция mysqli_real_escape_string, но она является длинной и принимает два параметра. Заменим, для удобства, эту функцию на представленную ниже:

	private static function escape($string) {
		return mysqli_real_escape_string(self::$db,$string);
	}

Функция принимает строку и возвращает экранированную для SQL-запроса. С помощью нее мы забываем о SQL-инъекциях, что является немаловажным фактом!

Чтобы выбрать поля таблицы, а конкретней свойства класса таблицы, воcпользуемся средствами php для работы с классами.
Код функции представлен ниже:

	private static function _getVars(){
		return array_filter(get_class_vars(get_called_class()),function($elem){
			if (!is_object($elem)) return true;
		});
	}

Функция забирает все свойства и фильтрует их. Если свойство является объектом, а она выбирает еще stack и db, то оно не входит. На выходе массив с полями таблицы. При вызове authors::_getVars(); функция вернет массив array(«idauthor»,«Name»,«Year»).

Выборка данных

Выборка данных является щекотливой темой для ORM библиотек и возникает вопрос, как данные доставать и как их представлять.
В текущей статье мы рассмотрим только один вариант запроса.

Метод является статическим и выбирает из базы данных один экземпляр по идентификатору (findID).
Код функции представлен ниже:

	static function findID($id){
		if (is_numeric($id)){												//Если число, то ищем по идентификатору
			$query = "SELECT * FROM `".get_called_class()."` WHERE `".key(self::_getVars())."` = $id LIMIT 1";
			$result = self::$db->query($query);								//Отправляем запрос
			self::$stack->push($query." [".$result->num_rows."]");			//Заносим запрос в стек
			if ($result->num_rows == 1){									//Если запрос вернул строку
				$row = $result->fetch_object();								//Строку запроса в класс
				$cName = get_called_class();								//Получем название класса
				$rClass = new $cName();										//Создаем экземпляр класса
				foreach ($row as $key => $value) $rClass->$key = $value;	//Переносим свойства класса
				return $rClass;												//Возвращаем класс
			} else return false;											//Если строка не найдена, то ложь
		} else return false;												//Если не число возвращаем ложь
	}

Код подробно описан комментариями и не требует дополнительного описания.
Получить экземпляр можно следующим образом:

    $auth = authors::findID(2);
    if ($auth){
        //Действия
    }else{
        //Если не найден
    }

Не статические методы

Хватит статических методов, перейдем к не статическим. Методы, которые относятся к конкретному экземпляру.
Выше мы выбрали экземпляр автора с идентификатором 2. Если запрос успешно выполнится, то у нас окажется экземпляр класса:

    $auth->idauthor = 2;
    $auth->Name = "Тургенев Иван Сергеевич";
    $auth->Year = "1818—1883";

Изменять параметры очень просто, а как же сохранять?
Сохранять так же просто. Ниже представлен код функции для сохранения:

	public function Save(){									//Сохраняем объект - UPDATE	
		$id = key(self::_getVars());						//Получаем идентификатор
		if (!isset($this->$id) || empty($this->$id)) return $this->Add();	//Если пусто, добавляем
		$query = "UPDATE `".get_called_class()."` SET ";	//Формируем запрос
		$columns = self::_getVars();						//Получем колонки таблицы
		$Update = array();									//Массив обновления
		foreach ($columns as $k => $v) {					//перебираем все колонки
			if ($id != $k)    //Убираем идентификатор из запроса
				$Update[] = "`".$k."` = ".self::RenderField($this->$k);	//Оборачиваем в оболочки
		}
		$query .= join(", ",$Update);						//Дополняем запрос данными
		$query .= " WHERE `$id` = ".self::escape($this->$id)." LIMIT 1";	//Дополняем запрос уточнениями
		$result = self::$db->query($query);					
		self::$stack->push($query." [".($result ? "TRUE" : self::getError())."]");	//Стек результатов
		return ($result) ? true : false;					//Возвращаем ответ
	}

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

        $auth->Name = "Толстой Лев Николаевич";
        echo $auth->Save() ? "Успешно" : "Запрос не удался((";

Функция Save имеет в себе замечательную функцию RenderField. Функция очень важная, является статической и отвечает за правильность построения запроса, ее код представлен ниже:

	private static function RenderField($field){
		$r = "";															//Строка для возвращения
		switch (gettype($field)) {											//Селектор типа передаваемого поля
			case "integer":	case "float":									//Тип int или float
				$r = $field;								
			break;
			case "NULL": 	$r = "NULL";  break;							//Тип NULL
			case "boolean": $r = ($field) ? "true" : "false"; break;		//Тип boolean
			case "string":													//если тип строковой
				$p_function = "/^[a-zA-Z_]+((.)*)/";						//Шаблон на функцию
				preg_match($p_function, $field,$mathes);					//Поиск соврадений на функцию
				if (isset($mathes[0])){										//Совпадения есть, это функция
					$p_value = "/((.+))/";								//Шаблон для выборки значения функции
					preg_match($p_value, $field,$mValue);					//Выборка значений
					if (isset($mValue[0]) && !empty($mValue[0])){			//Если данные между скобок существуют и не пустые
						$pv = trim($mValue[0],"()");						//Убираем скобки по концам
						$pv = "'".self::escape($pv)."'";					//Экранируем то что в скобках
						$r = preg_replace($p_value, "($pv)" , $field);		//Меняем под функцию
					}
					else $r = $field;										//Возвращаем функцию без параметров
				}
				else $r = "'".self::escape($field)."'";						//Если просто строка экранируем
			break;
			default: $r = "'".self::escape($field)."'";	break;				//По умолчанию экранируем
		}
		return $r;															//Возвращаем результат
	}

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

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

       $auth = new authors();
       $auth->Name = "Тургеньев Иван Сергеевич";
       $auth->Year = "1918-1983";
       $auth->Add();

Код функции добавления представлен ниже:

	public function Add(){									//Добавляем объект - INSERT
		$query = "INSERT INTO `".get_called_class()."` (";	//Подготавливаем запрос
		$columns = self::_getVars();						//Получем колонки
		$q_column = array();								//Массив полей для вставки
		$q_data = array();									//Массив данных для вставки
		foreach ($columns as $k => $v){						//Пробегаемся по столбцам
			$q_column[] = "`".$k."`";						//Обертываем в кавычки
			$q_data[] 	= self::RenderField($this->$k);		//Рендерим обертку для данных
		}
		$query .= join(", ",$q_column).") VALUES (";		//Дополняем запрос столбцами
		$query .= join(", ",$q_data).")";					//Дополняем запрос данными
		$result = self::$db->query($query);					//Делаем запрос
		$insert_id = self::$db->insert_id;					//Получаем идентификатор вставки
		self::$stack->push($query." [".($result ? $insert_id : self::getError())."]");	//Стек результатов
		return ($result) ? $insert_id : false;				//Возвращаем ответ
	}

Удаление объекта

Ну и напоследок удаление. В php нет функции delete и мы не будем нарушать традиции, поэтому назовем метод Remove();
Чтобы удалить запись автора из предыдущих примеров, необходимо выполнить код:

        $auth = authors::findID(2);
        $auth->Remove();

Выбираем экземпляр и удаляем. Все очень просто и лаконично! Код функции для удаления представлен ниже:

	public function Remove(){								//Удаляем объект - DELETE
		$id = key(self::_getVars());						//Выбираем идентификатор
		if (!empty($this->$id)){							//Если идентификатор не пустой
			$qDel = "DELETE FROM `".get_called_class()."` WHERE `$id` = ".$this->$id." LIMIT 1";
			$rDel = self::$db->query($qDel);				//Запрос на удаление
			self::$stack->push($qDel." [".($rDel ? "TRUE" : self::getError())."]");	//Стек результатов
			return $rDel ? true:false;						//Возвращаем ответ
		} else return false;								//Отрицательный ответ
	}

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

Автор: viktor_zloy

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js