+7 (958) 580-59-59

на связи с 9:00 до 21:00

Коннектор для Creatium

С помощью данного кода вы можете с лёгкостью подтягивать данные из MySQL и PostgreSQL базы на сайты созданные в Creatium.

Авторы творения

Создатель Марк Гаджимурадов: https://t.me/m_ivor

Доработал Вечкасов Кирилл: https://t.me/pomogay_marketing

📊 Руководство по работе с таблицами в Creatium

💡 Это руководство объясняет: как использовать блок "Таблица" в Creatium для фильтрации, сортировки и отображения данных из базы данных через графический интерфейс.

1. 🔌 Подключение таблицы

Шаг 1: Добавьте блок "Таблица"

Перетащите блок "Таблица" на страницу из панели компонентов

Шаг 2: Настройте подключение к данным

В настройках блока выберите "Подключение к таблице" и укажите:

  • URL: путь к вашему PHP скрипту
  • Таблица: имя таблицы в базе данных (например: "Двери")
⚠️ Важно! Убедитесь, что ваш PHP скрипт доступен и правильно настроен с секретным ключом.

2. 🔍 Настройка фильтров

Интерфейс фильтров в Creatium:

1 ID товара 2 Равняется 3 [значение] {x} ✕
💡 Важно! В Creatium операторы добавляются к названию параметра, например: id=, name~, price>

Элементы фильтра:

  1. Параметр - название поля + оператор (например: price>, name~)
  2. Оператор - указывается в конце названия параметра
  3. Значение - что искать

Добавление фильтра:

  1. Нажмите кнопку "Добавить фильтр"
  2. В поле "Параметр" введите название поля + оператор (например: price>)
  3. В поле "Значение" введите искомое значение
  4. Нажмите "Применить" для активации фильтра

3. 🔧 Операторы фильтрации

Операторы добавляются к названию поля в параметре. Если оператор не указан, используется = (равно).

Оператор Как писать Описание Пример параметра Пример значения
= id= или просто id Точное совпадение status активный
!= поле!= Не равно status!= удалено
> поле> Больше price> 1000
< поле< Меньше age< 18
>= поле>= Больше или равно score>= 80
<= поле<= Меньше или равно discount<= 50
~ поле~ Содержит текст description~ качественный
!~ поле!~ Не содержит текст title!~ тест
^ поле^ Начинается с name^ Алекс
!^ поле!^ Не начинается с email!^ admin
& поле& Заканчивается на file& .pdf
!& поле!& Не заканчивается на file!& .tmp

4. ⭐ Специальные операторы

@ - Множественный выбор

Как использовать:

  • Параметр: status@
  • Значение: активный,в_работе,завершен

Результат: Покажет записи, где статус равен любому из указанных значений

$ - Уникальные значения

Как использовать:

  • Параметр: user_id$
  • Значение: 1 (или да, true)

Результат: Покажет только уникальные записи по полю user_id

Группировка фильтров (И/ИЛИ)

По умолчанию все фильтры работают с логикой "И" (AND) - должны выполняться ВСЕ условия.

Для логики "ИЛИ" добавьте специальный фильтр:

  • Параметр: FILTER:OR
  • Значение: 1

5. 📊 Сортировка данных

Блок сортировки:

Сортировка

ID товара ▼ По убыванию ▼

Настройка сортировки:

  1. Нажмите "Добавить сортировку"
  2. Выберите поле для сортировки
  3. Выберите направление:
    • По возрастанию - от меньшего к большему (A-Z, 1-9)
    • По убыванию - от большего к меньшему (Z-A, 9-1)
💡 Совет: Можно добавить несколько сортировок. Они будут применяться в порядке добавления.

6. 📄 Пагинация

Настройки пагинации:


50

0

Параметры пагинации:

  • Количество записей - сколько записей показать на странице (например: 50)
  • Пропуск записей - сколько записей пропустить с начала (для постраничной навигации)

Пример для второй страницы:

  • Количество записей: 50
  • Пропуск записей: 50 (пропустить первые 50 записей)

7. 💡 Практические примеры

Пример 1: Поиск активных товаров дороже 5000 рублей

Фильтры:
  • Параметр: statusЗначение: активный
  • Параметр: price>Значение: 5000
Сортировка:
  • Поле: price → Направление: По убыванию

Пример 2: Поиск дверей определенных типов

Фильтры:
  • Параметр: type@Значение: межкомнатные,входные,балконные
  • Параметр: status!=Значение: удалено

Пример 3: Поиск товаров с описанием

Фильтры:
  • Параметр: description~Значение: качественный
  • Параметр: description!~Значение: тест

Пример 4: Логика ИЛИ - товары либо дешевые, либо со скидкой

