Создал класс (ООП PHP5) для работы с БД MySQL - покритикуйте, похвалите

Alexandre

PHPПенсионер
>>а я бы коннектился по требованию
>а в чем разница?
http://ibash.org.ru/quote.php?id=11195
кстати о лене...
надо просто делать ленивую коннекцию, а не плодить лишних методов.

спасибо, fixxxer за подсказку, я так сделаю в своем классе.
 

Volmir

Новичок
Скажите, Alexandre, а где можно почитать о "ленивом коннекте" и что это вообще такое?
 

Beavis

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

Rin

*
Я использую MysqlRapid

>если никаких запросов не выполняется, то и соединение не производится

в каких случаях запросы могут не выполняться, кэширование?
 

Beavis

Banned
Rin
Ну например есть очень простой сайт, и файл index.php в котором создается объект для работы с базой.
При его создании коннекта не происходит.
Если дальше подключается какой нибудь модуль, требующий соединения с бд, происходит подключение прямо перед запросом, а если какая нибудь статическая страничка, типа "о компании", которая не лезет в базу, то и соединения не создается

ну или кеш как вариант))
 

Volmir

Новичок
Выкладываю это исправленный и дополненный класс-обертку.

Дополнения:
- Синглтон
- данные для коннекта вынесены в отдельный класс (config)
- Учет времени выполнения всех запросов к MySQL (в отладочном режиме)
- защита при клонировании
- комментарии к функциям и переменным (немного)

PHP:
/**
 * Singleton DB class. Provides access to database MySQL
 * @author
 * @copyright
 */
class DB {
	/**
	 * @var DB
	 */
	protected static $instance = null;

	private $result;
	private $query;

	public $debug = 0;
	public $queries = array();

	/**
	 * total traffic or null
	 * @var int
	 */
	protected $_total_traffic = null;

	private function __construct() {
		self::connect();

		if (isset($_REQUEST['showqueries']) && $_REQUEST['showqueries']) {
			$this->debug = 1;
		}
	}
	/**
	 * Returns instance of DB
	 * @return DB
	 */
	public static function getInstance() {
		if (is_null(self::$instance))
		{
			self::$instance = new DB();
		}
		return self::$instance;
	}

	private function connect() {
		$config = config::getInstance()->config_values;
        mysql_select_db(
            $config['db']['dbname'],
            mysql_connect(
                $config['db']['host'],
                $config['db']['user'],
                $config['db']['password']
            )
        );
	}

	public function query($query) {
	    $this->result = array();
	    $this->query = trim($query);

		if ($this->debug) {
			$time_start = microtime(true);
		}

		$res = mysql_query($this->query);

		if ($this->debug) {
			$time_run = microtime(true) - $time_start;
			$this->queries[] = array('query' => $this->query, 'time' => $time_run);
		}

		if (!$res) {
    		throw new Exception($this->error());
		}

		if (preg_match('/^select /i', $this->query) || preg_match('/^show /i', $this->query)) {
			while ($this->result[] = mysql_fetch_assoc($res)) {}
		}

		return $res;
	}

	public function fetch_all() {
		return $this->result;
	}

	public function result($index, $field) {
		if ($index < 0 || $index >= sizeof($this->result)) {
    		throw new Exception('DB error: ' . $index . ': no such index');
		}
		return $this->result[$index][$field];
	}

	public function numrows() {
		return sizeof($this->result);
	}

	public function error() {
		return mysql_error();
	}

	public function insert_id() {
		return mysql_insert_id();
	}

	/**
	 * Singleton
	 */
	protected function  __clone() {
		;
	}

	/**
	 * Returns database traffic in this connection (bytes)
	 * @return int
	 */
	public function getTotalTraffic()
	{
		if (is_null($this->_total_traffic))
		{
			try
			{
				$this->query("SHOW STATUS LIKE 'Bytes_sent'");
				$this->_total_traffic = $this->result[0]['Value'];
			}
			catch (Exception $e)
			{
				$this->_total_traffic = null;
			}
		}
		return $this->_total_traffic;
	}
}
Пример использования класса:
PHP:
include("lib/config.class.php");
include("lib/db.mysql.class.php");

