Mysql2PHP или создание PHP объектов для хранения в реляционных базах данных

Небольшая заметка с примерами о том, как в PHP создать объекты на основе таблиц базы данных MySQL и немного о том, как сохранять и модифицировать данные при помощи этих объектов.

Много воды утекло с тех пор, как в языке программирования PHP появились первые реализации классов. До недавнего времени мне не удавалось найти время, чтобы оценить возможности OOP в PHP. Но вот не так давно, благодаря стечению обстоятельств я вернулся к разработке на этом прекрасном языке и готов поделиться парочкой умозаключений. Возможно, они станут для кого-то полезными, ведь так много людей сегодня занимается созданием сайтов.)

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

Итак, начнем сразу с примера - спроектируем простую базу данных. Берем Mysql Workbench 5.2.16 OSS Beta Revision 5249 и создаем Entity-Relationship модель. Рисуем красивые таблички и просто генерируем базу данных.

Теперь создадим классы для каждой таблицы. Пишем такой скрипт:

<?
	$c="\r\n/*\r\n*	This class generated by Mysql2php script.\r\n*	Author: Alexander Smelkov (alex@microgames.ru)\r\n*	Web page: www.microgames.ru\r\n*/\r\n";
	/*
	* SETTINGS
	*/
	$host 		= "localhost";
	$port 		= "3306";
	$dbname 	= "mydb";
	$user 		= "root";
	$password 	= "";
	$output_dir = "C:/Apache/my_site/entity/";
	$extends 	= "Entity";

	/*
	 * CONNECT TO DB
	 */
	$db_connect = mysql_connect($host,$user,$password);
	$select_db = mysql_select_db($dbname, $db_connect);

	/*
	 * TEMPLATES
	 */
	$template_h1 = "<?\r\n".$c."\r\n";
	$template_h2 = "require_once(\"<<>>.php\");\r\n\r\n";
	$template_h3 = "class <<>>";
	$template_h4 = " extends <<>> {\r\n\r\n";
	$template_h5 = " {\r\n\r\n";
	$template_h6 = "	var $<<>>\r\n";
	$template_h7 = "\r\n	public function getTableName(){\r\n";
	$template_h8 = "		return \"<<>>\";\r\n";
	$template_h9 = "	}\r\n";
	$template_h10 = "\r\n}\r\n\r\n";
	$template_h11 = "?>";

	/*
	 * get tables list and prepare class names
	 */
 
	$result = mysql_query("show tables from ".$dbname);
	$names = array();
	while($row = mysql_fetch_row($result)){
		$n = array();
		$n["table"] = $row[0];
		$className = "";
		foreach(explode("_", $n["table"]) as $piece){
			$className .= ucwords($piece);
		}
		$n["class"] = $className;
		$names[] = $n;
	}

	foreach($names as $name){
		$class_file = fopen($output_dir.$name["class"].".php", "w"); //Open for writing only; place the file pointer at the beginning of the file and truncate the file to zero length. If the file does not exist, attempt to create it. 
		//get column name from table
		$columns = mysql_query("show columns from ".$name["table"]);
		$columnArray = array();
		while($row = mysql_fetch_assoc($columns)){
			$c = array();
			$c["name"] = $row["Field"];
			$key = ($row["Key"] == "") ? "" : ", key: ".$row["Key"];
			$default = ($row["Default"] == "") ? "" : ", default: ".$row["Default"];
			$extra = ($row["Extra"] == "") ? "" : ", extra: ".$row["Extra"];
			$c["comment"] = "Type: ".$row["Type"].$key.$default.$extra;
			$columnArray[] = $c;
		}
	
		//start buid class
		$classText = "";
		$classText .= $template_h1;
		if($extends != "") $classText .= templateReplace($template_h2, "ENTITY_NAME", $extends);
		$classText .= templateReplace($template_h3, "CLASS_NAME", $name["class"]);
		$classText .= ($extends != "") ? templateReplace($template_h4, "ENTITY_NAME", $extends) : $template_h5;
		foreach($columnArray as $column){
			$classText .= templateReplace($template_h6, "VAR_NAME", $column["name"]."; 		// ".$column["comment"]);
		}
		$classText .= $template_h7;
		$classText .= templateReplace($template_h8, "TABLE_NAME", $name["table"]);
		$classText .= $template_h9;
		$classText .= $template_h10;
		$classText .= $template_h11;
		//write to file
		fwrite($class_file, $classText);
		fclose($class_file);
		echo "Class: ".$name["class"]." from table ".$name["table"]." READY!
";
	}

	function templateReplace($template, $replaceKey, $replaceVal) {
			$search = "/\<\<\<".strtoupper($replaceKey)."\>\>\>/";
			return preg_replace($search, $replaceVal, $template);
	}
