После долгих поисков интересующей меня библиотеки на 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 библиотека не является монстром современного мира, тем более писать я ее только начал, но вполне подходит для использования в небольших проектах. В следующей будет рассмотрено автоматическое генерирование модели базы данных.
Поделиться с друзьями
-->

Комментарии (55)


  1. Akuma
    17.07.2016 11:12
    +11

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

    Но дам пару советов:

    1. Не надо заставлять людей пихать все классы в один файл — жутко неудобно
    2. Не используйте get_called_class() для SQL запросов. Лучше получайте его заранее и сохраняйте, если уж так хочется его использовать.
    3. Почему у вас метод Add() возвращет ID вставленной записи (так и должно быть), а метод Remove() просто true/false? Надо бы возвращать кол-во удаленных строк, как и делает MySQL

    Вообще для подобных нужд можно использовать вот это: http://www.phpactiverecord.org/
    Довольно неплохая библитечка, проверенная временем, так сказать. Правда не обновлялась давно, но там особо и нечего обновлять наверное.


    1. staticlab
      17.07.2016 18:30

      Да я бы не сказал, что не обновляется: https://github.com/jpfuentes2/php-activerecord/commits/master


      1. Akuma
        17.07.2016 20:59

        Я просто посмотрел на дату релиза на сайте и 2010/2013 год :)

        Хотя, сам им пользуюсь для простенького проекта — очень даже круто работает. Шустро, память не жрет как та же доктрина.


    1. DioGen4ik
      18.07.2016 13:38

      Возможно remove не возвращает кол-во удалённых записей, потому что в методе remove строго прописано LIMIT 1.


      1. Akuma
        18.07.2016 13:42

        А что это меняет?
        Возможно вызов метода ничего не удалит, тогда должен вернуться ноль, даже если там LIMIT 1


    1. VolCh
      18.07.2016 14:37
      -2

      Надо бы возвращать кол-во удаленных строк

      Кому надо?


      1. webmasterx
        18.07.2016 14:59

        1) юнит тесту 2) UI


        1. VolCh
          18.07.2016 16:24
          -1

          Не очень хорошо представляю зачем. Команда на удаление одной записи, возвращает статус. В чём разница 0/1 или false/true?


          1. webmasterx
            18.07.2016 16:42

            true — успешно завершенная операция ( 0 или 1 строка удалены). Может ли это быть где-то важно? Я думаю что может, но где — сейчас не придумаю.


            1. VolCh
              18.07.2016 17:15

              0 удаленных строк для объекта, который только что из базы получен или только что в неё записан — не штатная ситуация, по-моему.


  1. webmasterx
    17.07.2016 11:12
    +7

    Вы забыли ссылку на гитхаб.
    p.s. вам еще рано писать на хабр, попробуйте еще раз через годик-другой


  1. SergeyRembo
    17.07.2016 11:43
    +4

    Извините, но ваш код заставил меня страдать.
    Пробежимся по главному:
    1. Почитайте на досуге PSR-1 и PSR-2 http://www.php-fig.org/psr/. На русском есть тут.
    2.

    static public function setup(mysqli $dbi,$enc = "utf8"){
    		if (is_object($dbi)){
    			...
    		}else{
    			throw new Exception("Параметр $dbi не является объектом mysqli", 1);
    			return false;
    		}
    	}
    

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

    3.
    SET NAMES '$enc'
    

    $query .= " WHERE `$id` = ".self::escape($this->$id)." LIMIT 1";
    

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

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

    4.
    $Update[] = "`".$k."` = ".self::RenderField($this->$k);
    

    Тут лучше подойдёт sprintf

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


    1. mougrim
      17.07.2016 18:05
      +1

      Во-первых: mysqli морально устарел и мне кажется, что лучше использовать PDO.

      См таблицу «Сравнение опций MySQL API в PHP» на http://php.net/manual/ru/mysqli.overview.php: Рекомендовано MySQL для разработки новых проектов: Расширение PHP mysqli: Да — отдается предпочтение.
      Mysqli не устарел морально и нужно смотреть, что для своей задачи использовать лучше.


      1. SergeyRembo
        17.07.2016 18:22
        -1

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

        И по поводу сравнение опций:
        API поддерживает подготавливаемые запросы на стороне клиента: Нет(mysqli) Да(PDO)

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


        1. M-A-XG
          17.07.2016 18:42
          -3

          Подготавливаемые запросы на стороне клиента — это псевдоподготавливаемые запросы, это все равно, что использовать mysqli_real_escape_string().


          1. SergeyRembo
            17.07.2016 19:00
            +1

            Сударь, вы не совсем правы. Вот прув.

            Подготавливает SQL запрос к базе данных к запуску посредством метода PDOStatement::execute(). Запрос может содержать именованные (:name) или неименованные (?) псевдопеременные, которые будут заменены реальными значениями во время запуска запроса на выполнение.


            Вызов PDO::prepare() и PDOStatement::execute() для запросов, которые будут запускаться многократно с различными параметрами, повышает производительность приложения, так как позволяет драйверу кэшировать на клиенте и/или сервере план выполнения запроса и метаданные, а также помогает избежать SQL инъекций, так как нет необходимости экранировать передаваемые параметры.


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


            1. zenn
              17.07.2016 19:47

              Добавлю на всякий случай — если для автора уж слишком «проблемно» изучение PDO, то подготавливаемые запросы есть и в mysqli для ооп и процедурного стиля: пруф. При использовании такого подхода разница между mysqli/pdo по синтаксису будет минимальна, а в дальнейшем поможет плавно перейти к PDO.


            1. M-A-XG
              17.07.2016 22:06

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

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

              Также я не особо понимаю смысла однотипных запросов, если можно выполнить один запрос с WHERE `column` IN (val1, ..., valN).

              По Вашей ссылке также:
              >Замечание:

              >Эмулируемые подготовленные запросы не создаются на сервере баз данных, поэтому PDO::prepare() не может проверить правильность построенного запроса.


              1. SergeyRembo
                17.07.2016 22:56

                Так, давайте разберёмся с PDO::prepare(), по моему вы что-то не поняли из документации или из моих объяснений. Я утверждаю что prepare компилит запрос, проверяя правильность синтаксиса и ждёт когда выполнится execute с параметрами. Соответственно для двух запросов, у которых разные только параметры нет смысла компилить запрос два раза, т.к sql код одинаковый (параметры не в счёт).
                Да, логично что prepare не хранит компилированные запрос где-то у себя, он возвращает объект, который нужно сохранить в статическое свойство класса, а при повторном вызове похожего запроса просто взять уже скомпилированный запрос а не вызывать заново prepare, сделать это можно в пару строчек:

                protected static $_cache = array();
                ....
                if (!isset(self::$_cache[md5($query)])) {
                    self::$_cache[md5($query)] = $this->_handler->prepare($query);
                }
                return self::$_cache[md5($query)]->execute($params);
                

                Накатал на коленке, надеюсь суть будет понятна.

                Теперь разберёмся с замечаниями, которые вы нашли по моей ссылке.
                Либо я не правильно понимаю что это значит, либо вы, но я утверждаю что prepare именно разбирает запрос и проверяет в нём ВСЁ, за исключением значения параметров. Т.е если в запросе ошибка синтаксиса, или отсутствует поле, то это выявится именно на этапе prepare, и именно на это уходит время, и именно по этому при повторном запросе проще брать его из кеша. Я так понял что вы упёртый и мне не поверите, по этому я запустил запрос и протестировал, и сейчас вам докажу (вы что-то на пруфы не расщедрились):

                Берём запрос:
                select2 a.subject  from `...` a
                

                Получаем Exception:
                SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'select2 a.subject ....' at line 1

                и стеком:
                #0 path/to/file: PDO->prepare('select2 a.subje...', Array)


                Ещё один:
                select a.subject2 from `...` a
                

                Получаем Exception:
                SQLSTATE[42S22]: Column not found: 1054 Unknown column 'a.subject2' in 'field list'

                и стеком:
                #0 path/to/file: PDO->prepare('select a.subjec...', Array)


                Надеюсь я вас убедил, если не верите, то запустите сами.

                Теперь по поводу того, зачем выполнять один запрос несколько раз, если можно сделать column in. Объясняю:
                Допустим у нас в ORM есть метод byId у которого есть запрос вида:

                select * from `table` where `id`=:id
                

                Далее в коде мы где-то хотим получить объём с id=1 и мы делаем Entity::byId(1). Далее мы с этим объектом работаем, и потом хотим получить объект с id=2 для каких-то других целей через 100500 строчек кода, и делаем Entity::byId(2). Так вот по-скольку эти строчки не идут друг за другом, и я не могу знать какие объекты мне будут нужны позднее, я использую параметаризированный запрос, который будет иметь один и тот же скомпилированный объект, а на этапе вызова заменять параметр :id на id нужного объекта, тем самым избегая затрат на парсинг и анализ этого запроса.

                Надеюсь понятно изложил.


                1. M-A-XG
                  17.07.2016 23:33
                  -1

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

                  Так ссылка-то Ваша… :)

                  Кстати, там же:
                  >Например, вы не можете привязать несколько значений к одному параметру для SQL выражения IN().

                  То есть для каждого value в IN(value1, ..., valueN) или заводить свой именованный параметр, или не использовать подготовленные выражения :)
                  Если заводить свой именованный параметр, то повторное использование заканает, если будет одинаковое число параметров.

                  >Теперь по поводу того, зачем выполнять один запрос несколько раз, если можно сделать column in.

                  В разных местах обычно нужно выбрать разные данные, * вибирать не совсем правильно. :)

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

                  http://stackoverflow.com/questions/15718148/php-prepared-statements-turn-emulation-off (первый ответ)
                  https://forum.phalconphp.com/discussion/9183/are-rawsql-prepared-statements-emulated-or-for-real


                  1. SergeyRembo
                    17.07.2016 23:47

                    Перевернули всё с ног на голову.

                    Так ссылка-то Ваша… :)


                    Ссылка моя, но я же не её автор. Вы скопировали кусок, который говорит что запросы «PDO::prepare() не может проверить правильность построенного запроса», я вам доказал что может, а вы снова доказываете что не может)

                    То есть для каждого value в IN(value1, ..., valueN) или заводить свой именованный параметр, или не использовать подготовленные выражения :)


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

                    В разных местах обычно нужно выбрать разные данные, * выбирать не совсем правильно. :)


                    Во-первых, пример тестовый. Во-вторых, если бы вы внимательно прочитали, то поняли, что byId нужен только в рамках одной сущности, а одна сущность это одна таблица, а в ней для любой записи набор полей одинаковый. А когда сущность другая там и запрос будет уже select * from table2 а не select * from table1 и кеш будет уже для каждой таблицы. Т.е для каждой таблицы byId будет хранить кеш запроса, т.е одна компиляция на одну таблицу, что существенно меньше, чем одна компиляция на один запрос к одном таблице. Посчитайте на калькуляторе


                  1. SergeyRembo
                    18.07.2016 00:17

                    Похоже я всё-таки допёр что вы пытаетесь мне объяснить)

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

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

                    Тут я ещё хотел написать, что ошибался, что execute делает заново разбор запроса и не учитывает результат prepare, но потом прочитал
                    http://php.net/manual/ru/pdo.prepared-statements.php

                    и понял, что по сути возможность компилить запросы и хранить их в БД зависит от самой БД.
                    В mySQL это http://dev.mysql.com/doc/refman/5.7/en/sql-syntax-prepared-statements.html

                    И почитайте как раз ссылку с php.net, там доступно рассказано то, что я уже так долго пытаюсь вам объяснить)


                    1. saksmt
                      19.07.2016 00:55

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

                      Я конечно не спец по БД, но для любого подготовленного запроса можно построить план запроса, просто в PHP это не имеет смысла, ибо подготовка жрёт ресурсы, а так как она будет выполняться при каждом запросе к скрипту смысл в ней отпадает, если вы конечно не демона на пыхе пишете.
                      Подготовка имеет смысл в приложениях, где она происходит один раз — при старте приложания (например в яве [если запросы не динамические])


                      1. webmasterx
                        19.07.2016 05:41

                        Подготовка имеет смысл. Например если использовать веб-сокеты. Или чтение большого массива данных (например из файла, или бд) и их вставка в бд


                        1. sumanai
                          19.07.2016 09:13

                          > Например если использовать веб-сокеты.
                          Это подпадает под
                          > демона на пыхе пишете


        1. mougrim
          17.07.2016 18:45
          +1

          Я бы не хотел разводить холивар о том, что лучше. У обоих библиотек есть свои плюсы и минусы. Я лишь подчеркнул, что mysqli не является устаревшем.


    1. MrSunny
      18.07.2016 10:29

      PDO не умеет асинхронность, тогда как mysqli — свободно.
      php.net/manual/ru/mysqlinfo.api.choosing.php


  1. MetaDone
    17.07.2016 11:47

    Вы назвали свое творение ORM, но ничего про отношения между объектами нет. Значит это просто сборщик запросов. Как практика — норм, вот пример кошерного сборщика запросов — https://github.com/auraphp/Aura.SqlQuery


    1. VolCh
      17.07.2016 12:26
      +1

      Справедливости ради, ORM не про отношения объектов, а про маппинг объектов на отношения (таблицы) РСУБД и обратно.


  1. VolCh
    17.07.2016 12:28
    +1

    Ещё одна реализация ActiveRecord?


    1. Delphinum
      18.07.2016 11:23

      Скорее TableGateway, не увидел бизнес-логики в сущностях.


  1. imfurman
    17.07.2016 12:32
    +5

    Честно говоря почему то люблю читать комменты про чужие неуклюжие велосипеды на php и так как изыскано каждый раз комментирующие поливают автора разными субстанциями. Возможно это какой-то фетиш, листая хабр каждый раз как вижу php, я написал <новое творение> и минусы… переходишь и получаешь неприкрытое удовольствие.


    1. Fedcomp
      17.07.2016 12:46
      +4

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


      1. shoomyst
        17.07.2016 13:07
        +1

        Велосипеды — это здорово, особенно в таком возрасте, когда ты считаешь себя самым одаренным программистом в мире))


        1. Fedcomp
          17.07.2016 13:31

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


          1. Delphinum
            18.07.2016 11:25

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


          1. Blast
            18.07.2016 13:38

            Таки возражу. Как мне кажется, если сразу учить, как оно делается адекватно, то знание сформируется на уровне «потому что так надо». Если же предварительно набить шишек, катаясь по граблям на велосипеде без седла с квадратными колесами, и потом уже с осознанием некой неправильности происходящего пойти за просветлением в умные книжки, то знаниям будет за что зацепиться, будет с чем сравнить нормальные решения и уже будем некое понимание, почему на самом деле нужно писать так, а не этак. Но разумеется, нужно держать баланс. Ещё такая ситуация, когда пишешь фигню а потом видишь нормальное решение, она отрезвляет, перестаёшь считать себя «самым одаренным программистом в мире»)


        1. MetaDone
          17.07.2016 14:58

          велосипеды хороши в качестве самообучения. Но вредны, если не покрыть их тестами. К примеру автор статьи сделал что-то работающее. Можно посоветовать ему соединить свое творение с http://codeception.com/ — тогда и появится минимальное разделение ответственности, может библиотека перестанет быть божественным объектом и в итоге все станет более-менее юзабельным


      1. Akuma
        17.07.2016 21:09

        ---del---


  1. Mendel
    17.07.2016 14:02
    +3

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

    По сути:
    1 — у Вас заметный пробел в базовых понятиях. Вы почитали документацию, но одной документации мало. Например понятие статики у вас несколько искаженное. «Метод существует у класса или у объекта»? что простите?) К данным это условно применимо, но не к методам. Почитайте какой-то учебник. А вообще постарайтесь минимизировать количество статических вызовов. для начала можете сделать синглтон. А вообще лучше использовать какой-то сервис-локатор. Тогда Вы сможете передавать класс работы с базой (обертку над ПДО) своим активрекорд или построителям, что даст Вам возможность делать разные конекты для разных таблиц, в разных базах (в том числе на разных СУБД).
    2 — про устаревшую библиотеку равно как и сам подход с эскейпингом отставший лет на 5-7 вам уже сказали. PDO для Вас выбор обязательный.
    3 — изучите получше принципы SOLID и DRY. Это не просто свод правил. Это практические принципы улучшающие любой код.
    3.1. — стоит отделять генерацию запросов от логики самих активрекорд. Это даст Вам возможность просто добавить еще один класс для другой СУБД просто слегка подправив запросы, но не потеряв совместимость с исходным мускулом.
    3.2. — get_class_vars() работоспособен здесь и сейчас. Но как только я расширяю класс дополнительным функционалом, добавляю новые статические поля, и весь код поломается. Вообще для простоты советую создать метод или статическую переменную с массивом имен полей, а сами поля хранить в другом массиве (не статическом), отдавая из через _гет и _сет.
    3.3. — имя поля с ид определяется исходя из того кто первый? эм… серьезно? Ну опять таки, даже если и так, то стоит такую вещь вынести в отдельное место, отдельным методом, чтобы при изменении логики не менять весь код. Вообще этот подход стоит применить везде. Вот захотите вы сделать метод поиска по произвольному условию, и что? писать новый метод? И для изменения метод, и для поиска, и для удаления...?

    ПС: а вообще продолжайте. Если будете учиться на своих ошибках, то быстро научитесь.
    ППС: не читайте перед обедом советских газет и людей с большими научными регалиями. Как правило их знания в ИТ успевают устареть на несколько поколений, чему пример ваш подход с эскейпингом вместо ПДО.


    1. mougrim
      17.07.2016 18:14

      2 — про устаревшую библиотеку равно как и сам подход с эскейпингом отставший лет на 5-7 вам уже сказали. PDO для Вас выбор обязательный.

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


      1. Mendel
        17.07.2016 23:35

        Ну она устаревшая именно потому что подход устаревший. Мы с вами можем и сами грамотно обернуть чтобы просто менять на другие движки попутно написав эмуляцию параметров где это нужно. Но:
        1 — зачем если есть из коробки универсальное?
        2 — новичек не потянет.


  1. M-A-XG
    17.07.2016 14:30

    Сырое.
    Нету JOINов.
    Вместо self::$db->query(«SET NAMES '$enc'»); лучше mysqli_set_charset($link, $enc);
    И, наверное, не стоит жестко завязываться на mysqli, как и на любой другой.

    Зачем $id = key(self::_getVars()); в Save(), Remove()?

    Да и вообще ORM — фигня.
    Смысл прописывать поля, которые есть у таблицы?

    Также у Вас не пройдет частичное обновление записей, также можно обновить только по первичному ключу.

    >if (isset($mValue[0]) && !empty($mValue[0]))

    Достаточно !empty($mValue[0]

    Также отступы слева во всех примерах кода стоило бы уменьшить.

    Ну такое.


    1. Aios
      17.07.2016 16:28

      У вас вообще своеобразное видение программирования) И честно говоря этот ваш «Тренд» порядком надоел.


      1. M-A-XG
        17.07.2016 16:44
        -3

        Ну а у Вас вообще собственного видения нет.
        Вы (не именно Вы, а вы) гавкаете даже на очевидные вещи.
        Отделяйте восприятие меня от восприятия текста, написанного мною.

        Вы хуже людей из сказки про «Король-то голый», потому что вы слепы.

        Я очень не хотел упоминать тут политику, хотя пару раз так и просилось.
        Но вы напоминаете секту свидетелей майдана, которая боится как черт ладана Шария, не имея при этом собственного мнения, а живя по методичках, при этом их мнение может резко коньюнктурно меняться на 180 градусов.


        1. Aios
          17.07.2016 17:05
          +2

          Извините меня конечно — но тут отделяй не отделяй. Результат один и тот же. Больным нужно лечится и держаться по дальше от здоровых.


    1. Delphinum
      18.07.2016 11:28

      Да и вообще ORM — фигня

      У автора нет ORM.


  1. Aios
    17.07.2016 15:41

    является результатом долгих мучений

    Собственно чем не подошли существующие аналоги?


    1. kovalevsky
      17.07.2016 15:50
      +10

      Судя по коду автора — существующие было слишком сложно понять


      1. Aios
        17.07.2016 15:51
        +2

        Тонко)


  1. Tekill
    18.07.2016 10:29

    Вспомнил себя, в свое время мне не хватало вот такой книги, чтобы мои библиотеки приближались к какой-либо стройной архитектуре и четкому разделению ответственностей:
    www.phptherightway.com/#databases


  1. maxru
    18.07.2016 11:20
    +5

    image


  1. saksmt
    19.07.2016 01:16
    +2

    Буду лучом света в этом чане нечистот комментариев. (не воспринимать как абсолютное одобрение)


    Вполне нормальная статья для начинающего.


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


    Про комментаторов, чьи слова порядком непонятны: временно забудь, повелосипедируй ещё немного в своё удовольствие, потом потыкай какой-нибудь готовый, поддерживаемый сообществом framework (только трупы не тормоши), потом прочти книгу банды четырёх, затем стоит прочесть "Шаблоны корпоративных приложений" и просветление придёт к тебе.


    И на будущее: не выкладывай свои велосипеды сюда (по крайней мере пока они полсотни звёзд на гитхабе не наберут, или ты не будешь в них абсолютно уверен), карму потом восстанавливать устанешь.


    Ну и добро пожаловать на хабр!


  1. POPSuL
    19.07.2016 08:53
    +1

    Очередная обертка над mysqli? Зачем? Сколько было подобных статей, и каждую из них сливали…
    Вот лучше бы изучили какую-нибудь доктрину, или eloquent...