Toolvana: production-ready media tools platform (CodeIgniter 4)
- 134-tool registry with programmatic SEO (unique titles/H1/descriptions, JSON-LD graphs, sitemap index, canonical 301 enforcement via required filter) - DB-backed job queue (SKIP LOCKED) with drivers: Ffmpeg, Images (GD), Pdf (qpdf/gs/poppler), Youtube (thumbnails), Qr (server-side PNG) - Security: SSRF guard, MIME validation, rate limits, API-key auth, bcrypt admin login, security headers - Admin panel: dashboard, tools/categories/guides CRUD, SEO audit, analytics, job inspector with retry, system health, feature flags - Docker deployment (nginx + web/api FPM pools + scalable workers), PHPUnit suite (19 tests / 1139 assertions), PWA manifest + service worker
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
/**
|
||||
* "Related tools" recommendation engine.
|
||||
*
|
||||
* Scoring signals (highest first):
|
||||
* 1. Curated related_slugs authored in the registry
|
||||
* 2. Same primary input format (mp4 -> mp3 implies mp4 -> wav)
|
||||
* 3. Same primary output format (jpg -> png implies webp -> png)
|
||||
* 4. Same category, weighted by popularity
|
||||
*/
|
||||
final class RelatedTools
|
||||
{
|
||||
public const LIMIT = 8;
|
||||
|
||||
public function forTool(array $tool, int $limit = self::LIMIT): array
|
||||
{
|
||||
$finder = service('finder');
|
||||
$result = [];
|
||||
|
||||
// 1) curated picks always lead
|
||||
foreach (($tool['related_slugs'] ?? []) as $slug) {
|
||||
if ($related = $finder->tool((string) $slug)) {
|
||||
$result[$related['slug']] = $related + ['_score' => 1000];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($finder->all() as $candidate) {
|
||||
if ($candidate['id'] === $tool['id'] || isset($result[$candidate['slug']])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$score = 0;
|
||||
if ($tool['primary_input_format'] !== null && $candidate['primary_input_format'] === $tool['primary_input_format']) {
|
||||
$score += 50;
|
||||
}
|
||||
if ($tool['primary_output_format'] !== null && $candidate['primary_output_format'] === $tool['primary_output_format']) {
|
||||
$score += 40;
|
||||
}
|
||||
if ((int) $candidate['category_id'] === (int) $tool['category_id']) {
|
||||
$score += 25;
|
||||
}
|
||||
// small popularity tiebreaker keeps ordering stable and useful
|
||||
$score += min(10, (int) round(((int) $candidate['popularity']) / 500));
|
||||
|
||||
if ($score >= 25) {
|
||||
$candidate['_score'] = $score;
|
||||
$result[$candidate['slug']] = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
usort($result, static fn (array $a, array $b): int => $b['_score'] <=> $a['_score']);
|
||||
|
||||
return array_slice(array_map(static function (array $t): array {
|
||||
unset($t['_score']);
|
||||
|
||||
return $t;
|
||||
}, $result), 0, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format-conversion cross links: for a converter like mp4-to-mp3,
|
||||
* surface sibling conversions from the same input (mp4-to-wav,
|
||||
* mp4-to-webm ...) and to the same output from other inputs.
|
||||
*
|
||||
* @return list<array>
|
||||
*/
|
||||
public function conversionsAround(array $tool, int $limit = 6): array
|
||||
{
|
||||
if ($tool['kind'] !== 'upload' || $tool['operation'] !== 'convert') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$finder = service('finder');
|
||||
$sameInput = [];
|
||||
$sameOutput = [];
|
||||
|
||||
foreach ($finder->all() as $candidate) {
|
||||
if ($candidate['id'] === $tool['id'] || $candidate['operation'] !== 'convert') {
|
||||
continue;
|
||||
}
|
||||
if ($tool['primary_input_format'] !== null && $candidate['primary_input_format'] === $tool['primary_input_format']) {
|
||||
$sameInput[] = $candidate;
|
||||
} elseif ($tool['primary_output_format'] !== null && $candidate['primary_output_format'] === $tool['primary_output_format']) {
|
||||
$sameOutput[] = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
usort($sameInput, static fn ($a, $b) => $b['popularity'] <=> $a['popularity']);
|
||||
usort($sameOutput, static fn ($a, $b) => $b['popularity'] <=> $a['popularity']);
|
||||
|
||||
return array_slice([...$sameInput, ...$sameOutput], 0, $limit);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user