$db = DB::getInstance();

//-------- SELECT --------
$sql = "SELECT * FROM news LIMIT 0,10";
$result = $db->query($sql);
$rows = $db->fetch_all();
if ($rows) {
	foreach ($rows as $row) {
		print_r($row);
	}
}

//-------- UPDATE --------
$sql = "UPDATE news SET nid=3 WHERE nid=3";
$result = $db->query($sql);

//-------- INSERT + LAST_INSERT_ID --------
$sql = "INSERT INTO news (nid, dt, title, full_text)
        VALUES ('', NOW(), 'Example title', 'Example text')";
$result = $db->query($sql);
echo 'LAST_INSERT_ID = ' . $db->insert_id() . "<br />";

//-------- DELETE --------
$sql = "DELETE FROM news WHERE nid=5";
$result = $db->query($sql);

//------- DEBUG, show count of queries, list of all queries --------
if ($db->debug && isset($db->queries)) {
    $mysql_time = '';
    echo '<hr noshade />';
    foreach ($db->queries as $query) {
        echo '<pre>' . $query['query'] . '</pre>run: ' . sprintf("%01.6f", $query['time']) . ' sec.<hr noshade />';
        $mysql_time += $query['time'];
    }
    echo 'Total queries: <b>' . count($db->queries) . '</b> (' . sprintf("%01.6f", $mysql_time) . ' sec.), ';
    echo 'Mysql traffic: <b>' . number_format($db->getTotalTraffic() / 1024, 1, '.', ' ') . 'Kb</b>.<hr noshade />';
}
}
 

tz-lom

Продвинутый новичок
1е - почему синглтон?
2е - не обрабатывается ситуация с коннектами на разные базы одного сервера
3е - ошибки надо кидать а не эхать
PHP:
 public function fetch_assoc()  {
        return $this->fetch_array();
    }
О_о'
 

Volmir

Новичок
public function fetch_assoc() - удалил из кода.

почему синглтон?
А как по-другому? Коннект к базе один раз произошел и пошли запросы выполняться.

3е - ошибки надо кидать а не эхать
Куда кидать? В Exception? Не совсем понял мысль.

PHP:
try
{
	$res = mysql_query($query);
}
catch (Exception $e)
{
	;
}
или так?

PHP:
if (!$res) {
	throw new Exception('<pre>' . $query . '</pre>');
}
 

tz-lom

Продвинутый новичок
про ошибки ваш 2й вариант я имел ввиду

по другому-без синглтона,ведь может потребоваться несколько соединений с разными базами,и DI для баз это на самом деле хорошее решение
 

Volmir

Новичок
Заменил вывод на:
PHP:
if (!$res) {
	echo '<pre>' . $query . '</pre>';
    	throw new Exception($this->error());
}
Информативность сообщения об ошибке сильно возросла! Спасибо за подсказку!

Пример:
SELECT * FROM news

Fatal error: Uncaught exception 'Exception' with message 'Table 'test.news' doesn't exist' in C:\webserver\Apache2\htdocs\www\projects\db.mysql.class.php:68 Stack trace: #0 C:\webserver\Apache2\htdocs\www\projects\index.php(9): DB->query('SELECT * FROM n...') #1 {main} thrown in C:\webserver\Apache2\htdocs\www\projects\db.mysql.class.php on line 68
 

Ragazzo

TDD interested
По оформлению:
сразу бросается в глаза
PHP:
        mysql_select_db($config['db']['db'], mysql_pconnect($config['db']['host'], $config['db']['user'], $config['db']['password']));
//форматирование
        mysql_select_db(
            $config['db']['db'], 
            mysql_pconnect(
                $config['db']['host'],
                $config['db']['user'], 
                $config['db']['password']
            )
        );
 

tz-lom

Продвинутый новичок
Заменил вывод на:
PHP:
if (!$res) {
	echo '<pre>' . $query . '</pre>';
    	throw new Exception($this->error());
}
Информативность сообщения об ошибке сильно возросла! Спасибо за подсказку!
FACEPALM

