Сервисы
Полезное
Портфолио
Блог
С помощью данного кода вы можете с лёгкостью подтягивать данные из MySQL и PostgreSQL базы на сайты созданные в Creatium.
Авторы творения
Создатель Марк Гаджимурадов: https://t.me/m_ivor
Доработал Вечкасов Кирилл: https://t.me/pomogay_marketing
API предоставляет универсальный интерфейс для работы с базами данных PostgreSQL и MySQL через HTTP-запросы с расширенными возможностями фильтрации, сортировки и типизации данных.
$dbType = 'pgsql'; // 'pgsql' или 'mysql'
$secretHash = 'c9b939a42d2f5d76196661333b9d0e9'; // секретный ключ доступа
$enableCache = false; // включить общее кеширование
$enableAlwaysCache = false; // принудительное кеширование для указанных таблиц
$alwaysCacheTables = ''; // таблицы для принудительного кеша, если у вас psql база, указываете полный путь до таблицы public.table_1, public.table_2
$cacheHours = 0; // время жизни кеша в часах (0 = бессрочно)
Обязательный параметр: hash
в URL
GET /api.php?table=my_table&hash=c9b939a42d2f5d7619666174eeb9d0e9
Без корректного hash-кода доступ к данным заблокирован.
table
(обязательный) - имя таблицы для запросаlimit
- максимальное количество записейskip
- количество записей для пропуска (offset)GET /api.php?table=users&hash=YOUR_HASH&limit=10&skip=20
Фильтры передаются в теле POST-запроса в формате JSON:
{
"properties": {
"name": "John",
"age>=": 18,
"status!=": "deleted"
}
}
Оператор | Описание | Пример | SQL эквивалент |
---|---|---|---|
= | Равенство | "name": "John" | name = 'John' |
!= | Неравенство | "status!=": "deleted" | status != 'deleted' |
> | Больше | "age>": 18 | age > 18 |
< | Меньше | "age<": 65 | age < 65 |
>= | Больше или равно | "age>=": 18 | age >= 18 |
<= | Меньше или равно | "age<=": 65 | age <= 65 |
~ | Содержит | "name~": "John" | name LIKE '%John%' |
!~ | Не содержит | "name!~": "spam" | name NOT LIKE '%spam%' |
^ | Начинается с | "name^": "John" | name LIKE 'John%' |
!^ | Не начинается с | "name!^": "spam" | name NOT LIKE 'spam%' |
& | Заканчивается на | "email&": ".com" | email LIKE '%.com' |
!& | Не заканчивается на | "email!&": ".test" | email NOT LIKE '%.test' |
@ | IN (список значений) | "status@": "1,2,3" | status IN (1,2,3) |
$ | DISTINCT | "category$": "" | SELECT DISTINCT category |
{
"name!=": "" // Исключает пустые строки И NULL значения
}
Генерирует: name != '' AND name IS NOT NULL
Одинаковые операторы автоматически группируются:
{
"status": ["active", "pending"],
"category": ["news", "blog"]
}
Генерирует: status IN ('active', 'pending') AND category IN ('news', 'blog')
{
"properties": {
"category$": "any_value" // значение может быть пустым
}
}
Результат: SELECT DISTINCT category FROM table
По умолчанию все условия объединяются через AND. Для создания OR-групп используйте директивы:
{
"properties": {
"name": "John",
"age>=": 18,
"FILTER:OR": "",
"status": "vip",
"balance>": 1000,
"FILTER:AND": "",
"created>=": "2024-01-01"
}
}
Результат: (name = 'John' AND age >= 18) OR (status = 'vip' AND balance > 1000) AND (created >= '2024-01-01')
Сортировка задается в JSON теле запроса:
{
"sort": [
{"column": "created_at", "direction": "DESC"},
{"column": "name", "direction": "ASC"}
]
}
GET /api.php?table=users&limit=10&skip=20&hash=YOUR_HASH
{
"limit": 10,
"skip": 20
}
Система автоматически определяет типы столбцов на основе названий, комментариев и содержимого.
type: "image"
)Условия определения:
Примеры столбцов:
avatar_img
product_image
cover_img
thumbnail_image
Обработка данных:
// Было: "https://example.com/image.jpg"
// Стало:
{
"file": "https://example.com/image.jpg",
"size": [1920, 1080]
}
Поддерживаемые форматы: jpg, jpeg, png, webp, gif
type: "markdown"
)Условия определения:
Примеры столбцов:
content_markdown
description_markdown
markdown_text
post_markdown
String - текстовые данные (по умолчанию)
Number - числовые данные (int, float)
Boolean - логические значения
Система автоматически извлекает комментарии из метаданных базы данных:
PostgreSQL: Использует col_description()
MySQL: Использует
INFORMATION_SCHEMA.COLUMNS
Комментарии используются для:
{
"columns": [
{
"id": "name",
"name": "Имя пользователя",
"type": "string",
"comment": "Имя пользователя"
},
{
"id": "avatar_img",
"name": "avatar_img",
"type": "image",
"comment": ""
}
],
"rows": [
{
"id": 1,
"name": "John Doe",
"avatar_img": {
"file": "https://example.com/avatar.jpg",
"size": [1920, 1080]
}
}
],
"totalCount": 156
}
curl -X GET "https://yoursite.com/api.php?table=users&hash=YOUR_HASH"
curl -X POST "https://yoursite.com/api.php?table=users&hash=YOUR_HASH&limit=10" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"status": "active",
"age>=": 18
},
"sort": [
{"column": "created_at", "direction": "DESC"}
]
}'
curl -X POST "https://yoursite.com/api.php?table=posts&hash=YOUR_HASH" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"category$": "1"
}
}'
curl -X POST "https://yoursite.com/api.php?table=products&hash=YOUR_HASH" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"price>=": 100,
"FILTER:OR": "",
"category": "electronics",
"brand~": "Apple"
}
}'
Система поддерживает гибкое кеширование:
$enableCache = true;
$cacheHours = 1; // кеш на 1 час
$noCacheTables = 'logs,temp_data'; // эти таблицы не кешируются
$enableAlwaysCache = true;
$alwaysCacheTables = 'static_content,settings'; // всегда кешируются
Система создает лог-файлы для отладки:
input_json.log
- входящие JSON запросыdebug_sql.log
- сгенерированные SQL запросы и параметры{"error": "Table name is required."}
HTML страница с сообщением "Для загрузки данных введите секретный ключ"
{"error": "Database connection failed"}
10 июня 2025
Все пустые записи возвращают состояние null, так как из-за пустых ячеек где отсутствовал текст и статус ячейки был не NULL ломались формы загрузки картинок.
<?php
/* ===================== Конфигурация ===================== */
// Тип подключения к базе данных: 'mysql' или 'pgsql'
$dbType = 'mysql'; // изменить на 'pgsql' для подключения к PostgreSQL
// Секретный HASH код для доступа к данным
$secretHash = '****';
/* ====================================================== */
// Проверка наличия и корректности секретного HASH
if (!isset($_GET['hash']) || $_GET['hash'] !== $secretHash) {
echo '<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Ошибка доступа</title>
<style>
body { display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #f9f9f9; }
.message { font-size: 2em; text-align: center; color: #333; }
</style>
</head>
<body>
<div class="message">Для загрузки данных введите секретный ключ</div>
</body>
</html>';
exit;
}
// ===================== Настройки кеширования =====================
$enableCache = false;
$noCacheTables = '';
$cacheHours = 0;
$enableAlwaysCache = false;
$alwaysCacheTables = '****';
// =================================================================
// Параметры подключения к базе данных
$host = '****';
$username = '****';
$password = '****';
$dbname = ($dbType === 'pgsql') ? 'public' : 'mysql';
$port = ($dbType === 'pgsql') ? '5432' : '3306';
// Получаем имя таблицы из GET-параметра 'table'
$table = $_GET['table'] ?? null;
if (!$table) {
http_response_code(400);
echo json_encode(['error' => 'Table name is required.']);
exit;
}
// Подготовка списков кеширования
$noCacheTableList = array_map('trim', explode(',', $noCacheTables));
$alwaysCacheTableList = array_map('trim', explode(',', $alwaysCacheTables));
$shouldUseCache =
($enableCache && !in_array($table, $noCacheTableList))
|| ($enableAlwaysCache && in_array($table, $alwaysCacheTableList));
/**
* Определение типа столбца
*/
function getColumnType($value) {
if (is_bool($value)) return 'string';
if (is_int($value) || is_float($value)) return 'number';
return 'string';
}
/**
* Получение комментариев столбцов из базы данных
*/
function getColumnComments($pdo, $table, $dbType) {
$comments = [];
try {
if ($dbType === 'pgsql') {
// Для PostgreSQL
$parts = explode('.', $table);
if (count($parts) === 2) {
$schema = $parts[0];
$tableName = $parts[1];
} else {
$schema = 'public';
$tableName = $table;
}
$sql = "
SELECT
c.column_name,
COALESCE(col_description(pgc.oid, c.ordinal_position), '') as comment
FROM information_schema.columns c
LEFT JOIN pg_class pgc ON pgc.relname = c.table_name
LEFT JOIN pg_namespace pgn ON pgn.oid = pgc.relnamespace
WHERE c.table_schema = :schema
AND c.table_name = :table_name
AND (pgn.nspname = :schema2 OR pgn.nspname IS NULL)
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'schema' => $schema,
'table_name' => $tableName,
'schema2' => $schema
]);
} else {
// Для MySQL
$parts = explode('.', $table);
if (count($parts) === 2) {
$database = $parts[0];
$tableName = $parts[1];
} else {
// Получаем текущую базу данных
$dbResult = $pdo->query("SELECT DATABASE()")->fetchColumn();
$database = $dbResult ?: 'mysql';
$tableName = $table;
}
$sql = "
SELECT
COLUMN_NAME as column_name,
COALESCE(COLUMN_COMMENT, '') as comment
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = :database
AND TABLE_NAME = :table_name
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'database' => $database,
'table_name' => $tableName
]);
}
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$comments[$row['column_name']] = $row['comment'] ?: '';
}
} catch (Exception $e) {
// В случае ошибки возвращаем пустой массив и логируем
error_log("Error in getColumnComments: " . $e->getMessage());
return [];
}
return $comments;
}
// Читаем JSON из тела запроса
$input = json_decode(file_get_contents('php://input'), true) ?: [];
// Сразу же логируем его в отдельный файл
$logFile = __DIR__ . '/input_json.log';
$logEntry = date('c')
. " | INPUT JSON: " . json_encode($input, JSON_UNESCAPED_UNICODE)
. "\n";
file_put_contents($logFile, $logEntry, FILE_APPEND);
// Блок кеширования
if ($shouldUseCache) {
$cacheDir = __DIR__ . '/cache_treba_online_ru';
if (!file_exists($cacheDir)) mkdir($cacheDir, 0777, true);
$cacheKey = md5(json_encode($_GET) . json_encode($input));
$cacheFile = "$cacheDir/{$cacheKey}.json";
$cacheValid = $cacheHours === 0
? file_exists($cacheFile)
: (file_exists($cacheFile) && time() - filemtime($cacheFile) < $cacheHours * 3600);
if ($cacheValid) {
header('Content-Type: application/json');
echo file_get_contents($cacheFile);
exit;
}
}
try {
// Подключение к БД
if ($dbType === 'pgsql') {
$dsn = "pgsql:host={$host};port={$port};dbname={$dbname}";
} else {
$dsn = "mysql:host={$host};port={$port};dbname={$dbname};charset=utf8";
}
$pdo = new PDO($dsn, $username, $password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
// Получаем комментарии столбцов (с обработкой ошибок)
$colComments = [];
try {
$colComments = getColumnComments($pdo, $table, $dbType);
} catch (Exception $e) {
// Логируем ошибку, но продолжаем работу без комментариев
error_log("Error getting column comments: " . $e->getMessage());
}
$params = [];
$distinctColumns = [];
$groups = [];
$currentGroup = ['joiner' => 'AND', 'conditions' => [], 'grouped' => []];
$globalCounter = 0;
if (!empty($input['properties']) && is_array($input['properties'])) {
foreach ($input['properties'] as $property => $value) {
// GROUP directives FILTER:AND/FILTER:OR
if (stripos($property, 'FILTER:') === 0) {
$directive = strtoupper(substr($property, 7));
if (in_array($directive, ['AND', 'OR'])) {
// flush grouped conditions
foreach ($currentGroup['grouped'] as $col => $ops) {
foreach ($ops as $op => $vals) {
if (count($vals) > 1) {
$pls = [];
foreach ($vals as $v) {
$globalCounter++;
$ph = "{$col}_" . str_replace(['=', '!'], '', $op) . "_{$globalCounter}";
$pls[] = ":{$ph}";
$params[$ph] = $v;
}
$inOp = $op === '=' ? 'IN' : 'NOT IN';
$currentGroup['conditions'][] = "{$col} {$inOp} (" . implode(', ', $pls) . ")";
} else {
$globalCounter++;
$ph = "{$col}_" . str_replace(['=', '!'], '', $op) . "_{$globalCounter}";
$currentGroup['conditions'][] = "{$col} {$op} :{$ph}";
$params[$ph] = $vals[0];
}
}
}
if (!empty($currentGroup['conditions'])) $groups[] = $currentGroup;
$currentGroup = ['joiner' => $directive, 'conditions' => [], 'grouped' => []];
continue;
}
}
// parse operator and column
$cleanKey = preg_replace('/\d+$/', '', $property);
// Список всех операторов, добавили '@'
$ops = ['>=','<=','!=','!~','!^','!&','!%','>','<','=','~','^','&','$','#','%','@'];
$operator = '=';
$column = $cleanKey;
foreach ($ops as $op) {
if (substr($cleanKey, -strlen($op)) === $op) {
$operator = $op;
$column = substr($cleanKey, 0, -strlen($op));
break;
}
}
// Новый оператор '@': разбивает строку "1,2,3" на три отдельных условия AND
if ($operator === '@') {
// если пришло не array, а строка — разбиваем по запятым
$values = is_array($value) ? $value : explode(',', $value);
$placeholders = [];
foreach ($values as $v) {
$v = trim($v);
$globalCounter++;
$ph = "{$column}_at_{$globalCounter}";
$placeholders[] = ":{$ph}";
$params[$ph] = $v;
}
// добавляем одно условие IN (...)
$currentGroup['conditions'][] = "{$column} IN (" . implode(', ', $placeholders) . ")";
// и пропускаем дальнейшую логику для этого оператора
continue;
}
// special: != empty => exclude empty & NULL
if ($operator === '!=' && ($value === '' || $value === null)) {
$currentGroup['conditions'][] = "{$column} != ''";
$currentGroup['conditions'][] = "{$column} IS NOT NULL";
continue;
}
// skip empty only for '='
$isEmptyScalar = !is_array($value) && ($value === '' || $value === null);
$isEmptyArray = is_array($value) && empty($value);
if ($operator === '=' && ($isEmptyScalar || $isEmptyArray)) {
continue;
}
// DISTINCT
if ($operator === '$') {
// Проверяем, что значение не пустое
if (!empty($value) || $value === 0 || $value === '0') {
$distinctColumns[] = $column;
}
continue;
}
// '=' and '!=' grouping
if (in_array($operator, ['=', '!='])) {
$split = is_array($value) ? $value : [$value];
foreach ($split as $v) {
$currentGroup['grouped'][$column][$operator][] = $v;
}
continue;
}
// other operators
$globalCounter++;
$ph = "{$column}_" . preg_replace('~[^A-Za-z0-9]~', '', $operator) . "_{$globalCounter}";
switch ($operator) {
case '~':
$currentGroup['conditions'][] = "{$column} LIKE :{$ph}";
$params[$ph] = "%{$value}%";
break;
case '!~':
$currentGroup['conditions'][] = "{$column} NOT LIKE :{$ph}";
$params[$ph] = "%{$value}%";
break;
case '^':
$currentGroup['conditions'][] = "{$column} LIKE :{$ph}";
$params[$ph] = "{$value}%";
break;
case '!^':
$currentGroup['conditions'][] = "{$column} NOT LIKE :{$ph}";
$params[$ph] = "{$value}%";
break;
case '&':
$currentGroup['conditions'][] = "{$column} LIKE :{$ph}";
$params[$ph] = "%{$value}";
break;
case '!&':
$currentGroup['conditions'][] = "{$column} NOT LIKE :{$ph}";
$params[$ph] = "%{$value}";
break;
default:
$currentGroup['conditions'][] = "{$column} {$operator} :{$ph}";
$params[$ph] = $value;
}
}
// flush last grouped
foreach ($currentGroup['grouped'] as $col => $ops) {
foreach ($ops as $op => $vals) {
if (count($vals) > 1) {
$pls = [];
foreach($vals as $v) {
$globalCounter++;
$ph = "{$col}_" . str_replace(['=', '!'], '', $op) . "_{$globalCounter}";
$pls[] = ":{$ph}";
$params[$ph] = $v;
}
$inOp = $op === '=' ? 'IN' : 'NOT IN';
$currentGroup['conditions'][] = "{$col} {$inOp} (" . implode(', ', $pls) . ")";
} else {
$globalCounter++;
$ph = "{$col}_" . str_replace(['=', '!'], '', $op) . "_{$globalCounter}";
$currentGroup['conditions'][] = "{$col} {$op} :{$ph}";
$params[$ph] = $vals[0];
}
}
}
if (!empty($currentGroup['conditions'])) $groups[] = $currentGroup;
}
// build WHERE
$where = '';
$clauses = [];
foreach ($groups as $g) {
if (count($g['conditions']) > 1) {
$clauses[] = '(' . implode(" {$g['joiner']} ", $g['conditions']) . ')';
} elseif (count($g['conditions']) === 1) {
$clauses[] = $g['conditions'][0];
}
}
if ($clauses) $where = 'WHERE ' . implode(' AND ', $clauses);
// sorting/limit/offset
$orderBy = '';
if (!empty($input['sort']) && is_array($input['sort'])) {
$parts = [];
foreach ($input['sort'] as $s) {
$parts[] = "{$s['column']} {$s['direction']}";
}
$orderBy = 'ORDER BY ' . implode(', ', $parts);
}
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : ($input['limit'] ?? 0);
$limitClause = $limit ? "LIMIT {$limit}" : '';
$offset = isset($input['skip'])
? (int)$input['skip']
: (isset($_GET['skip']) ? (int)$_GET['skip'] : 0);
$offsetClause = $offset ? "OFFSET {$offset}" : '';
// SELECT vs DISTINCT
if (!empty($distinctColumns)) {
$select = 'SELECT DISTINCT ' . implode(', ', $distinctColumns);
$countQ = "SELECT COUNT(*) FROM (SELECT DISTINCT " . implode(', ', $distinctColumns) . " FROM {$table} {$where}) as sub";
} else {
$select = 'SELECT *';
$countQ = "SELECT COUNT(*) FROM {$table} {$where}";
}
// get totalCount
$cstm = $pdo->prepare($countQ);
$cstm->execute($params);
$totalCount = (int)$cstm->fetchColumn();
// main query
$sql = "{$select} FROM {$table} {$where} {$orderBy} {$limitClause} {$offsetClause}";
$stm = $pdo->prepare($sql);
$stm->execute($params);
// DEBUG: записываем сгенерированный SQL и параметры
$debugInfo = [
'sql' => $sql,
'params' => $params,
'distinctColumns' => $distinctColumns,
'input' => $input
];
file_put_contents(__DIR__.'/debug_sql.log',
date('Y-m-d H:i:s') . "\n" .
"SQL: " . $sql . "\n" .
"PARAMS: " . print_r($params, true) .
"DISTINCT: " . print_r($distinctColumns, true) .
"INPUT: " . print_r($input, true) .
"\n" . str_repeat("-", 80) . "\n\n",
FILE_APPEND
);
$items = $stm->fetchAll(PDO::FETCH_ASSOC);
// Преобразование пустых строк в null
foreach ($items as &$row) {
foreach ($row as $colName => &$value) {
// Проверяем, является ли значение пустой строкой
if (is_string($value) && trim($value) === '') {
$value = null;
}
}
}
unset($row, $value); // разрываем ссылки после foreach
/**
* === НОВАЯ ЧАСТЬ: обработка url-ов изображений ===
* Список форматов, которые мы считаем «изображением»:
* jpg, jpeg, png, webp, gif
*/
$imageExtensions = ['jpg','jpeg','png','webp','gif'];
$extPattern = implode('|', $imageExtensions);
$regexImageUrl = '/^https?:\/\/.+\.(' . $extPattern . ')$/i';
foreach ($items as &$row) {
foreach ($row as $colName => $value) {
if (is_string($value) && preg_match($regexImageUrl, $value)) {
// Заменяем строку на вложенный массив с ключами "file" и "size"
$row[$colName] = [
'file' => $value,
'size' => [1920, 1080]
];
}
}
}
unset($row); // разрываем ссылку после foreach
// columns meta с учетом комментариев
$columns = [];
if ($items) {
// Перебираем все имена столбцов на основании ключей первой строки
foreach (array_keys($items[0]) as $colName) {
// Комментарий для этого столбца (или пустая строка, если его нет)
$commentText = $colComments[$colName] ?? '';
// Для нечувствительного к регистру поиска
$commentLower = mb_strtolower($commentText);
// Определяем тип в зависимости от слов 'markdown' или 'image' в комментарии
// или если в названии столбца есть 'img' или 'image'
$colNameLower = mb_strtolower($colName);
if (strpos($commentLower, 'markdown') !== false) {
$colType = 'markdown';
} elseif (strpos($commentLower, 'image') !== false ||
strpos($colNameLower, 'img') !== false ||
strpos($colNameLower, 'photo') !== false ||
strpos($colNameLower, 'foto') !== false ||
strpos($colNameLower, 'image') !== false) {
$colType = 'image';
} else {
// Стандартный способ: string/number/boolean и т.д.
$colType = getColumnType($items[0][$colName]);
}
// Если комментарий непустой — используем его как отображаемое имя,
// иначе берём само имя столбца
$displayName = $commentText !== '' ? $commentText : $colName;
$columns[] = [
'id' => $colName,
'name' => $displayName,
'type' => $colType,
'comment' => $commentText
];
}
}
// Формируем финальный ответ
$response = [
'columns' => $columns,
'rows' => $items,
'totalCount' => $totalCount
];
// Сохраняем в кеш если нужно
if ($shouldUseCache) {
$json = json_encode($response);
file_put_contents($cacheFile, $json);
}
header('Content-Type: application/json');
echo json_encode($response);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
?>
Хватит читать советы «для всех»
Подписывайся — рассказываю, как маркетинг работает на практике, с цифрами и кейсами.
Без воды, прямо и честно.
Сервисы
Полезное
Портфолио
Портфолио
ИП Вечкасов Кирилл Александрович, ИНН: 860326713173, ОГРН: 323784700359197