Фильтры:
  • Параметр: price<Значение: 1000
  • Параметр: FILTER:ORЗначение: 1
  • Параметр: discount>Значение: 0

Результат: Товары с ценой меньше 1000 ИЛИ с любой скидкой

8. 🔧 Решение проблем

❌ Таблица не загружается

Возможные причины:
  • Неправильно указан URL к PHP скрипту
  • Ошибка в названии таблицы
  • Проблемы с подключением к базе данных
Решение: Проверьте URL и имя таблицы в настройках блока

❌ Фильтры не работают

Возможные причины:
  • Включена опция "Игнорировать фильтры с пустыми значениями" и поле пустое
  • Неправильно выбран оператор для типа данных
  • Ошибка в написании значения
Решение: Проверьте настройки фильтров и значения

❌ Показываются неправильные данные

Возможные причины:
  • Неправильная настройка операторов
  • Конфликт между несколькими фильтрами
  • Неправильно настроена группировка (И/ИЛИ)
Решение: Поочередно отключайте фильтры для поиска проблемного

✅ Полезные советы

  • Тестируйте фильтры по одному - добавляйте по одному фильтру и проверяйте результат
  • Используйте понятные значения - убедитесь, что значения в фильтрах точно соответствуют данным в БД
  • Проверяйте регистр - для текстовых полей важен регистр букв
  • Кешируйте результаты - для больших таблиц включите кеширование в PHP скрипте

🎯 Готово!

Теперь вы знаете, как эффективно работать с таблицами в Creatium. Используйте операторы в названиях параметров для создания мощных фильтров!

🚀 Быстрая шпаргалка операторов:

Базовые: id= id!= price> age< score>= discount<=
Текстовые: name~ title!~ code^ email!^ file& url!&
Специальные: status@ (выбор) user_id$ (уникальные) FILTER:OR (логика)

<

Обновления

10 июня 2025

Все пустые записи возвращают состояние null, так как из-за пустых ячеек где отсутствовал текст и статус ячейки был не NULL ломались формы загрузки картинок.

10 июля 2025

Для выделения только уникальных строк оператором $ требуется вместо value прописывать true, чтобы активировался тег вывода только уникальных строку.

30 июля 2025

Технические доработки. Повышена стабильность.

20 сентября 2025

Скорректирован код, убраны опечатки, обновлён мануал работы с коннектором.

КОД ДЛЯ КОПИРОВАНИЯ

<?php
/* ===================== Конфигурация ===================== */
// Тип подключения к базе данных: 'mysql' или 'pgsql'
$dbType = 'pgsql'; // изменить на 'mysql' для подключения к MySQL

// Секретный HASH код для доступа к данным
$secretHash = 'asdkjaskjda8sd78asd9';

// ===================== Настройки S3 =====================
$s3Config = [
    'endpoint' => 'https://****',
    'bucket' => '****',
    'access_key' => '****',
    'secret_key' => '****',
    'region' => 'ru-1' // или другой регион
];

// ===================== Настройки кеширования =====================
$enableCache       = false; // включен для демонстрации S3
$cacheStorage      = 's3'; // 'local' или 's3'
$noCacheTables     = '';
$cacheHours        = 2; // кешируем на 2 часа
$enableAlwaysCache = false;
$alwaysCacheTables = '****';
/* ====================================================== */

// Проверка наличия и корректности секретного 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;
}

// Параметры подключения к базе данных
$host     = 'host';
$username = 'username';
$password = 'password';
$dbname   = ($dbType === 'pgsql') ? 'public' : 'mysql';
$port     = ($dbType === 'pgsql') ? '5432' : '3306';

/**
 * Класс для работы с S3 кешем
 */
class S3Cache {
    private $config;
    private $cachePrefix = 'cache/';
    private $dbname;
    
    public function __construct($config) {
        $this->config = $config;
        $this->dbname = $config['dbname'] ?? 'default';
    }
    
    /**
     * Генерация подписи для AWS S3
     */
    private function generateSignature($method, $contentMd5, $contentType, $date, $resource) {
        $stringToSign = "$method\n$contentMd5\n$contentType\n$date\n$resource";
        return base64_encode(hash_hmac('sha1', $stringToSign, $this->config['secret_key'], true));
    }
    