ну вот смотри,сдох у тебя запрос,не важно почему,и пользователь радостно увидит весь запрос
1е - нафига он ему
2е - крякер конечно скажет спасибо,но оно тебе надо?
3е - а если ошибка ожидаемая и ловимая?
 

Ragazzo

TDD interested
Неплохо бы сделать в конфиге метод, возвращающий значение по имени...
PHP:
        $config = config::getInstance()->config_values;
        mysql_select_db(
            $config['db']['db'],
т.е что-то вроде
config::getInstance()->getValueByName('db');
а лучше было бы наверно так
config::getInstance()->db->getParam('param_name');
хотя не видя конфига трудно сказать...и еще
>>$config['db']['db']
странно выглядит..индексы именуй логично...
 

Volmir

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

Дополняю мысль: надо текст запроса, URL в котором произошла ошибка слать по почте разработчику. А вывод запросов - действительно отключить.

Вот конфиг. Конструкция типа "config::getInstance()->getValueByName('db');" вполне реализуема.
PHP:
class config
{
	/**
	 * @var string $config_file
	 */
	private static $config_file = 'config/config.ini.php';

	/**
	 * @var array $config_values;
	 */
	public $config_values = array();

	/**
	 * @var object $instance
	 */
	private static $instance = null;

	/**
	 * @the constructor is set to private so
	 * @so nobody can create a new instance using new
	 */
	private function __construct()
	{
		$this->config_values = parse_ini_file(self::$config_file, true);
	}

	/**
	 * Return Config instance or create intitial instance
	 * @access public
	 * @return object
	 */
	public static function getInstance()
	{
 		if(is_null(self::$instance))
 		{
 			self::$instance = new config();
 		}
		return self::$instance;
	}

	/**
	 * @get a config option by key
	 * @access public
	 * @param string $key:The configuration setting key
	 * @return string
	 */
	public function getValue($key)
	{
		return self::$config_values[$key];
	}

	/**
	 * @__clone
	 * @access private
	 */
	private function __clone()
	{
		;
	}
}
 

vovanium

Новичок
Volmir
Хм, а зачем сразу весь результат селекта загонять в массив? Всё равно массив нужно обрабатывать как-то, т.е. лишний цикл + лишняя память занятая массивом, сделал бы что-то типа fetch_all?

Плюс
PHP:
$query_info = array('query' => $query, 'time' => $time_run);
 $this->queries[] = $query_info;
заменить на
PHP:
 $this->queries[] = array('query' => $query, 'time' => $time_run);
зачем плодить переменную которая не используется больше
 

Volmir

Новичок
Хм, а зачем сразу весь результат селекта загонять в массив? Всё равно массив нужно обрабатывать как-то, т.е. лишний цикл + лишняя память занятая массивом, сделал бы что-то типа fetch_all?
заменил на конструкцию типа:
PHP:
if (preg_match('/^select /i', trim($this->query))) {
			while ($this->result[] = mysql_fetch_assoc($res)) {}
		}

	public function fetch_all() {
		return $this->result;
	}
ты за год даже экзепшины не выучил? ужс.
В последней редакции класса есть еще места, в которых можно добавить вызов Exception ?

зачем писать такой класс если есть PDO ?
Для обучения "правильному" и "корректному" стилю программирования, ухода от "говонокода", выяснения деталей и нюансов.
+ я собираюсь применять этот класс в простых, "быстрописных" скриптах, где не надо использовать большие фреймворки. Так же есть планы собрать для себя минималистичный, быстрый и занимающий мало места фреймворк (база, шаблонизатор, MVC, конфиг, роутер, внутр сообщения, редирект, обработка ошибок + что-то еще по мелочи), в котором я буду хорошо ориентироваться - для быстрого написания различных проектов.

В посте #46 постоянно обновляю код с учетом всех изменений.

И еще добавлю. При помощи коллективного разума класс просто преобразился! Все ближе и ближе к идеалу :)
 

whirlwind

TDD infected, paranoid
> Для обучения "правильному" и "корректному" стилю программирования, ухода от "говонокода", выяснения деталей и нюансов.

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