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

Volmir

Новичок
Нет, я использую ОРМ в своей повседневной работе, но сказать, что она очень быстрая и простая в поддержке не могу.
Поработал только что над проектом, где ОРМ нет, написал груду функций на каждый SQL-запрос (выносил из контроллера обращение к БД). Производительность упала, количество ненужного кода сильно возросло. Стало грустно :(
Сразу захотелось использовать какой-то простой класс, с помощью которого можно было бы формировать SQL-запросы и получать результаты их выполнения в виде массивов или объектов.
Кто знает, есть ли простые и быстрые ОRM? :)
 

Фанат

oncle terrible
Команда форума
У меня вопросы не концептуальные по классу будут, а простые, чисто в плане испольования..
Я и сам-то не гений ООП, и пользуюсь всего одной функцией. Но пользоваться ей удобнее, чем этим классом.

почему методу fetch_all();требутeся предварительный вызов метода query? зачем писать две строчки, когда можно одну?
где методы fetch-row, fetch-col и fetch-one? неужели соответствующие ситуации возникают так редко?
где в примерах работа с динамически формируемыми запросами, которые составляют 99% используемых в реальности?
И, как следствие - искейпинг, с защитой от дурака и лишней работы?

такое ощущение, что этот (как и многие другие классы, впрочем), написан не от реальной потребости, а как отбывание некой повинности. всем по уставу положено - и я напишу.
 

Volmir

Новичок
почему методу fetch_all();требутeся предварительный вызов метода query? зачем писать две строчки, когда можно одну?
где методы fetch-row, fetch-col и fetch-one? неужели соответствующие ситуации возникают так редко?
Переделываю класс с учетом вышеизложенных требований ...
Скоро выложу результат.

где в примерах работа с динамически формируемыми запросами, которые составляют 99% используемых в реальности?
Так далеко я заглядывать пока не хочу. В данном классе предполагается, что текст запроса программист формирует сам.
 

Фанат

oncle terrible
Команда форума
Я ж ведь как раз о том, что требования должны быть не от дяди с форума, от от жизни, от жизненной потребности.
иначе класс получится мертвый.

А зря не хочешь. Это практически единственное, для чего такой класс нужен.
Тем более, что организовать собственный механизм плейсхолдеров на основе sprintf - это 5 строчек кода. Но зато класс будет все искейпить сам.
Только не забывать процентики для лайка удваивать.

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

Volmir

Новичок
Выкладываю редакцию класса "на сейчас" (с учетом последних правок и пожеланий).

Добавлены 3 метода:
public function fetch_all($query = '') {
}
public function fetch_row($query = '') {
}
public function fetch_val($query = '') {
}

Простота работы с классом увеличилась, функциональность расширилась.

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

	private $result;
	private $query;

	private $_fetch_row = false;

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


	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_local']['dbname'],
            mysql_connect(
                $config['db_local']['host'],
                $config['db_local']['user'],
                $config['db_local']['password']
            )
        );
	}

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

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

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

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

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

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

		return $result;
	}

	public function fetch_all($query = '') {
		if ($query)
		{
			$this->query($query);
		}
		return $this->result;
	}

	public function fetch_row($query = '') {
		if ($query)
		{
			$this->query($query);
		}
		return $this->result[0];
	}

	public function fetch_val($query = '') {
		if ($query)
		{
            $this->_fetch_row = true;
            $this->query($query);
            $this->_fetch_row = false;
	    }
		return $this->result[0];
	}

	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() {
		;
	}
}
И пример вызова класса:
PHP:
include("lib/config.class.php");
include("lib/db.mysql.class.php");

$db = DB::getInstance();

//-------- SELECT ALL --------
$sql = "SELECT * FROM hotel LIMIT 10";
$rows = $db->fetch_all($sql);
if ($rows) {
	echo '<hr />';
	echo 'SELECT ALL: <br />';
	foreach ($rows as $row) {
		print_r($row);
	}
}
echo '<hr />';

//-------- SELECT one row --------
$sql = "SELECT * FROM hotel LIMIT 10";
$row = $db->fetch_row($sql);
echo 'SELECT one row: <br />';
print_r($row);
echo '<hr />';

//-------- SELECT value --------
$sql = "SELECT hotel_id FROM hotel LIMIT 10";
$hotel_id = $db->fetch_val($sql);
echo 'SELECT one value: ' . $hotel_id;
echo '<hr />';

//-------- UPDATE --------
$sql = "UPDATE hotel SET hotel_id=3 WHERE hotel_id=3";
$result = $db->query($sql);
echo 'UPDATE: ' . $result;
echo '<hr />';

//-------- INSERT + LAST_INSERT_ID --------
$sql = "INSERT INTO hotel (hotel_id)
        VALUES ('')";
