Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.48% covered (warning)
83.48%
96 / 115
20.00% covered (danger)
20.00%
1 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
SqlPdfTemplateVersionRepository
83.33% covered (warning)
83.33%
95 / 114
20.00% covered (danger)
20.00%
1 / 5
19.50
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createVersion
73.68% covered (warning)
73.68%
42 / 57
0.00% covered (danger)
0.00%
0 / 1
3.16
 findVersionsByTemplateId
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
5.01
 findById
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 rollbackToVersion
93.75% covered (success)
93.75%
30 / 32
0.00% covered (danger)
0.00%
0 / 1
6.01
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Modules\Pdf\Infrastructure\Repository;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Database\FallbackPdoResolver;
12use App\Modules\Pdf\Domain\Model\PdfTemplate;
13use App\Modules\Pdf\Domain\Model\PdfTemplateVersion;
14use App\Modules\Pdf\Domain\Repository\PdfTemplateRepositoryInterface;
15use App\Modules\Pdf\Domain\Repository\PdfTemplateVersionRepositoryInterface;
16use InvalidArgumentException;
17use PDO;
18
19/**
20 * SQL-Backed Repository Implementation for PDF Template Versions.
21 *
22 * @package App\Modules\Pdf\Infrastructure\Repository
23 */
24final class SqlPdfTemplateVersionRepository implements PdfTemplateVersionRepositoryInterface
25{
26    private const string SELECT_FIELDS = '`id`, `template_id`, `version_number`, `change_summary`, '
27        . '`blocks_json`, `body_html`, `header_html`, `footer_html`, `css_styles`, `special_access`, '
28        . '`created_at`, `created_by`';
29
30    /**
31     * SqlPdfTemplateVersionRepository constructor.
32     *
33     * @param PDO|null                                 $pdo          PDO connection.
34     * @param PdfTemplateRepositoryInterface|null      $templateRepo Parent template repository.
35     */
36    public function __construct(
37        private ?PDO $pdo = null,
38        private ?PdfTemplateRepositoryInterface $templateRepo = null
39    ) {
40        $this->pdo = $pdo ?? FallbackPdoResolver::resolveDefaultConnection();
41        $this->templateRepo = $templateRepo ?? new SqlPdfTemplateRepository($this->pdo);
42    }
43
44    /**
45     * {@inheritdoc}
46     */
47    public function createVersion(
48        PdfTemplate $template,
49        ?string $summary = null,
50        int $userId = 1
51    ): PdfTemplateVersion {
52        if ($this->pdo === null) {
53            return new PdfTemplateVersion(
54                id: 1,
55                templateId: $template->id,
56                versionNumber: 1,
57                changeSummary: $summary,
58                blocksJson: $template->blocksJson,
59                bodyHtml: $template->bodyHtml,
60                headerHtml: $template->headerHtml,
61                footerHtml: $template->footerHtml,
62                cssStyles: $template->cssStyles,
63                recordStatus: 1,
64                createdAt: date('Y-m-d H:i:s'),
65                createdBy: $userId
66            );
67        }
68
69        $stmtMax = $this->pdo->prepare(
70            'SELECT COALESCE(MAX(`version_number`), 0) '
71            . 'FROM `a_mod_pdf_template_versions_records` WHERE `template_id` = :tid'
72        );
73        $stmtMax->execute([':tid' => $template->id]);
74        $maxVer = (int)$stmtMax->fetchColumn();
75        $nextVer = $maxVer + 1;
76
77        $blocksString = $template->blocksJson !== null
78            ? json_encode($template->blocksJson, JSON_THROW_ON_ERROR)
79            : null;
80
81        $stmt = $this->pdo->prepare(
82            'INSERT INTO `a_mod_pdf_template_versions_records` ('
83            . '`template_id`, `version_number`, `change_summary`, `blocks_json`, `body_html`, '
84            . '`header_html`, `footer_html`, `css_styles`, `created_by`'
85            . ') VALUES (:tid, :vnum, :summary, :blocks, :body, :header, :footer, :css, :uid)'
86        );
87
88        $stmt->execute([
89            ':tid'     => $template->id,
90            ':vnum'    => $nextVer,
91            ':summary' => $summary,
92            ':blocks'  => $blocksString,
93            ':body'    => $template->bodyHtml,
94            ':header'  => $template->headerHtml,
95            ':footer'  => $template->footerHtml,
96            ':css'     => $template->cssStyles,
97            ':uid'     => $userId,
98        ]);
99
100        $newId = (int)$this->pdo->lastInsertId();
101
102        return new PdfTemplateVersion(
103            id: $newId,
104            templateId: $template->id,
105            versionNumber: $nextVer,
106            changeSummary: $summary,
107            blocksJson: $template->blocksJson,
108            bodyHtml: $template->bodyHtml,
109            headerHtml: $template->headerHtml,
110            footerHtml: $template->footerHtml,
111            cssStyles: $template->cssStyles,
112            recordStatus: 1,
113            createdAt: date('Y-m-d H:i:s'),
114            createdBy: $userId
115        );
116    }
117
118    /**
119     * {@inheritdoc}
120     */
121    public function findVersionsByTemplateId(int $templateId): array
122    {
123        if ($this->pdo === null) {
124            return [];
125        }
126
127        $stmt = $this->pdo->prepare(
128            'SELECT ' . self::SELECT_FIELDS . ' FROM `a_mod_pdf_template_versions_records` '
129            . 'WHERE `template_id` = :tid ORDER BY `version_number` DESC'
130        );
131        $stmt->execute([':tid' => $templateId]);
132        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
133
134        $versions = [];
135        if (is_array($rows)) {
136            foreach ($rows as $row) {
137                if (is_array($row)) {
138                    $versions[] = PdfTemplateVersion::fromDatabaseRow($row);
139                }
140            }
141        }
142
143        return $versions;
144    }
145
146    /**
147     * {@inheritdoc}
148     */
149    public function findById(int $versionId): ?PdfTemplateVersion
150    {
151        if ($this->pdo === null) {
152            return null;
153        }
154
155        $stmt = $this->pdo->prepare(
156            'SELECT ' . self::SELECT_FIELDS . ' FROM `a_mod_pdf_template_versions_records` '
157            . 'WHERE `id` = :id LIMIT 1'
158        );
159        $stmt->execute([':id' => $versionId]);
160        $row = $stmt->fetch(PDO::FETCH_ASSOC);
161
162        return is_array($row) ? PdfTemplateVersion::fromDatabaseRow($row) : null;
163    }
164
165    /**
166     * {@inheritdoc}
167     */
168    public function rollbackToVersion(int $templateId, int $versionId, int $userId = 1): PdfTemplate
169    {
170        $version = $this->findById($versionId);
171        if ($version === null || $version->templateId !== $templateId) {
172            throw new InvalidArgumentException(
173                "PDF Version ID {$versionId} does not belong to Template ID {$templateId}."
174            );
175        }
176
177        if ($this->pdo !== null) {
178            $blocksString = $version->blocksJson !== null
179                ? json_encode($version->blocksJson, JSON_THROW_ON_ERROR)
180                : null;
181
182            $stmt = $this->pdo->prepare(
183                'UPDATE `a_mod_pdf_template_records` SET '
184                . '`body_html` = :body, `header_html` = :header, `footer_html` = :footer, '
185                . '`blocks_json` = :blocks, `css_styles` = :css, `updated_at` = NOW(6) '
186                . 'WHERE `id` = :id LIMIT 1'
187            );
188            $stmt->execute([
189                ':body'   => $version->bodyHtml,
190                ':header' => $version->headerHtml,
191                ':footer' => $version->footerHtml,
192                ':blocks' => $blocksString,
193                ':css'    => $version->cssStyles,
194                ':id'     => $templateId,
195            ]);
196        }
197
198        $updated = $this->templateRepo?->findById($templateId);
199        if ($updated === null) {
200            throw new InvalidArgumentException("Failed to reload template ID {$templateId} after rollback.");
201        }
202
203        $this->createVersion(
204            $updated,
205            sprintf('Rollback to revision #%d', $version->versionNumber),
206            $userId
207        );
208
209        return $updated;
210    }
211}