'required|alpha_dash|max_length[120]|is_unique[tools.slug,id,{id}]', 'name' => 'required|string|max_length[140]', ]; public function findBySlug(string $slug): ?array { return $this->where('slug', mb_strtolower($slug))->where('status', 'active')->first(); } /** @return list */ public function activeInCategory(int $categoryId): array { return $this->where('category_id', $categoryId) ->where('status', 'active') ->orderBy('popularity', 'DESC') ->orderBy('name', 'ASC') ->findAll(); } /** @return list */ public function popular(int $limit = 8): array { return $this->where('status', 'active') ->orderBy('popularity', 'DESC') ->orderBy('use_count', 'DESC') ->limit($limit) ->findAll(); } /** @return list */ public function featured(int $limit = 8): array { return $this->where('status', 'active') ->where('is_featured', 1) ->orderBy('popularity', 'DESC') ->limit($limit) ->findAll(); } /** @return list */ public function recent(int $limit = 6): array { return $this->where('status', 'active') ->orderBy('created_at', 'DESC') ->limit($limit) ->findAll(); } /** @return list ordered A-Z for the directory */ public function allActive(): array { return $this->where('status', 'active')->orderBy('name', 'ASC')->findAll(); } public function countActive(): int { return $this->where('status', 'active')->countAllResults(); } /** * Candidates for the related-tools engine: everything sharing the same * category, plus anything matching a format pair. Scoring happens in * App\Libraries\RelatedTools. * * @return list */ public function candidatesFor(array $tool): array { return $this->where('status', 'active') ->groupStart() ->where('category_id', (int) $tool['category_id']) ->orWhere('primary_input_format', $tool['primary_input_format'] ?? null) ->orWhere('primary_output_format', $tool['primary_output_format'] ?? null) ->groupEnd() ->limit(200) ->findAll(); } public function bumpUseCount(int $id): void { $this->builder()->set('use_count', 'use_count+1', false)->where('id', $id)->update(); } }