    /**
     * Выполнение HTTP запроса к S3
     */
    private function makeRequest($method, $key, $data = null, $contentType = 'application/json') {
        $url = $this->config['endpoint'] . '/' . $this->config['bucket'] . '/' . $key;
        $date = gmdate('r');
        $contentMd5 = $data ? base64_encode(md5($data, true)) : '';
        $resource = '/' . $this->config['bucket'] . '/' . $key;
        
        $signature = $this->generateSignature($method, $contentMd5, $contentType, $date, $resource);
        $authorization = 'AWS ' . $this->config['access_key'] . ':' . $signature;
        
        $headers = [
            'Date: ' . $date,
            'Authorization: ' . $authorization,
        ];
        
        if ($data) {
            $headers[] = 'Content-Type: ' . $contentType;
            $headers[] = 'Content-MD5: ' . $contentMd5;
        }
        
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_HEADER => true, // для получения заголовков
        ]);
        
        if ($data) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        }
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $error = curl_error($ch);
        curl_close($ch);
        
        if ($error) {
            throw new Exception("cURL Error: $error");
        }
        
        $headers = substr($response, 0, $headerSize);
        $body = substr($response, $headerSize);
        
        return ['code' => $httpCode, 'data' => $body, 'headers' => $headers];
    }
    
    /**
     * Сохранение данных в S3
     */
    public function put($key, $data) {
        try {
            $fullKey = $this->cachePrefix . $this->dbname . '/' . $key;
            $result = $this->makeRequest('PUT', $fullKey, $data);
            return $result['code'] === 200;
        } catch (Exception $e) {
            error_log("S3 PUT Error: " . $e->getMessage());
            return false;
        }
    }
    
    /**
     * Получение данных из S3
     */
    public function get($key) {
        try {
            $fullKey = $this->cachePrefix . $this->dbname . '/' . $key;
            $result = $this->makeRequest('GET', $fullKey);
            
            if ($result['code'] === 200) {
                return $result['data'];
            }
            return false;
        } catch (Exception $e) {
            error_log("S3 GET Error: " . $e->getMessage());
            return false;
        }
    }
    
    /**
     * Проверка существования файла в S3
     */
    public function exists($key) {
        try {
            $fullKey = $this->cachePrefix . $this->dbname . '/' . $key;
            $result = $this->makeRequest('HEAD', $fullKey);
            return $result['code'] === 200;
        } catch (Exception $e) {
            return false;
        }
    }
    
    /**
     * Получение времени последнего изменения файла
     */
    public function getLastModified($key) {
        try {
            $fullKey = $this->cachePrefix . $this->dbname . '/' . $key;
            $result = $this->makeRequest('HEAD', $fullKey);
            
            if ($result['code'] === 200) {
                // Парсим заголовки для получения Last-Modified
                $headers = $result['headers'];
                if (preg_match('/Last-Modified:\s*(.+)/i', $headers, $matches)) {
                    return strtotime(trim($matches[1]));
                }
            }
            return false;
        } catch (Exception $e) {
            return false;
        }
    }
}

/**
 * Локальный кеш для сравнения
 */
class LocalCache {
    private $cacheDir;
    
    public function __construct($config) {
        $this->cacheDir = $config['cache_dir'] ?? (__DIR__ . '/cache_treba_online_ru');
        if (!file_exists($this->cacheDir)) {
            mkdir($this->cacheDir, 0777, true);
        }
    }
    
    public function put($key, $data) {
        $file = $this->cacheDir . '/' . $key;
        return file_put_contents($file, $data) !== false;
    }
    
    public function get($key) {
        $file = $this->cacheDir . '/' . $key;
        return file_exists($file) ? file_get_contents($file) : false;
    }
    
    public function exists($key) {
        $file = $this->cacheDir . '/' . $key;
        return file_exists($file);
    }
    
    public function getLastModified($key) {
        $file = $this->cacheDir . '/' . $key;
        return file_exists($file) ? filemtime($file) : false;
    }
}

/**
 * Универсальный класс для работы с кешем
 */
class CacheManager {
    private $storage;
    private $cacheHours;
    
    public function __construct($storageType, $config, $cacheHours = 0) {
        $this->cacheHours = $cacheHours;
        
        if ($storageType === 's3') {
            $this->storage = new S3Cache($config);
        } else {
            $this->storage = new LocalCache($config);
        }
    }
    
    public function get($key) {
        $data = $this->storage->get($key);
        
        if ($data !== false && $this->cacheHours > 0) {
            $lastModified = $this->storage->getLastModified($key);
            if ($lastModified && (time() - $lastModified) > ($this->cacheHours * 3600)) {
                return false; // кеш устарел
            }
        }
        
        return $data;
    }
    
    public function put($key, $data) {
        return $this->storage->put($key, $data);
    }
    
    public function exists($key) {
        return $this->storage->exists($key);
    }
}

