Автоматическая генерация параметров для PDO

в 9:37, , рубрики: PDO, php, автоматизация рутины, Программирование, метки: , ,

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

$paramsArray[‘fname’] = $_POST[‘fname’];
$paramsArray[‘sname’] = $_POST[‘sname’];

Всех, кому интересно, как я решил такую задачу, приглашаю под кат.

Для начала стоит рассказать немного о PDO.
Дело в том, что я стал использовать PDO в своих проектах не так давно. Положительных эмоций была масса. Во-первых, практически не приходилось больше самому заботится о «чистке» передаваемых параметров: все, что могло нанести вред базе, благополучно отметалось при подготовке запроса. Во-вторых сменить СУБД, чаще всего, можно было заменой одной единственной строки. Плюс ко всему, мы имеем возможность использовать сразу несколько СУБД, что в некоторых случаях может быть полезно. В-третьих, параметры легко передавать: поместили все в массив и — готово. Можно использовать как безымянные, на манер JDBC, так и именные параметры.
Пример:

require_once('config.php');
$dbc = new PDO("mysql:host=".HOST.";dbname=".DATA_BASE_NAME.";charset=utf8", USER, PASSWORD);

#Для безымянных
$sqlString = "INSERT INTO table1(`name`, `text`, `date`) VALUES (?, ?, ?)";
$params = array("First Post", "Hello World", "01/01/2013");
$statment = $dbc->prepare($sqlString);
$statment->execute($params);

#Для именных
$sqlString = "INSERT INTO table1(`name`, `text`, `date`) VALUES (:name, :text, :date)";

$params = array("name" => "Second Post", "text" => "Say Hello", "date" => "01/02/2013");

$statment = $dbc->prepare($sqlString);
$statment->execute($params);

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

$params = array("text" => "Habrahabr", "name" => "Third Post", "date" => "01/03/2013");
$statment->execute($params);

Итак, соберем все в единое целое и напишем немного кода.

class DB {
    static private $dbc
        static public function dbc () {
        	try {
        	require_once('config.php');
            self::$dbc = new PDO("mysql:host=".HOST.";dbname=".DATA_BASE_NAME.";charset=utf8", USER, PASSWORD);
			} catch(PDOException $exc) {
				Logger::setLog($exc->getMessage());
				return false;
			}
		}
		static public function insert($sqlString, $object) {
			if (!isset(self::$dbc)) {
                self::dbc();
            }
            try{
                $statment = self::$dbc->prepare($sqlString);
                $statment->execute((array)$object);
                return self::$dbc->lastInsertId();
            } catch (PDOException $exc) {
            	Logger::setLog($exc->getMessage());
                return false;
            }
		}
}

Первый метод создает новое подключение. Второй выполняет запросы INSERT. Если подключение не было создано, создает его. PDO умеет выбрасывать исключения, так что желательно их перехватить. Можно передавать как массивы, так и объекты. Лично я чаще предпочитаю вторые.

За обработку форм назначим класс PostController.

class PostController {
    const INSERT = "INSERT INTO `posts` (`name`, `text`, `date`) VALUES (:name, :text, :date)";
    
    public static function post(){
        if (isset($_POST["new_post"])) {
            $insertObject = NULL;
            $insertObject->name = $_POST["name"];
            $insertObject->text = $_POST["text"];
            $insertObject->date = $_POST["date"];
            DB::insert(self::INSERT, $insertObject);
        }
    }
}

А теперь представьте, что у нас достаточно много форм, или таблицы состоят из 10 — 20 полей. А если и то и другое? Записи вида $insertObject->name = $_POST[«name»]; будут захламлять объемную часть кода. Если честно, первая идея, которая мне пришла в голову, была написать нечто подобное:

DB::insert(self::INSERT, $_POST);

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

class PostController {
    const INSERT = "INSERT INTO `posts` (`name`, `text`, `date`) VALUES (:name, :text, :date)";
    
    public static function post(){
        if (isset($_POST["new_post"])) {
            $insertArray = $_POST;
            unset($insertArray["new_post"]);
            DB::insert(self::INSERT, $insertArray);
        }
    }
}

Решение действительно работало, код сократился, хотя и стал менее понятным. Однако, в этом году в рамках курсовой работы мне пришлось трудиться над одним WEB сервисом для университета. Проблема заключалась в том, что была одна огромная форма с множеством элементов. В базе было много таблиц, каждая из которых, в свою очередь, содержала множество полей. Выбор таблиц и полей для вставки зависел от махинаций на форме. И первый, и второй способы требовали написания солидных кусков кода, чего из-за ограниченности сроков делать совершенно не хотелось. Спустя 10 минут размышлений в голову пришел один метод, который бы, разбирая строку запроса SQL, сам находил данные для параметров, сопоставлял их, и возвращал готовый объект для вставки.

class PostController {
    const INSERT = "INSERT INTO `posts` (`name`, `text`, `date`) VALUES (:name, :text, :date)";

    #Шаблон поиска параметров.
    #Если кто-то использует и другие символы, можно их легко добавить
    const PATTERN = "|:(([-A-Za-z1-9_])*?)|U";
	public static function findObjectInPost($queryString) {
		$fields = array();
		$postObject = NULL;
		preg_match_all(self::PATTERN, $queryString, $fields);
		foreach ($fields[1] as $value) {
			if (isset($_POST[$value])) {
				$sender = $_POST[$value];
				if (strcasecmp($sender, "") == 0) {
					$sender = NULL;
				}
				$postObject->$value = $sender;
			}
		}
		return $postObject;
	}
    
    public static function post(){
        if (isset($_POST["new_post"])) {
            unset($_POST["new_post"]);
            DB::insert(self::INSERT, $_POST);
        }
    }
}

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

В результате получаем быструю обработку запроса в одну строку:

    public static function post(){
        if (isset($_POST["new_post"])) {
            DB::insert(self::INSERT_POST, $insertObject = self::findObjectInPost(self::INSERT_POST));
        }
            if (isset($_POST["new_company"])) {
            DB::insert(self::INSERT_COMPANY, $insertObject = self::findObjectInPost(self::INSERT_COMPANY));
        }
           if (isset($_POST["new_author"])) {
            DB::insert(self::INSERT_AUTHOR, $insertObject = self::findObjectInPost(self::INSERT_AUTHOR));
        }
    }

Резюме.
Таким образом, можно автоматизировать процесс задания именных параметров для выполнения SQL запроса, тем самым сократив код и время, затрачиваемое на разработку. Разумеется, решение применимо к любым запросам с именными параметрами.

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

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

Автор: authoris

Источник

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


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