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,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\CatalogBuilder;
|
||||
use App\Models\CategoryModel;
|
||||
use App\Models\GuideModel;
|
||||
use App\Models\ToolModel;
|
||||
|
||||
/**
|
||||
* Admin base: shared layout rendering + nav context.
|
||||
* All children sit behind the admin-auth filter.
|
||||
*/
|
||||
abstract class AdminBase extends BaseController
|
||||
{
|
||||
protected function render(string $view, array $data = []): string
|
||||
{
|
||||
$data['viewContent'] = view('admin/' . $view, $data);
|
||||
|
||||
return view('layout', ['seo' => $this->seo] + $data);
|
||||
}
|
||||
|
||||
/** Shared tool-form field list. */
|
||||
protected function toolFields(): array
|
||||
{
|
||||
return [
|
||||
'slug', 'name', 'short_description', 'description', 'icon',
|
||||
'seo_title', 'seo_description', 'h1', 'intro', 'popularity', 'status',
|
||||
];
|
||||
}
|
||||
|
||||
/** Build a registry row from POST for admin-created tools. */
|
||||
protected function toolFromInput(array $post, int $categoryId): array
|
||||
{
|
||||
// reuse the builder's content generator so admin-created tools get
|
||||
// complete SEO fields even when left blank
|
||||
$generated = null;
|
||||
if (! empty($post['format_in']) && ! empty($post['format_out'])) {
|
||||
foreach (CatalogBuilder::build() as $candidate) {
|
||||
if ($candidate['slug'] === strtolower($post['format_in']) . '-to-' . strtolower($post['format_out'])) {
|
||||
$generated = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'category_id' => $categoryId,
|
||||
'kind' => $post['kind'] ?? ($generated['kind'] ?? 'upload'),
|
||||
'operation' => $post['operation'] ?? ($generated['operation'] ?? 'convert'),
|
||||
'slug' => $post['slug'] ?? ($generated['slug'] ?? ''),
|
||||
'name' => $post['name'],
|
||||
'short_description' => $post['short_description'] ?? '',
|
||||
'description' => $post['description'] ?? null,
|
||||
'input_formats' => json_encode($generated['input_formats'] ?? []),
|
||||
'output_formats' => json_encode($generated['output_formats'] ?? []),
|
||||
'primary_input_format' => $post['format_in'] ?? ($generated['primary_input_format'] ?? null),
|
||||
'primary_output_format' => $post['format_out'] ?? ($generated['primary_output_format'] ?? null),
|
||||
'icon' => $post['icon'] ?? 'file',
|
||||
'seo_title' => $post['seo_title'] ?: ($generated['seo_title'] ?? ($post['name'] . ' – Free Online Tool')),
|
||||
'seo_description' => $post['seo_description'] ?: mb_substr((string) ($post['short_description'] ?? ''), 0, 300),
|
||||
'h1' => $post['h1'] ?: $post['name'],
|
||||
'intro' => $post['intro'] ?: ($generated['intro'] ?? null),
|
||||
'how_to' => json_encode($generated['how_to'] ?? []),
|
||||
'faqs' => json_encode($generated['faqs'] ?? []),
|
||||
'related_slugs' => json_encode([]),
|
||||
'aliases' => json_encode(array_filter(array_map('trim', explode(',', (string) ($post['aliases'] ?? ''))))),
|
||||
'requires_binaries' => json_encode($generated['requires_binaries'] ?? []),
|
||||
'accepts_mimes' => json_encode($generated['accepts_mimes'] ?? []),
|
||||
'popularity' => (int) ($post['popularity'] ?? 1000),
|
||||
'status' => in_array(($post['status'] ?? ''), ['active', 'draft', 'inactive'], true) ? $post['status'] : 'active',
|
||||
];
|
||||
}
|
||||
|
||||
protected function categories(): array
|
||||
{
|
||||
return model(CategoryModel::class)->findAll();
|
||||
}
|
||||
|
||||
protected function toolsModel(): ToolModel
|
||||
{
|
||||
return model(ToolModel::class);
|
||||
}
|
||||
|
||||
protected function guidesModel(): GuideModel
|
||||
{
|
||||
return model(GuideModel::class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user