?>

Что он делает? Соединятся с базой данных и последовательно, для всех таблиц, генерирует и сохраняет в заданной папке файлы с классами таблиц - Entity. Для того, чтобы все заработало сразу, надо убедиться что у скрипта хватает прав записывать в выбранную папку. Ну и заменить параметры подключения к базе данных. Мы получим для каждой таблицы файл такого вида:

require_once("Entity.php");

class SiteUser extends Entity {

	var $id; 		// Type: int(11), key: PRI, extra: auto_increment
	var $name; 	// Type: varchar(255)
	var $lastName; // Type: varchar(255)
	var $createdOn; // Type: timestamp, default: CURRENT_TIMESTAMP, extra: on update CURRENT_TIMESTAMP

	public function getTableName(){
		return "site_user";
	}

}

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

Во первых, он должен уметь читать из базы данных. Неплохо бы чтобы он туда еще и записывал сам. И обновлял. Это самые простые и необходимые вещи.

Добавляем:

public function select(){
}
public function insert() {
}
public function update(){
}

Затем, каким-то образом надо запихнуть внутрь соединение с базой данных. Лучше это дело вынести в отдельный класс. И использовать статический метод для выполнения SQL запроса.

Одно ограничение для использования этого метода: как нетрудно заметить сгенерированные классы содержат public свойства, которые соответствуют полям таблицы. Для того чтобы построить запрос к конкретной таблице достаточно из объекта выбрать все public свойства и их значения. Исходя из вышесказанного понятно, что использовать другие public переменные в классах таблиц и Entity нельзя. Но можно с успехом заменить их на protected или private или static.

Для того, чтобы продемонстрировать как применить на практике такой фильтр, я имплементировал метод insert.

Еще одно небольшое замечание: названия сгенерированных классов начинаются с заглавной буквы, а все подчеркивания в названиях таблиц удалены, и следующее за подчеркиванием слова тоже начинается с большой буквы. Если кто не знает зачем - так принято. А вот для того чтобы получить название таблицы для соответствующего объекта я добавил метод getTableName(). Конечно же можно без труда восстановить название таблицы из имени класса. Кому как нравиться.

Вот они два метода для тестирования:

public function insert() {
	$class = new ReflectionClass(get_class($this));
	$classVars = get_object_vars($this);
	$props = $class->getProperties();
	$propsAarray = array();
	foreach( $props as $prop ) {
		if ( !$prop->isPublic() || $prop->isStatic() ) continue;
		$propsAarray[] = $prop->getName();
	}
	$query = "INSERT INTO ".$this->getTableName()." SET ";
	$sql = array();
	foreach($propsAarray as $k => $v){
		$sql[] = (is_null($classVars[$v])) ? $v."=NULL":$v."='".mysql_real_escape_string($classVars[$v])."'";
	}
	$query .= implode(", ", $sql);
	try{
		$this->execQuery($query);
	}catch(Exception $e){
   		echo "There is an exception: ". $e->getMessage();
	}
}
	
public function execQuery($query) {
	$host 		= "localhost";
	$port 		= "3306";
	$dbname 	= "mydb";
	$user 		= "root";
	$db_connect = mysql_connect($host,$user,$password);
	$select_db = mysql_select_db($dbname, $db_connect);
  	$result = @mysql_query ($query);
	if (!$result) throw new Exception (mysql_error ());
}

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

<?

require_once("entity/SiteUser.php");

$my_class = new SiteUser();
$my_class->name = "Vasily";
$my_class->lastName = "Pupkin";
$my_class->insert();

?>

Никто не мешает расширить и углубить описанный выше подход. Например, создать триггеры – специальные методы beforeInsert() и afterInsert() и пр. Но самое интересное это конечно же объединения. И тут можно использовать совершенно различные подходы. Как мне кажется, создать универсальный способ объединения очень непросто и гораздо продуктивнее в каждом проекте для таких запросов формировать SQL индивидуально вручную. В любом случае это большая отдельная тема на которую возможно позднее я попробую поговорить. Те кому совсем лениво набирать буквы могут скачать пример вышеописанного кода тут.

Александр Смелков
Санкт-Петербург Зима 2014