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,106 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
final class ToolModel extends Model
|
||||
{
|
||||
protected $table = 'tools';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
protected $allowedFields = [
|
||||
'category_id', 'kind', 'operation', 'slug', 'name', 'short_description',
|
||||
'description', 'input_formats', 'output_formats', 'primary_input_format',
|
||||
'primary_output_format', 'icon', 'seo_title', 'seo_description', 'h1',
|
||||
'intro', 'how_to', 'faqs', 'related_slugs', 'aliases', 'requires_binaries',
|
||||
'accepts_mimes', 'max_upload_mb', 'popularity', 'use_count', 'status',
|
||||
'is_featured', 'sort_order',
|
||||
];
|
||||
protected $validationRules = [
|
||||
'slug' => '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<array> */
|
||||
public function activeInCategory(int $categoryId): array
|
||||
{
|
||||
return $this->where('category_id', $categoryId)
|
||||
->where('status', 'active')
|
||||
->orderBy('popularity', 'DESC')
|
||||
->orderBy('name', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/** @return list<array> */
|
||||
public function popular(int $limit = 8): array
|
||||
{
|
||||
return $this->where('status', 'active')
|
||||
->orderBy('popularity', 'DESC')
|
||||
->orderBy('use_count', 'DESC')
|
||||
->limit($limit)
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/** @return list<array> */
|
||||
public function featured(int $limit = 8): array
|
||||
{
|
||||
return $this->where('status', 'active')
|
||||
->where('is_featured', 1)
|
||||
->orderBy('popularity', 'DESC')
|
||||
->limit($limit)
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/** @return list<array> */
|
||||
public function recent(int $limit = 6): array
|
||||
{
|
||||
return $this->where('status', 'active')
|
||||
->orderBy('created_at', 'DESC')
|
||||
->limit($limit)
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/** @return list<array> 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<array>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user