HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
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;
    }
}