// Получаем имя таблицы из 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) {
    if ($cacheStorage === 's3') {
        // Добавляем dbname в конфигурацию S3
        $cacheConfig = array_merge($s3Config, ['dbname' => $dbname]);
    } else {
        $cacheConfig = ['cache_dir' => __DIR__ . '/cache_treba_online_ru'];
    }
    
    $cache = new CacheManager($cacheStorage, $cacheConfig, $cacheHours);
    
    $cacheKey = md5(json_encode($_GET) . json_encode($input)) . '.json';
    
    // Проверяем кеш
    $cachedData = $cache->get($cacheKey);
    if ($cachedData !== false) {
        header('Content-Type: application/json');
        echo $cachedData;
        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 === '!=') {
                $globalCounter++;
                $ph = "{$column}_ne_{$globalCounter}";

                // Специальная логика для сравнения с true/false, если поле текстовое
                if (is_string($value) && strtolower($value) === 'true') {
                    // Добавляем условие: не равно 'true' ИЛИ значение NULL
                    $currentGroup['conditions'][] = "({$column} IS NULL OR {$column} != 'true')";
                } elseif (is_string($value) && strtolower($value) === 'false') {
                    $currentGroup['conditions'][] = "({$column} IS NULL OR {$column} != 'false')";
                } else {
                    // Обычное сравнение
                    $currentGroup['conditions'][] = "{$column} != :{$ph}";
                    $params[$ph] = $value;
                }
                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 === ') {
                // Добавляем в DISTINCT если значение "положительное"
                if ($value == '1' || $value === 1 || $value === true || 
                    (is_string($value) && in_array(strtolower($value), ['1', 'true', 'on', 'yes']))) {
                    $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 = '';
    $groupBy = '';
    
    // Обрабатываем сортировку из input
    if (!empty($input['sort']) && is_array($input['sort'])) {
        $sortParts = [];
        foreach ($input['sort'] as $s) {
            $sortParts[] = "{$s['column']} {$s['direction']}";
        }
        $orderBy = 'ORDER BY ' . implode(', ', $sortParts);
    } else {
        // Если сортировка не указана, применяем сортировку по умолчанию
        $orderBy = 'ORDER BY id ASC';
    }
    $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)) {
        // Для получения уникальных полных строк по определенному столбцу
        if ($dbType === 'pgsql') {
            $distinctCol = $distinctColumns[0]; // берем первый столбец для DISTINCT
            $select = "SELECT DISTINCT ON ({$distinctCol}) *";
            $countQ = "SELECT COUNT(*) FROM (SELECT DISTINCT ON ({$distinctCol}) * FROM {$table} {$where}) as sub";
        } else {
            // Для MySQL используем GROUP BY
            $distinctCol = $distinctColumns[0]; // берем первый столбец для DISTINCT
            $select = 'SELECT *';
            $groupBy = "GROUP BY {$distinctCol}";
            $countQ = "SELECT COUNT(*) FROM (SELECT * FROM {$table} {$where} GROUP BY {$distinctCol}) 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
    if (!empty($distinctColumns)) {
        if ($dbType === 'pgsql') {
            $sql = "{$select} FROM {$table} {$where} {$orderBy} {$limitClause} {$offsetClause}";
        } else {
            // Для MySQL добавляем GROUP BY
            $sql = "{$select} FROM {$table} {$where} {$groupBy} {$orderBy} {$limitClause} {$offsetClause}";
        }
    } else {
        $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 и обработка creatium_template
    $templateRows = []; // массив для строк с creatium_template
    $regularRows = [];  // массив для обычных строк
    
    foreach ($items as &$row) {
        $hasTemplate = false; // флаг для определения строки с шаблоном
        
        foreach ($row as $colName => &$value) {
            // Проверяем, является ли значение пустой строкой
            if (is_string($value) && trim($value) === '') {
                $value = null;
            }
            // Проверяем на "creatium_template" - заменяем на пустую строку
            if (is_string($value) && trim($value) === 'creatium_template') {
                $value = '';
                $hasTemplate = true; // отмечаем, что в этой строке был шаблон
            }
        }
        
        // Разделяем строки: с шаблоном - в начало, обычные - после
        if ($hasTemplate) {
            $templateRows[] = $row;
        } else {
            $regularRows[] = $row;
        }
    }
    unset($row, $value); // разрываем ссылки после foreach
    
    // Объединяем массивы: сначала строки с шаблонами, потом обычные
    $items = array_merge($templateRows, $regularRows);
    
    // Переупорядочиваем ключи в каждой строке: ID первым
    foreach ($items as &$row) {
        $reorderedRow = [];
        
        // Сначала добавляем ID (ищем поле независимо от регистра)
        foreach ($row as $colName => $value) {
            if (strtolower($colName) === 'id') {
                $reorderedRow[$colName] = $value;
                break;
            }
        }
        
        // Затем добавляем остальные поля
        foreach ($row as $colName => $value) {
            if (strtolower($colName) !== 'id') {
                $reorderedRow[$colName] = $value;
            }
        }
        
        $row = $reorderedRow;
    }
    unset($row); // разрываем ссылку после 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);
        $cache->put($cacheKey, $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

Политика конфиденциальности
Офферта