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 */ 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); } }