всем привет я пишу сайт на пхп четыре (из-за pdo это пока не получается, поэтому если есть замена pdo для работы с различными бд — подскажите)
главная фича моего движка в простой кодовой базе, в которой разберётся любой начинающий программист на html, css и php
есть поддержка виртуальных хостов. один движок может обслуживать сколько угодно сайтов, у каждого сайта может быть свой индивидуальный дизайн, при этом у них единая база данных с сообщениями и пользователями. например, на одном движке можно вести несколько сайтов, а затем «на лету» их смержить в один, объеденив область видимости нескольских сайтов в один, либо же наоборот, если проект разрастается и условная тема про собачек на сайте котиков набирает популярность — сайт можно раздробить на два, один про кошечек, второй про собачек, при этом пользователям не придётся создавать новые аккаунты на втором сайте.
как уже сказал, у каждого хоста может быть свой индивидуальный дизайн, то есть пользователь вообще не будет знать, что сайт про кошечек и сайт про собачек — это один и тот-же сайт. он увидит перед собой два разных сайта, у каждого своя область видимости в базе данных и дизайн.
все сообщения на сайте — как статьи на википедии, или как сообщения на ЛОРе, могут редактироваться сколько угодно раз и хранить всю историю изменений. весь ключевой вопрос лишь в том, способен ли выдержать SQLite такую нагрузку, или же придётся смотреть в сторону другой базы данных???
пользователи могут создавать сколько угодно аккаунтов и логиниться под разными аккаунтами, а затем переключаться между ними. считаю это прикольным.
у меня своя собственная реализация сессий, которая никак не связана со встроенными сессиями на пхп и никак их не использует. то есть session_*(); и $_SESSION не задействуются. моя реализация позволяет более тонко контролировать все нюансы, например, не создавать пустую сессию «в холостую», или например точно определить, что в папке на диске достаточно места для хранения данных сессии.
вот пример базы данных для сообщений. они хранят всю историю изменений.
CREATE TABLE IF NOT EXISTS 'post'
(
'id' INTEGER PRIMARY KEY,
'domain' NVARCHAR(255) DEFAULT NULL,
'sub_reply' INTEGER DEFAULT NULL,
'date_created' DATETIME DEFAULT CURRENT_TIMESTAMP,
'date_modified' DATETIME DEFAULT NULL,
'flags' NVARCHAR(200) DEFAULT NULL
);
CREATE TABLE IF NOT EXISTS 'post_history'
(
'id' INTEGER PRIMARY KEY,
'sub_post' INTEGER NOT NULL,
'ref_text' INTEGER NOT NULL,
'date_created' DATETIME DEFAULT CURRENT_TIMESTAMP,
'date_modified' DATETIME DEFAULT NULL
);
CREATE TABLE IF NOT EXISTS 'post_text'
(
'id' INTEGER PRIMARY KEY,
'text' TEXT DEFAULT NULL,
'text_headers' TEXT DEFAULT NULL
);
сам текст сообщений хранится отдельно в третьей таблице, это сделано потому, что у каждой базы данных (конкретно у MySQL и PostgreSQL) на этот счёт разное мнение. кто-то хранит TEXT и BLOB далеко за пределами основной таблицы, чтобы минимизировать нагрузку при выборке из базы, а кто-то хранит там же рядышком. правильнее будет хранить TEXT отдельно. поэтому храню.
я не осилил ООП и не смог написать на классах, поэтому я писал на функциях, и я их назвал неклассами.
в папочке ./src/noclass/ у меня лежат следующие неклассы.
database.noclass.php:
<?php
function db_directory_sqlite() {
return dirname($_SERVER['DOCUMENT_ROOT']) . '/db';
}
function db_connect($db_dsn = null, $db_username = null, $db_password = null, $db_attributes = null) {
$db = false;
if (strpos($db_dsn, 'sqlite:') == 0) {
$db_file = substr($db_dsn, strlen('sqlite:'));
if ($db_file == ':memory:') {
$db = true;
}
elseif (is_writeable(db_directory_sqlite())) {
$db = true;
}
}
if ($db == false) {
return false;
}
$db = db_connect_retry($db_dsn, $db_username, $db_password, $db_attributes);
return $db;
}
function db_connect_retry($db_dsn, $db_username, $db_password, $db_attributes) {
$db = false;
while ($db == false) {
try {
$db = new PDO($db_dsn, $db_username, $db_password, $db_attributes);
}
catch (Exception $e) {
if (stripos($e->getMessage(), 'DATABASE IS LOCKED')) {
usleep(50000);
continue;
}
return false;
}
}
return $db;
}
function db_open_sqlite_in_memory() {
$db_file = false;
$db_dsn = 'sqlite::memory:';
$db = db_connect($db_dsn);
return $db;
}
function db_open_sqlite($db_file) {
if ($db_file == ':memory:') {
$db = db_open_sqlite_in_memory();
return $db;
}
$db_dsn = 'sqlite:' . $db_file;
$db = db_connect($db_dsn);
return $db;
}
function db_open($db_dsn) {
$db = db_open_sqlite($db_dsn);
return $db;
}
function db_exec($db, $query_string) {
$query = $db->exec($query_string);
return $query;
}
function db_query($db, $query_string) {
$query = $db->query($query_string);
return $query;
}
function db_run($db, $query_string, $query_args = false) {
if ($query_args == false) {
$query = db_query($db, $query_string);
return $query;
}
$query = $db->prepare($query_string);
$query->execute($query_args);
return $query;
}
function db_begin_transaction($db) {
$query = $db->beginTransaction();
return $query;
}
function db_is_transaction($db) {
$query = $db->inTransaction();
return $query;
}
function db_rollback($db) {
$query = $db->rollBack();
return $query;
}
function db_commit($db) {
$query = $db->commit();
return $query;
}
function db_last_id($db) {
$query = $db->lastInsertId();
return $query;
}
function db_insert($db, $table, $values) {
$keys = array_keys($values);
$values = array_values($values);
$columns = array();
foreach ($keys as $item) {
$columns[] = "'$item'";
}
$columns = implode(',', $columns);
$placeholders = array();
foreach ($keys as $item) {
$placeholders[] = "?";
}
$placeholders = implode(',', $placeholders);
$query = "INSERT INTO $table ($columns) VALUES ($placeholders);";
$query = db_run($db, $query, $values);
return $query;
}
function db_update($db, $table, $value, $where) {
$collection = array_merge($value, $where);
$collection = array_values($collection);
$fields = array();
foreach ($value as $item => $data) {
$fields[] = "$item = ?";
}
$fields = implode(',', $fields);
$places = array();
foreach ($where as $item => $data) {
$places[] = "$item = ?";
}
$places = implode(' AND ', $places);
$query = "UPDATE $table SET $fields WHERE $places;";
$query = db_run($db, $query, $collection);
return $query;
}
function db($db_name) {
$db_file = db_directory_sqlite() . DIRECTORY_SEPARATOR . $db_name;
$db = db_open($db_file);
return $db;
}
post.noclass.php:
<?php
function post_insert($text_headers, $text, $replyto = false) {
$db = db('example.db');
if ($db == false) {
return false;
}
db_begin_transaction($db);
if (db_post_insert($db, $text_headers, $text, $replyto)) {
db_commit($db);
return true;
}
db_rollback($db);
return false;
}
function db_post_insert($db, $text_headers, $text, $replyto = false) {
if ($replyto == false) {
$id = db_post_insert_id($db);
if ($id == false) {
return false;
}
}
else {
$replyto = db_post_select_id_single($db, $replyto);
if ($replyto == false) {
return false;
}
else {
$id = db_post_insert_id($db);
if ($id == false) {
return false;
}
}
}
if (db_post_insert_post($db, $id, $replyto)) {
$ref_text = db_post_insert_post_text($db, $text_headers, $text);
if ($ref_text == false) {
return false;
}
if (db_post_insert_post_history($db, $id, $ref_text)) {
return $id;
}
}
return false;
}
function db_post_insert_id($db) {
$value = array(
'id' => null
);
db_insert($db, 'post', $value);
return db_last_id($db);
}
function db_post_select_id_single($db, $id) {
$value = array(
'id' => $id
);
$query = 'SELECT id FROM post WHERE id = :id;';
$query = db_run($db, $query, $value);
$row = $query->fetch();
return ($row == false) ? false : $row['id'];
}
function db_post_insert_post($db, $id, $replyto = false) {
$date_created = date('Y-m-d H:i:s', time());
$date_modified = null;
$search = array(
'id' => $id
);
$entry = array(
'id' => null,
'domain' => null,
'sub_reply' => $replyto,
'date_created' => $date_created,
'date_modified' => $date_modified,
'flags' => null
);
// remove false, null and other zero-values from entry
$entry = array_filter($entry);
if (db_update($db, 'post', $entry, $search)) {
return $id;
}
return false;
}
function db_post_insert_post_text($db, $text_headers, $text) {
if ($text_headers == false) {
//nothing to do -- skip headers
}
else {
$text_headers = serialize($text_headers);
}
$entry = array(
'id' => null,
'text' => $text,
'text_headers' => $text_headers
);
// remove false, null and other zero-values from entry
$entry = array_filter($entry);
if (db_insert($db, 'post_text', $entry)) {
return db_last_id($db);
}
return false;
}
function db_post_insert_post_history($db, $sub_post, $ref_text) {
$date_created = date('Y-m-d H:i:s', time());
$date_modified = null;
$entry = array(
'id' => null,
'sub_post' => $sub_post,
'ref_text' => $ref_text,
'date_created' => $date_created,
'date_modified' => $date_modified
);
// remove false, null and other zero-values from entry
$entry = array_filter($entry);
if (db_insert($db, 'post_history', $entry)) {
return db_last_id($db);
}
return false;
}
function db_post_select_count($db) {
$query = db_query($db, 'SELECT COUNT(*) as sizeof FROM post;');
$row = $query->fetch();
return $row['sizeof'];
}
function post_update($id, $text_headers, $text) {
$db = db('example.db');
if ($db == false) {
return false;
}
db_begin_transaction($db);
if (db_post_update($db, $id, $text_headers, $text)) {
db_commit($db);
return true;
}
db_rollback($db);
return false;
}
function db_post_update($db, $id, $text_headers, $text) {
$id = db_post_select_id_single($db, $id);
if ($id == false) {
return false;
}
if (db_post_update_post($db, $id)) {
$ref_text = db_post_insert_post_text($db, $text_headers, $text);
if ($ref_text == false) {
return false;
}
if (db_post_insert_post_history($db, $id, $ref_text)) {
return $id;
}
}
return false;
}
function db_post_update_post($db, $id) {
$date_created = null;
$date_modified = date('Y-m-d H:i:s', time());
$search = array(
'id' => $id
);
$entry = array(
'id' => null,
'domain' => null,
'sub_reply' => null,
'date_created' => $date_created,
'date_modified' => $date_modified,
'flags' => null
);
// remove false, null and other zero-values from entry
$entry = array_filter($entry);
if (db_update($db, 'post', $entry, $search)) {
return $id;
}
return false;
}
function db_post_select_id($db, $id = false) {
if ($id == false) {
return false;
}
$value = array(
'id' => $id
);
$query = '
SELECT * FROM post
INNER JOIN post_history AS initial ON (initial.sub_post = post.id AND initial.id = (SELECT id FROM post_history WHERE sub_post = post.id ORDER BY id ASC LIMIT 1))
INNER JOIN post_history AS current ON (current.sub_post = post.id AND current.id = (SELECT id FROM post_history WHERE sub_post = post.id ORDER BY id DESC LIMIT 1))
INNER JOIN post_text ON post_text.id = current.ref_text
WHERE post.id = :id
';
$query = db_run($db, $query, $value);
$row = $query->fetch();
$row = post_row($row);
return $row;
}
function db_post_select_page($db, $page = false, $limit = 10) {
$pages = (db_post_select_count($db) / $limit);
$pages = ceil($pages);
if ($page == false) {
$page = $pages;
}
elseif ($page > $pages) {
$page = $pages;
}
elseif ($page < 1) {
$page = $pages;
}
$offset = ($page - 1) * $limit;
// should I make it more stronger?
// escape offset and limit
$query = '
SELECT * FROM post
INNER JOIN post_history AS initial ON (initial.sub_post = post.id AND initial.id = (SELECT id FROM post_history WHERE sub_post = post.id ORDER BY id ASC LIMIT 1))
INNER JOIN post_history AS current ON (current.sub_post = post.id AND current.id = (SELECT id FROM post_history WHERE sub_post = post.id ORDER BY id DESC LIMIT 1))
INNER JOIN post_text ON post_text.id = current.ref_text
ORDER BY id DESC LIMIT ' . $limit . ' OFFSET ' . $offset . ';
';
$query = db_query($db, $query);
$array = array();
while ($row = $query->fetch()) {
$array[] = post_row($row);
}
return $array;
}
function post_row($row) {
if ($row == false) {
return false;
}
if ($row['date_created'] == null) {
$row['date_created'] = '(unavailable)';
}
if ($row['date_modified'] == null) {
$row['date_modified'] = '(unavailable)';
}
if (empty($row['text_headers'])) {
$row['text_headers'] = array();
}
else {
$row['text_headers'] = unserialize($row['text_headers']);
}
if (empty($row['text_headers']['title'])) {
$row['text_headers']['title'] = '';
}
return $row;
}
function post_escape($text) {
$text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
$text = nl2br($text);
return $text;
}
function post_input_filtered($text) {
$text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
return $text;
}
function post_text_filtered($text) {
$text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
return $text;
}
session.noclass.php
<?php
function session_directory() {
return dirname($_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR . 'db' . DIRECTORY_SEPARATOR . 'sess';
}
function session_open($session_name = 'ident') {
$session_id = session_open_if_cookie_is_set($session_name);
if ($session_id == false) {
$session_id = session_new($session_name);
}
return $session_id;
}
function session_open_if_cookie_is_set($session_name = 'ident') {
if (isset($_COOKIE[$session_name])) {
$session_id = $_COOKIE[$session_name];
if (strlen(str_replace(array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'), '', $session_id)) > 0) {
$session_id = session_new($session_name);
}
return $session_id;
}
return false;
}
function session_load($session_id) {
$session_file = session_directory() . DIRECTORY_SEPARATOR . $session_id;
$session_data = false;
if ($fp = @fopen($session_file, 'r')) {
flock($fp, LOCK_SH);
$session_data = fread($fp, filesize($session_file));
$session_data = unserialize($session_data);
flock($fp, LOCK_UN);
fclose($fp);
}
if (is_array($session_data)) {
return $session_data;
}
return array();
}
function session_save($session_id, $session_data = array()) {
$session_file = session_directory() . DIRECTORY_SEPARATOR . $session_id;
$session_data = serialize($session_data);
if ($fp = @fopen($session_file, 'w+')) {
flock($fp, LOCK_EX);
$bytes_written = fwrite($fp, $session_data);
flock($fp, LOCK_UN);
fclose($fp);
}
return $bytes_written;
}
function session_new($session_name) {
if (headers_sent()) {
return false;
}
$session_id = bin2hex(session_random_bytes(8));
$cookie_lifetime = 0;
$cookie_path = '/';
$cookie_domain = '';
$cookie_secure = false;
$cookie_httponly = false;
if (setcookie($session_name, $session_id, $cookie_lifetime, $cookie_path, $cookie_domain, $cookie_secure, $cookie_httponly) == false) {
return false;
}
return $session_id;
}
function session_random_bytes($length = 32) {
$random_bytes = '';
for ($i = 0; $i < $length; $i++) {
$random_bytes .= chr(rand(0, 255));
}
return $random_bytes;
}
function session() {
$id = session_open();
if ($id == false) {
return false;
}
$session = session_load($id);
return $session;
}
вот вам небольшая инструкция как использовать сессии
вы просто делаете $session = session(); и создаётся новая сессия и в переменную $session загружается массив со всеми данными сессии.
если вы хотите что-то сохранить в сессию, то вы должны проделать все действия вручую:
// создаём новую сессию
// в $id помещается идентификатор сессии, ну типа PHPSESSID
$id = session_open();
// загружаем данные сессии в переменную $session
// можно для этого использовать $_SESSION но я не хочу и не буду
$session = session_load($id);
// создаём в $session приветствие
$session = array('greeting' => 'hello world!');
// сохраняем данные сессии, за вас это никто не сделает
session_save($id, $session);
есть такой забавный момент, когда мы не хотим создавать сессию, если у пользователя её нету. для этого есть функция session_open_if_cookie_is_set();, которая запускает сессию только при наличии куки.
в целом моя реализация сессий лучше пхпшной т.к. она не создаёт пустых файлов!!! каждый момент вы контролируете сами!!!
теперь давайте я вам расскажу про сообщения а-ля википедия или лор.
для отправки сообщения я делаю так:
$headers = array(
'title' => 'заголовок письма'
);
$content = 'текст письма';
post_insert($headers, $content);
как вы знаете у сообщения есть не только его содержимое, не только сам текст сообщения. у сообщения есть мета-данные, такие как заголовок, тэги, ну ещё что-нибудь может быть. поэтому все эти мета-данные помещаются в отдельный массив.
затем при выборке сообщения из базы мы можем сравить, сделать diff не только на сам текст сообщения, но и на все заголовки тоже сделать diff и увидеть различия между двумя версиями сообщения включая всю мета-информацию, — заголовки, теги. теперь вы понимаете почему я решил так сделать.
функция post_insert(); вернёт просто true в случае узбека. иначе false.
для того, чтобы отредактировать сообщение, есть функция post_update();, но первым параметром ещё нужно указать ID сообщения.
например мы только что добавили сообщение, его ID 1, теперь мы хотим его отредактировать.
$id = '1';
$headers = array(
'title' => 'заголовок письма (отредактировано)'
);
$content = 'исправленный текст письма';
post_update($id, $headers, $content);
и теперь в базу будет сохранена новая версия сообщения.
выполняем sqlite example.db .dump и смотрим
INSERT INTO post VALUES(1,NULL,NULL,'2022-07-17 06:24:55','2022-07-17 06:25:26',NULL);
INSERT INTO post_history VALUES(1,1,1,'2022-07-17 06:24:55',NULL);
INSERT INTO post_history VALUES(2,1,2,'2022-07-17 06:25:26',NULL);
INSERT INTO post_text VALUES(1,'текст письма','a:1:{s:5:"title";s:31:"заголовок письма";}');
INSERT INTO post_text VALUES(2,'исправленный текст письма','a:1:{s:5:"title";s:64:"заголовок письма (отредактировано)";}');
по факту сообщение одно, а ревизии у него две! так-то!
для того, чтобы прочитать сообщение в массив мы подключаемся к базе данных и выполняем одну функцию, указав ID сообщения
$db = db('example.db');
if ($db == false) {
return false;
}
$post = db_post_select_id($db, $_GET['id']);
var_dump($post);
для того, чтобы вывести страницу со списком сообщений есть функция db_post_select_page($db, $_GET['page']);
в целом мой код будет очень хорошо документирован, но это чуть попозже, пока только реализация.
мой код написан очень качественно на совесть, писал для себя.
очень скоро будет релиз и я создаю отдельный сайт посвящённый своей цмс на пхп четыре. оцените качество кода, насколько он красив и хорош, и очень строгий!
главный вопрос — выдержит ли такая структура базы данных на SQLite нагрузку с бесконечным редактированием сообщений???