Работа с набором меток в PHP: подходы и реализация
В управлении контентом часто требуется работа с тегами (метками). Ниже рассматриваются различные способы хранения и обработки списка тегов в PHP. Основное внимание уделяется эффективной реализации с использованием реляционной базы данных.
Подходы к работе со списком тегов
Наиболее эффективное решение для управления тегами - хранение в отдельной таблице с отношением многие ко многим. Это позволяет выполнять сложные запросы, избегать дублирования данных и обеспечивать целостность.
Как реализовать нормализованное хранение тегов с помощью PDO?
Создаются две таблицы: tags (id, name) и content_tags (content_id, tag_id). Пример кода для MySQL:
$sql = 'CREATE TABLE tags (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL
)';
$pdo->exec($sql);
$sql = 'CREATE TABLE content_tags (
content_id INT NOT NULL,
tag_id INT NOT NULL,
PRIMARY KEY (content_id, tag_id),
FOREIGN KEY (tag_id) REFERENCES tags(id)
)';
$pdo->exec($sql);
Процедура добавления тега к контенту включает проверку существования тега и вставку связи:
function addTagToContent($pdo, $contentId, $tagName) {
// Проверяется существование тега
$stmt = $pdo->prepare('SELECT id FROM tags WHERE name = ?');
$stmt->execute([$tagName]);
$row = $stmt->fetch();
if ($row) {
$tagId = $row['id'];
} else {
$stmt = $pdo->prepare('INSERT INTO tags (name) VALUES (?)');
$stmt->execute([$tagName]);
$tagId = $pdo->lastInsertId();
}
// Связь устанавливается
$stmt = $pdo->prepare('INSERT IGNORE INTO content_tags (content_id, tag_id) VALUES (?, ?)');
$stmt->execute([$contentId, $tagId]);
}
Как хранить теги в виде строки с разделителями?
Самый простой способ заключается в объединении тегов через запятую. Подходит для небольших проектов без сложного поиска.
$tags = ['php', 'контент', 'управление'];
$tagsStr = implode(',', $tags);
// Вывод
$tagsArray = explode(',', $tagsStr);
foreach ($tagsArray as $tag) {
echo htmlspecialchars(trim($tag)) . ' ';
}
Как использовать JSON для хранения массива тегов?
Современные СУБД (MySQL 5.7+, PostgreSQL) поддерживают тип JSON, что позволяет хранить структурированные данные в одном поле.
$tags = ['php', 'json', 'теги'];
$tagsJson = json_encode($tags);
// Извлечение
$tagsArray = json_decode($tagsJson, true);
foreach ($tagsArray as $tag) {
echo htmlspecialchars($tag) . ' ';
}
В MySQL возможен поиск по JSON с помощью JSON_CONTAINS.
$stmt = $pdo->prepare('SELECT * FROM content WHERE JSON_CONTAINS(tags, ?)');
$stmt->execute(['"php"']);
Как создать облако тегов с помощью библиотеки?
Библиотеки, например php-tag-cloud, упрощают генерацию облака тегов с различными размерами.
use PhpTagCloud\TagCloud;
$tags = [
'PHP' => 50,
'MySQL' => 30,
'JavaScript' => 20,
];
$cloud = new TagCloud($tags);
echo $cloud->render();
Как организовать древовидную структуру тегов?
Для категорий с подкатегориями применяется таблица с полем parent_id.
$sql = 'CREATE TABLE tags (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
parent_id INT DEFAULT NULL,
FOREIGN KEY (parent_id) REFERENCES tags(id)
)';
$pdo->exec($sql);
// Выборка потомков
function getChildren($pdo, $parentId) {
$stmt = $pdo->prepare('SELECT * FROM tags WHERE parent_id = ?');
$stmt->execute([$parentId]);
return $stmt->fetchAll();
}
Расширенные примеры работы с тегами
Ниже приведены примеры, охватывающие класс для управления тегами, поиск по нескольким тегам, автодополнение и импорт из CSV.
// Пример 1. Класс TagManager для нормализованного хранения
class TagManager {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function addTagsToContent(int $contentId, array $tagNames): void {
$this->pdo->beginTransaction();
try {
foreach ($tagNames as $name) {
$name = trim($name);
if ($name === '') continue;
// Получение или создание тега
$stmt = $this->pdo->prepare('SELECT id FROM tags WHERE name = ?');
$stmt->execute([$name]);
$row = $stmt->fetch();
if (!$row) {
$stmt = $this->pdo->prepare('INSERT INTO tags (name) VALUES (?)');
$stmt->execute([$name]);
$tagId = (int)$this->pdo->lastInsertId();
} else {
$tagId = (int)$row['id'];
}
// Установка связи
$stmt = $this->pdo->prepare('INSERT IGNORE INTO content_tags (content_id, tag_id) VALUES (?, ?)');
$stmt->execute([$contentId, $tagId]);
}
$this->pdo->commit();
} catch (Exception $e) {
$this->pdo->rollBack();
throw $e;
}
}
public function getTagsForContent(int $contentId): array {
$stmt = $this->pdo->prepare('SELECT t.name FROM tags t JOIN content_tags ct ON t.id = ct.tag_id WHERE ct.content_id = ?');
$stmt->execute([$contentId]);
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
public function renderTagCloud(): string {
$stmt = $this->pdo->query('SELECT t.name, COUNT(ct.content_id) as cnt FROM tags t JOIN content_tags ct ON t.id = ct.tag_id GROUP BY t.id ORDER BY cnt DESC');
$tags = $stmt->fetchAll();
if (empty($tags)) return '';
$min = min(array_column($tags, 'cnt'));
$max = max(array_column($tags, 'cnt'));
$html = '';
foreach ($tags as $tag) {
$size = 1 + (($tag['cnt'] - $min) / ($max - $min)) * 4; // от 1 до 5
$html .= '' . htmlspecialchars($tag['name']) . ' ';
}
return $html;
}
}
// Пример использования класса $manager = new TagManager($pdo); $manager->addTagsToContent(1, ['PHP', 'MySQL', 'Web']); $tags = $manager->getTagsForContent(1); // $tags = ['PHP', 'MySQL', 'Web'] echo $manager->renderTagCloud(); // Выводит HTML с тегами разного размера в зависимости от числа использований
// Пример 2. Поиск контента по списку тегов (AND/OR)
function searchByTags(PDO $pdo, array $tags, string $mode = 'AND'): array {
$placeholders = implode(',', array_fill(0, count($tags), '?'));
if ($mode === 'AND') {
$sql = 'SELECT c.* FROM content c
JOIN content_tags ct ON c.id = ct.content_id
JOIN tags t ON ct.tag_id = t.id
WHERE t.name IN (' . $placeholders . ')
GROUP BY c.id
HAVING COUNT(DISTINCT t.id) = ?';
$params = array_merge($tags, [count($tags)]);
} else {
$sql = 'SELECT DISTINCT c.* FROM content c
JOIN content_tags ct ON c.id = ct.content_id
JOIN tags t ON ct.tag_id = t.id
WHERE t.name IN (' . $placeholders . ')';
$params = $tags;
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
// Вызов $resultsAnd = searchByTags($pdo, ['PHP', 'база данных'], 'AND'); $resultsOr = searchByTags($pdo, ['PHP', 'база данных'], 'OR');
// Пример 3. Автодополнение тегов (AJAX endpoint)
// Файл ajax_tags.php
<?
header('Content-Type: application/json');
require 'db.php';
$q = $_GET['q'] ?? '';
if (strlen($q) < 1) {
echo json_encode([]);
exit;
}
$stmt = $pdo->prepare('SELECT name FROM tags WHERE name LIKE ? LIMIT 10');
$stmt->execute([$q . '%']);
$suggestions = $stmt->fetchAll(PDO::FETCH_COLUMN);
echo json_encode($suggestions);
?>
// Запрос: GET /ajax_tags.php?q=ph // Ответ: ["php","phpmyadmin","photoshop"]
// Пример 4. Импорт тегов из CSV
function importTagsFromCSV(PDO $pdo, string $csvFile): void {
$handle = fopen($csvFile, 'r');
$pdo->beginTransaction();
try {
while (($row = fgetcsv($handle)) !== false) {
$name = trim($row[0] ?? '');
if ($name === '') continue;
$stmt = $pdo->prepare('INSERT IGNORE INTO tags (name) VALUES (?)');
$stmt->execute([$name]);
}
$pdo->commit();
fclose($handle);
} catch (Exception $e) {
$pdo->rollBack();
fclose($handle);
throw $e;
}
}
// CSV файл (tags.csv):
// php,MySQL,JavaScript
// После импорта в таблице tags появились записи (дубликаты игнорируются)