Add exact application files
This commit is contained in:
386
src/DataService.php
Normal file
386
src/DataService.php
Normal file
@@ -0,0 +1,386 @@
|
||||
<?php
|
||||
namespace App;
|
||||
|
||||
use PDO;
|
||||
|
||||
class DataService
|
||||
{
|
||||
public function __construct(private PDO $pdo) {}
|
||||
|
||||
public function fetchData(
|
||||
string $schema,
|
||||
string $table,
|
||||
array $columns,
|
||||
int $page,
|
||||
int $pageSize,
|
||||
array $filters,
|
||||
?array $sort
|
||||
): array {
|
||||
$offset = ($page - 1) * $pageSize;
|
||||
|
||||
$colNames = array_map(fn($c) => $c['COLUMN_NAME'], $columns);
|
||||
$quotedColumns = array_map(fn($name) => "`$name`", $colNames);
|
||||
$selectList = implode(', ', $quotedColumns);
|
||||
|
||||
$whereParts = [];
|
||||
$params = [];
|
||||
foreach ($filters as $i => $f) {
|
||||
$field = $f['field'] ?? null;
|
||||
$value = $f['value'] ?? null;
|
||||
if (!$field || !in_array($field, $colNames, true) || $value === null || $value === '') {
|
||||
continue;
|
||||
}
|
||||
$param = ":f{$i}";
|
||||
$whereParts[] = "`$field` LIKE $param";
|
||||
$params[$param] = '%' . $value . '%';
|
||||
}
|
||||
$whereSql = $whereParts ? 'WHERE ' . implode(' AND ', $whereParts) : '';
|
||||
|
||||
$orderSql = '';
|
||||
if ($sort && !empty($sort['field']) && in_array($sort['field'], $colNames, true)) {
|
||||
$dir = strtoupper($sort['dir'] ?? 'ASC');
|
||||
if (!in_array($dir, ['ASC', 'DESC'], true)) {
|
||||
$dir = 'ASC';
|
||||
}
|
||||
$orderSql = "ORDER BY `{$sort['field']}` $dir";
|
||||
}
|
||||
|
||||
$sql = "SELECT $selectList
|
||||
FROM `{$schema}`.`{$table}`
|
||||
$whereSql
|
||||
$orderSql
|
||||
LIMIT :limit OFFSET :offset";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
foreach ($params as $k => $v) {
|
||||
$stmt->bindValue($k, $v);
|
||||
}
|
||||
$stmt->bindValue(':limit', $pageSize, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
$countSql = "SELECT COUNT(*) AS cnt
|
||||
FROM `{$schema}`.`{$table}` $whereSql";
|
||||
$countStmt = $this->pdo->prepare($countSql);
|
||||
foreach ($params as $k => $v) {
|
||||
$countStmt->bindValue($k, $v);
|
||||
}
|
||||
$countStmt->execute();
|
||||
$total = (int)$countStmt->fetchColumn();
|
||||
|
||||
$lastPage = max(1, (int)ceil($total / $pageSize));
|
||||
|
||||
return [
|
||||
'data' => $rows,
|
||||
'last_page' => $lastPage,
|
||||
'total' => $total,
|
||||
'current_page' => $page
|
||||
];
|
||||
}
|
||||
|
||||
public function insertRow(string $schema, string $table, array $row, array $columns): array
|
||||
{
|
||||
$insertCols = [];
|
||||
$placeholders = [];
|
||||
$params = [];
|
||||
|
||||
foreach ($columns as $c) {
|
||||
$name = $c['COLUMN_NAME'];
|
||||
$extra = $c['EXTRA'] ?? '';
|
||||
|
||||
if (str_contains($extra, 'auto_increment')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (array_key_exists($name, $row)) {
|
||||
$insertCols[] = "`$name`";
|
||||
$placeholders[] = ":$name";
|
||||
$params[":$name"] = $row[$name];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($insertCols)) {
|
||||
$sql = "INSERT INTO `{$schema}`.`{$table}` () VALUES ()";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
|
||||
try {
|
||||
$stmt->execute();
|
||||
return ['inserted' => true, 'id' => $this->pdo->lastInsertId()];
|
||||
} catch (\PDOException $e) {
|
||||
throw new \RuntimeException($this->formatPDOError($e, $schema, $table));
|
||||
}
|
||||
}
|
||||
|
||||
$sql = sprintf(
|
||||
"INSERT INTO `%s`.`%s` (%s) VALUES (%s)",
|
||||
$schema, $table,
|
||||
implode(',', $insertCols),
|
||||
implode(',', $placeholders)
|
||||
);
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
|
||||
try {
|
||||
$stmt->execute($params);
|
||||
return ['inserted' => true, 'id' => $this->pdo->lastInsertId()];
|
||||
} catch (\PDOException $e) {
|
||||
throw new \RuntimeException($this->formatPDOError($e, $schema, $table, $params));
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Метод для форматирования ошибок
|
||||
private function formatPDOError(\PDOException $e, string $schema, string $table, array $params = []): string
|
||||
{
|
||||
$code = $e->getCode();
|
||||
$message = $e->getMessage();
|
||||
|
||||
// Foreign Key constraint
|
||||
if ($code === '23000' && str_contains($message, 'foreign key constraint')) {
|
||||
if (preg_match('/FOREIGN KEY $$`([^`]+)`$$/', $message, $matches)) {
|
||||
$field = $matches[1];
|
||||
return "Ошибка: поле '{$field}' должно содержать существующее значение из связанной таблицы.";
|
||||
}
|
||||
return "Ошибка: нарушена связь с другой таблицей. Проверьте правильность заполнения полей-ссылок.";
|
||||
}
|
||||
|
||||
// Duplicate key
|
||||
if ($code === '23000' && str_contains($message, 'Duplicate entry')) {
|
||||
if (preg_match('/Duplicate entry \'([^\']+)\' for key \'([^\']+)\'/', $message, $matches)) {
|
||||
$value = $matches[1];
|
||||
$key = $matches[2];
|
||||
return "Ошибка: значение '{$value}' уже существует (ключ '{$key}').";
|
||||
}
|
||||
return "Ошибка: запись с таким значением уже существует.";
|
||||
}
|
||||
|
||||
// NOT NULL constraint
|
||||
if (str_contains($message, "cannot be null") || str_contains($message, "doesn't have a default value")) {
|
||||
if (preg_match('/Column \'([^\']+)\'/', $message, $matches)) {
|
||||
$field = $matches[1];
|
||||
return "Ошибка: поле '{$field}' обязательно для заполнения.";
|
||||
}
|
||||
return "Ошибка: не заполнены обязательные поля.";
|
||||
}
|
||||
|
||||
return "Ошибка БД: " . $message;
|
||||
}
|
||||
|
||||
public function updateRow(string $schema, string $table, array $row, array $columns, array $pk): array
|
||||
{
|
||||
if (empty($pk)) {
|
||||
throw new \RuntimeException('No primary key — update disabled');
|
||||
}
|
||||
|
||||
$sets = [];
|
||||
$params = [];
|
||||
|
||||
foreach ($columns as $c) {
|
||||
$name = $c['COLUMN_NAME'];
|
||||
if (in_array($name, $pk, true)) {
|
||||
continue;
|
||||
}
|
||||
if (array_key_exists($name, $row)) {
|
||||
$sets[] = "`$name` = :v_$name";
|
||||
$params[":v_$name"] = $row[$name];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($sets)) {
|
||||
return ['updated' => 0, 'message' => 'No changes'];
|
||||
}
|
||||
|
||||
$whereParts = [];
|
||||
foreach ($pk as $name) {
|
||||
if (!array_key_exists($name, $row)) {
|
||||
throw new \RuntimeException("Missing PK value: $name");
|
||||
}
|
||||
$whereParts[] = "`$name` = :pk_$name";
|
||||
$params[":pk_$name"] = $row[$name];
|
||||
}
|
||||
|
||||
$sql = sprintf(
|
||||
"UPDATE `%s`.`%s` SET %s WHERE %s",
|
||||
$schema, $table,
|
||||
implode(', ', $sets),
|
||||
implode(' AND ', $whereParts)
|
||||
);
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
return ['updated' => $stmt->rowCount()];
|
||||
}
|
||||
|
||||
public function deleteRow(string $schema, string $table, array $row, array $pk): array
|
||||
{
|
||||
if (empty($pk)) {
|
||||
throw new \RuntimeException('No primary key — delete disabled');
|
||||
}
|
||||
|
||||
$whereParts = [];
|
||||
$params = [];
|
||||
|
||||
foreach ($pk as $name) {
|
||||
if (!array_key_exists($name, $row)) {
|
||||
throw new \RuntimeException("Missing PK value: $name");
|
||||
}
|
||||
$whereParts[] = "`$name` = :pk_$name";
|
||||
$params[":pk_$name"] = $row[$name];
|
||||
}
|
||||
|
||||
$sql = sprintf(
|
||||
"DELETE FROM `%s`.`%s` WHERE %s",
|
||||
$schema, $table,
|
||||
implode(' AND ', $whereParts)
|
||||
);
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
return ['deleted' => $stmt->rowCount()];
|
||||
}
|
||||
|
||||
public function insertMultipleRows(string $schema, string $table, array $rows, array $columns): array
|
||||
{
|
||||
if (empty($rows)) {
|
||||
return ['inserted' => 0, 'errors' => 0, 'message' => 'No rows provided'];
|
||||
}
|
||||
|
||||
$inserted = 0;
|
||||
$errors = 0;
|
||||
$errorMessages = [];
|
||||
|
||||
$validColumns = [];
|
||||
foreach ($columns as $c) {
|
||||
$name = $c['COLUMN_NAME'];
|
||||
$extra = $c['EXTRA'] ?? '';
|
||||
|
||||
if (!str_contains($extra, 'auto_increment')) {
|
||||
$validColumns[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$this->pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
foreach ($rows as $index => $row) {
|
||||
try {
|
||||
$insertCols = [];
|
||||
$placeholders = [];
|
||||
$params = [];
|
||||
|
||||
foreach ($validColumns as $name) {
|
||||
if (array_key_exists($name, $row)) {
|
||||
$insertCols[] = "`$name`";
|
||||
$placeholders[] = ":$name";
|
||||
$params[":$name"] = $row[$name];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($insertCols)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sql = sprintf(
|
||||
"INSERT INTO `%s`.`%s` (%s) VALUES (%s)",
|
||||
$schema, $table,
|
||||
implode(',', $insertCols),
|
||||
implode(',', $placeholders)
|
||||
);
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$inserted++;
|
||||
} catch (\PDOException $e) {
|
||||
$errors++;
|
||||
$errorMessages[] = "Строка " . ($index + 1) . ": " . $this->formatPDOError($e, $schema, $table);
|
||||
}
|
||||
}
|
||||
|
||||
$this->pdo->commit();
|
||||
} catch (\Exception $e) {
|
||||
$this->pdo->rollBack();
|
||||
throw new \RuntimeException('Import failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return [
|
||||
'inserted' => $inserted,
|
||||
'errors' => $errors,
|
||||
'errorMessages' => $errorMessages
|
||||
];
|
||||
}
|
||||
|
||||
public function exportCSV(
|
||||
string $schema,
|
||||
string $table,
|
||||
array $columns,
|
||||
array $filters,
|
||||
?array $sort
|
||||
): array {
|
||||
$colNames = array_map(fn($c) => $c['COLUMN_NAME'], $columns);
|
||||
$quotedColumns = array_map(fn($name) => "`$name`", $colNames);
|
||||
$selectList = implode(', ', $quotedColumns);
|
||||
|
||||
$whereParts = [];
|
||||
$params = [];
|
||||
foreach ($filters as $i => $f) {
|
||||
$field = $f['field'] ?? null;
|
||||
$value = $f['value'] ?? null;
|
||||
if (!$field || !in_array($field, $colNames, true) || $value === null) {
|
||||
continue;
|
||||
}
|
||||
$param = ":f{$i}";
|
||||
$whereParts[] = "`$field` LIKE $param";
|
||||
$params[$param] = '%' . $value . '%';
|
||||
}
|
||||
$whereSql = $whereParts ? 'WHERE ' . implode(' AND ', $whereParts) : '';
|
||||
|
||||
$orderSql = '';
|
||||
if ($sort && !empty($sort['field']) && in_array($sort['field'], $colNames, true)) {
|
||||
$dir = strtoupper($sort['dir'] ?? 'ASC');
|
||||
if (!in_array($dir, ['ASC', 'DESC'], true)) {
|
||||
$dir = 'ASC';
|
||||
}
|
||||
$orderSql = "ORDER BY `{$sort['field']}` $dir";
|
||||
}
|
||||
|
||||
$sql = "SELECT $selectList
|
||||
FROM `{$schema}`.`{$table}`
|
||||
$whereSql
|
||||
$orderSql";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
foreach ($params as $k => $v) {
|
||||
$stmt->bindValue($k, $v);
|
||||
}
|
||||
$stmt->execute();
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
$csv = $this->arrayToCSV($colNames, $rows);
|
||||
|
||||
return [
|
||||
'csv' => $csv,
|
||||
'rowCount' => count($rows)
|
||||
];
|
||||
}
|
||||
|
||||
private function arrayToCSV(array $headers, array $rows): string
|
||||
{
|
||||
$output = fopen('php://temp', 'r+');
|
||||
|
||||
fputcsv($output, $headers, ';');
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$rowData = [];
|
||||
foreach ($headers as $header) {
|
||||
$rowData[] = $row[$header] ?? '';
|
||||
}
|
||||
fputcsv($output, $rowData, ';');
|
||||
}
|
||||
|
||||
rewind($output);
|
||||
$csv = stream_get_contents($output);
|
||||
fclose($output);
|
||||
|
||||
return $csv;
|
||||
}
|
||||
}
|
||||
43
src/Db.php
Normal file
43
src/Db.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace App;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
|
||||
class Db
|
||||
{
|
||||
// Простая фабрика PDO по логину/паролю MariaDB из сессии
|
||||
public static function connectFromSession(): PDO
|
||||
{
|
||||
if (empty($_SESSION['db_user']) || empty($_SESSION['db_pass'])) {
|
||||
throw new \RuntimeException('Not authenticated');
|
||||
}
|
||||
|
||||
$user = $_SESSION['db_user'];
|
||||
$pass = $_SESSION['db_pass'];
|
||||
|
||||
// TODO: вынести host/port/charset в конфиг .env
|
||||
$dsn = 'mysql:host=localhost;port=3306;charset=utf8mb4';
|
||||
|
||||
$pdo = new PDO($dsn, $user, $pass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_EMULATE_PREPARES => false, // важная настройка [web:28][web:25]
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
public static function testConnection(string $user, string $pass): void
|
||||
{
|
||||
$dsn = 'mysql:host=localhost;port=3306;charset=utf8mb4'; // MariaDB совместим [web:22][web:28]
|
||||
try {
|
||||
new PDO($dsn, $user, $pass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
throw new \RuntimeException('Connection failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/MetaService.php
Normal file
121
src/MetaService.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
namespace App;
|
||||
|
||||
use PDO;
|
||||
|
||||
class MetaService
|
||||
{
|
||||
public function __construct(private PDO $pdo) {}
|
||||
|
||||
public function getSchemaTree(): array
|
||||
{
|
||||
$sql = "
|
||||
SELECT TABLE_SCHEMA, TABLE_NAME
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_TYPE = 'BASE TABLE'
|
||||
AND TABLE_SCHEMA NOT IN ('information_schema','mysql','performance_schema','sys')
|
||||
ORDER BY TABLE_SCHEMA, TABLE_NAME
|
||||
";
|
||||
$rows = $this->pdo->query($sql)->fetchAll();
|
||||
|
||||
$tree = [];
|
||||
foreach ($rows as $row) {
|
||||
$schema = $row['TABLE_SCHEMA'];
|
||||
$table = $row['TABLE_NAME'];
|
||||
$tree[$schema]['name'] = $schema;
|
||||
$tree[$schema]['tables'][] = $table;
|
||||
}
|
||||
return array_values($tree);
|
||||
}
|
||||
|
||||
public function getTableMeta(string $schema, string $table): array
|
||||
{
|
||||
$sql = "
|
||||
SELECT
|
||||
COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, COLUMN_KEY,
|
||||
IS_NULLABLE, COLUMN_DEFAULT, EXTRA, ORDINAL_POSITION
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table
|
||||
ORDER BY ORDINAL_POSITION
|
||||
";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([':schema' => $schema, ':table' => $table]);
|
||||
$cols = $stmt->fetchAll();
|
||||
|
||||
// ✅ Получаем информацию о Foreign Keys
|
||||
$fkSql = "
|
||||
SELECT
|
||||
COLUMN_NAME,
|
||||
REFERENCED_TABLE_SCHEMA,
|
||||
REFERENCED_TABLE_NAME,
|
||||
REFERENCED_COLUMN_NAME
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = :schema
|
||||
AND TABLE_NAME = :table
|
||||
AND REFERENCED_TABLE_NAME IS NOT NULL
|
||||
";
|
||||
$fkStmt = $this->pdo->prepare($fkSql);
|
||||
$fkStmt->execute([':schema' => $schema, ':table' => $table]);
|
||||
$foreignKeys = $fkStmt->fetchAll();
|
||||
|
||||
// Создаём map FK для быстрого доступа
|
||||
$fkMap = [];
|
||||
foreach ($foreignKeys as $fk) {
|
||||
$fkMap[$fk['COLUMN_NAME']] = [
|
||||
'ref_schema' => $fk['REFERENCED_TABLE_SCHEMA'],
|
||||
'ref_table' => $fk['REFERENCED_TABLE_NAME'],
|
||||
'ref_column' => $fk['REFERENCED_COLUMN_NAME']
|
||||
];
|
||||
}
|
||||
|
||||
// Primary keys
|
||||
$pk = [];
|
||||
foreach ($cols as $c) {
|
||||
if ($c['COLUMN_KEY'] === 'PRI') {
|
||||
$pk[] = $c['COLUMN_NAME'];
|
||||
}
|
||||
}
|
||||
|
||||
$enrichedCols = array_map(function($c) use ($fkMap) {
|
||||
$name = $c['COLUMN_NAME'];
|
||||
|
||||
return [
|
||||
'COLUMN_NAME' => $name,
|
||||
'DATA_TYPE' => $c['DATA_TYPE'],
|
||||
'COLUMN_TYPE' => $c['COLUMN_TYPE'],
|
||||
'COLUMN_KEY' => $c['COLUMN_KEY'],
|
||||
'IS_NULLABLE' => $c['IS_NULLABLE'] === 'YES',
|
||||
'COLUMN_DEFAULT' => $c['COLUMN_DEFAULT'],
|
||||
'HAS_DEFAULT' => !empty($c['COLUMN_DEFAULT']),
|
||||
'EXTRA' => $c['EXTRA'],
|
||||
'IS_AUTO_INCREMENT' => str_contains($c['EXTRA'] ?? '', 'auto_increment'),
|
||||
'ORDINAL_POSITION' => (int)$c['ORDINAL_POSITION'],
|
||||
'IS_REQUIRED' => $c['IS_NULLABLE'] === 'NO' && empty($c['COLUMN_DEFAULT']),
|
||||
'EDITOR_TYPE' => $this->getEditorType($c),
|
||||
// ✅ Добавляем информацию о FK
|
||||
'IS_FOREIGN_KEY' => isset($fkMap[$name]),
|
||||
'FOREIGN_KEY' => $fkMap[$name] ?? null
|
||||
];
|
||||
}, $cols);
|
||||
|
||||
return [
|
||||
'columns' => $enrichedCols,
|
||||
'primaryKey' => $pk,
|
||||
'totalColumns' => count($enrichedCols),
|
||||
'foreignKeys' => $foreignKeys
|
||||
];
|
||||
}
|
||||
|
||||
private function getEditorType(array $col): string
|
||||
{
|
||||
$type = strtolower($col['DATA_TYPE']);
|
||||
$fullType = strtolower($col['COLUMN_TYPE']);
|
||||
|
||||
if (str_contains($fullType, 'int') || str_contains($type, 'int')) return 'number';
|
||||
if (str_contains($fullType, 'float') || str_contains($fullType, 'decimal') || str_contains($fullType, 'double')) return 'number';
|
||||
if (str_contains($type, 'date')) return 'datetime';
|
||||
if (str_contains($type, 'time')) return 'time';
|
||||
if (str_contains($type, 'bool') || str_contains($type, 'boolean')) return 'tickCross';
|
||||
return 'input';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user