$result = $db->query($sql);
echo 'INSERT + LAST_INSERT_ID: ' . $db->insert_id();
echo '<hr />';

//-------- DELETE --------
$sql = "DELETE FROM hotel WHERE hotel_id = " . $db->insert_id();
$result = $db->query($sql);
echo 'DELETE: ' . $result;
echo '<hr />';

//------- 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.)';
}
 

Volmir

Новичок
if (isset($_REQUEST['showqueries']) && $_REQUEST['showqueries']) WTF??
Это вызов режима отладки прямо в строке браузера GET-запросом.
Пример: http://site.com/index.php?showqueries=1

Может и не безопасно, но очень удобно для отладки (на сервере разработки конечно, не на продакшне).
 

Ragazzo

TDD interested
Volmir
Я про само условие..второе условие что проверяет то?
 

Фанат

oncle terrible
Команда форума
insert не жизненный.
делаем регистрацию юзера с добавлением двух десятков полей, смотрим на получившийся код и думаем - как его можно оптимизировать.

по поводу искейпинга.
Сделать свои собственные плейсхолдеры на базе sprintf-a совсем ведь несложно.
к функции query добавляется буквально 5 строчек!

PHP:
function query() {
  $args = func_get_args();
  $query = array_shift($args);
  $query = str_replace("%s","'%s'",$query); 

  foreach ($args as $key => $val) {
    $args[$key] = mysql_real_escape_string($val);
  }

  $query = vsprintf($query, $args);
}
и всё!

PHP:
$sql = "DELETE FROM hotel WHERE hotel_id = %d";
$result = $db->query($sql, $db->insert_id());

$sql = "SELECT hotel_id FROM hotel WHERE name = %s";
$hotel_id = $db->fetch_val($sql,$_GET['name']);

//единственная проблема будет с лайком. надо будет делать так:

$sql = "SELECT hotel_id FROM hotel WHERE name = %s";
$name = '%'.$_GET['name'].'%';
$hotel_id = $db->fetch_val($sql,$name);
 

Volmir

Новичок
по поводу искейпинга.
Сделать свои собственные плейсхолдеры на базе sprintf-a совсем ведь несложно.
к функции query добавляется буквально 5 строчек!
Спасибо за расширенный комментарий. Попробую внедрить этот код, поработать с ним более вдумчиво.
 

tz-lom

Продвинутый новичок
Фанат
я предпочитаю вместо % использовать # чтобы запрос всегда падал если есть незаполненные поля
 

Фанат

oncle terrible
Команда форума
tz-lom
так спринтф тоже упадет.

а # - имеются в виду свои собственные плесхолдеры, стр-реплейсом?
А какие типы поддерживаются?
 

Фанат

oncle terrible
Команда форума
Chusha
правильный, кстати, подход. именно тянуть весь функционал
чтобы он весь был в одном месте.

другое дело что да - query не должна дублировать функционал fetch_all
должна быть квери-аналог мускуль_квери, должны быть аналоги фетч_аррэй (ели придется работать с большим количеством данных - не одними веб-страницами жив сайт)
а уже используя этот функционал пишутся обертки для получения нуджной инфы разом
 

tz-lom

Продвинутый новичок
Фанат
string экранируется , всякие там integer float нет , BOOL и NULL преобразуются в соответствующие строки и есть "тип" UnsafeSQL если вдруг понадобится сделать прямую вставку неэкранированных строк
 

Фанат

oncle terrible
Команда форума
tz-lom
просто поверить не могу. неужели строки экранируются? А булев тип преобразовывается? Это просто удивительно.
твой комментарий просто кладезь информации. я столько много нового узнал о твоей работе с плейсхолдерами.
 

Фанат

oncle terrible
Команда форума
ну, в общем, да. абстрагировать до такой стпени не стоило.
метод query должен возвращать ресурс
а остальные методы уже добывать из этого ресурса данные в нужном формате.

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

в Exception($this->error() желательно добавить сам запрос.

вообще, конечно, этот класс требует одного - обкатки практикой.
реальное использование (после которого автор выкинет, в частности, с матюками конструкцию while ($this->result[] = mysql_fetch_assoc($result)) {}) принесет гораздо больше пользы, чем 1000 советов на форуме.
 

malina95

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

Дополнения:
- Синглтон
- данные для коннекта вынесены в отдельный класс (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 />';
}
}

Если вы уже делаете класс по работе с БД то надо делать хорошо, и использовать расширенный метод подключения MYSQLi
 

Фанат

oncle terrible
Команда форума
специалистов-то понабежало.
причём спроси что одного, что второго - зачем им mysqli - ведь не ответят.
 
Сверху