File: /home/xedaptot/ai.naniguide.com/app/Services/PageTemplateService.php
<?php
namespace App\Services;
use App\Dto\Refactor\PageTemplate\CopyPageTemplateDto;
use App\Dto\Refactor\PageTemplate\CreatePageTemplateDto;
use App\Dto\Refactor\PageTemplate\DeletePageTemplatesDto;
use App\Dto\Refactor\PageTemplate\RenamePageTemplateDto;
use App\Dto\Refactor\PageTemplate\SaveContentDto;
use App\Model\PageTemplate;
use App\Model\Template;
use App\Model\TemplateCategory;
use Illuminate\Support\Facades\Gate;
/**
* PageTemplateService — business logic for the PageTemplate wrapper CRUD.
*
* Rules (matches CLAUDE.md DEV.md layering):
* - Controller → Service → Model. No business logic in controllers or models.
* - Cascade deletes always go through TemplateService::for() to keep the
* wrapper ↔ Template 1:1 invariant + never leave orphan Template rows.
* - Admin-curated derivatives are always tagged Extended (never Base).
*/
class PageTemplateService
{
/**
* Create a new PageTemplate by cloning a base PageTemplate's Template content.
*
* @return PageTemplate|null null if the base uid doesn't resolve
*/
public function createFromBase(CreatePageTemplateDto $dto): ?PageTemplate
{
$base = PageTemplate::findByUid($dto->baseTemplateUid);
if (!$base || !$base->template) {
return null;
}
return $this->cloneFromTemplate($dto->name, $base->template);
}
/**
* Copy an existing PageTemplate to a new PageTemplate with a new display name.
*
* @return PageTemplate|null null if the source uid doesn't resolve
*/
public function copyFrom(CopyPageTemplateDto $dto): ?PageTemplate
{
$source = PageTemplate::findByUid($dto->sourceUid);
if (!$source || !$source->template) {
return null;
}
return $this->cloneFromTemplate($dto->newName, $source->template);
}
/**
* Rename a PageTemplate (updates the wrapper's display name only; the underlying
* Template row keeps its existing name — that's the editor's internal title).
*/
public function rename(RenamePageTemplateDto $dto): bool
{
$pt = PageTemplate::findByUid($dto->uid);
if (!$pt) {
return false;
}
$pt->name = $dto->newName;
$pt->save();
return true;
}
/**
* Bulk-delete PageTemplates by UIDs. Each row goes through TemplateService for
* atomic wrapper+Template cleanup. Per-item authorization is re-checked so a
* user with partial permissions only deletes rows they own/can manage.
*
* @return int Number of PageTemplates actually deleted
*/
public function bulkDelete(DeletePageTemplatesDto $dto): int
{
if (empty($dto->uids)) {
return 0;
}
$deleted = 0;
foreach (PageTemplate::whereIn('uid', $dto->uids)->get() as $pt) {
if (Gate::denies('delete', $pt)) {
continue;
}
TemplateService::for($pt)->deleteSubjectAndTemplate();
$deleted++;
}
return $deleted;
}
/**
* Save the builder's round-tripped content to the PageTemplate's Template.
*
* Returns a result array with keys:
* ok: bool
* errors: \Illuminate\Support\MessageBag|array (populated only on failure)
*/
public function saveContent(SaveContentDto $dto): array
{
$pt = PageTemplate::findByUid($dto->uid);
if (!$pt || !$pt->template) {
return ['ok' => false, 'errors' => ['uid' => ['Page template not found.']]];
}
$validator = $pt->template->updateContent($dto->json, $dto->content);
if ($validator->fails()) {
return ['ok' => false, 'errors' => $validator->errors()];
}
return ['ok' => true, 'errors' => []];
}
/**
* Internal — create a fresh PageTemplate cloned from an existing Template, and
* tag the result as Extended (admin-curated derivatives are always Extended,
* never Base — Base is reserved for shipped-seed rows from TemplateSeeder).
*/
private function cloneFromTemplate(string $name, Template $source): PageTemplate
{
$new = new PageTemplate();
$new->name = $name;
$newTemplate = TemplateService::for($new)->setTemplate($source, $name);
$extended = TemplateCategory::whereName('Extended')->first();
if ($extended && $newTemplate) {
$base = TemplateCategory::whereName('Base')->first();
if ($base) {
$newTemplate->categories()->detach($base->id);
}
if (!$newTemplate->categories()->where('template_categories.id', $extended->id)->exists()) {
$newTemplate->categories()->attach($extended->id);
}
}
return $new;
}
/**
* Factory used by TemplateSeeder to register shipped Base-tier PageTemplates.
* Different from createFromBase(): seeder supplies the ALREADY-CREATED Template
* row, and seeded entries should NOT be auto-promoted to Extended.
*/
public function seedBaseTemplate(string $name, Template $template): PageTemplate
{
$pt = new PageTemplate();
$pt->name = $name;
$newTemplate = TemplateService::for($pt)->setTemplate($template, $name);
// Shipped samples default to Extended (commerce templates are rich, not base-tier).
// TemplateSeeder may override by passing to createFromBase instead if needed.
$extended = TemplateCategory::whereName('Extended')->first();
if ($extended && $newTemplate) {
if (!$newTemplate->categories()->where('template_categories.id', $extended->id)->exists()) {
$newTemplate->categories()->attach($extended->id);
}
}
return $pt;
}
}