Video platforms need dynamic category systems that adapt to regional content. Here's how I built the category engine for TrendVidStream, a platform serving 8 diverse global regions.
The Challenge
Categories seem simple until you serve multiple regions. Music is universal, but category IDs vary between regions. Some regions have categories that others don't. Category popularity differs dramatically: Gaming dominates in the US, Music leads in UAE.
Data Model
CREATE TABLE categories (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
video_count INTEGER DEFAULT 0,
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE category_regions (
category_id INTEGER NOT NULL,
region TEXT NOT NULL,
video_count INTEGER DEFAULT 0,
rank INTEGER DEFAULT 0,
PRIMARY KEY (category_id, region),
FOREIGN KEY (category_id) REFERENCES categories(id)
);
Category Manager
<?php
class CategoryManager
{
private Cache $cache;
private PDO $db;
public function __construct(PDO $db, Cache $cache)
{
$this->db = $db;
$this->cache = $cache;
}
public function getAll(): array
{
$cached = $this->cache->get('global:categories');
if ($cached !== null) {
return $cached;
}
$stmt = $this->db->query('
SELECT c.*, COUNT(DISTINCT cr.region) as region_count
FROM categories c
LEFT JOIN category_regions cr ON c.id = cr.category_id
GROUP BY c.id
ORDER BY c.video_count DESC
');
$categories = $stmt->fetchAll(PDO::FETCH_ASSOC);
$this->cache->set('global:categories', $categories, Cache::TTL_CATEGORIES);
return $categories;
}
public function getForRegion(string $region): array
{
$cacheKey = "categories:$region";
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return $cached;
}
$stmt = $this->db->prepare('
SELECT c.*, cr.video_count as regional_count, cr.rank
FROM categories c
JOIN category_regions cr ON c.id = cr.category_id
WHERE cr.region = ?
ORDER BY cr.rank ASC
');
$stmt->execute([$region]);
$categories = $stmt->fetchAll(PDO::FETCH_ASSOC);
$this->cache->set($cacheKey, $categories, Cache::TTL_CATEGORIES);
return $categories;
}
public function updateCounts(): void
{
$this->db->exec('
UPDATE categories SET video_count = (
SELECT COUNT(*) FROM videos WHERE videos.category_id = categories.id
)
');
$this->db->exec('
UPDATE category_regions SET video_count = (
SELECT COUNT(*) FROM videos
WHERE videos.category_id = category_regions.category_id
AND videos.region = category_regions.region
)
');
// Invalidate cache
$this->cache->delete('global:categories');
}
}
Cache Invalidation
Categories change rarely, so we use a long TTL (24 hours). Cache is invalidated when:
- New videos are fetched (cron updates category counts)
- Admin manually saves changes
// In the fetch cron job:
$fetcher->run();
$categoryManager->updateCounts();
// Cache automatically invalidated in updateCounts()
HTTP Cache Headers
<?php
function setCategoryHeaders(): void
{
header('Cache-Control: public, max-age=10800, stale-while-revalidate=7200');
header('Vary: Accept-Encoding');
}
The 3-hour max-age with 2-hour stale-while-revalidate means users get fast responses while the cache refreshes in the background.
Category Normalization Across Regions
<?php
class CategoryNormalizer
{
// YouTube category IDs that map to our unified categories
private const MAPPING = [
1 => 'film-animation',
2 => 'autos-vehicles',
10 => 'music',
15 => 'pets-animals',
17 => 'sports',
20 => 'gaming',
22 => 'people-blogs',
23 => 'comedy',
24 => 'entertainment',
25 => 'news-politics',
26 => 'howto-style',
27 => 'education',
28 => 'science-tech',
];
public function normalize(int $youtubeCategoryId): ?string
{
return self::MAPPING[$youtubeCategoryId] ?? null;
}
}
This category system powers the navigation at TrendVidStream, where users can browse trending videos by category across all 8 regions. The caching ensures fast performance while keeping data fresh.
See the full category system in action at trendvidstream.com.
Top comments (0)