Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.89% covered (success)
97.89%
93 / 95
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
CommentRelationHydrator
97.87% covered (success)
97.87%
92 / 94
75.00% covered (warning)
75.00%
6 / 8
16
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getSelectCommentsBase
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getSelectAttachmentsBase
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 hydrateCommentsWithRelations
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 loadAttachmentsForCommentIds
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 loadParentsForRows
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
1 / 1
3
 formatAuthorName
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 hydrateAttachment
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
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\Comments\Infrastructure\Hydrator;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Comments\Domain\Model\Comment;
12use App\Modules\Comments\Domain\Model\CommentAttachment;
13use PDO;
14
15/**
16 * Hydrates Comment domain aggregates with attachments, author profiles, and parent threads.
17 *
18 * Encapsulates batch database eager-loading for Comment and CommentAttachment entity graphs.
19 *
20 * @package App\Modules\Comments\Infrastructure\Hydrator
21 */
22final readonly class CommentRelationHydrator
23{
24    private string $tableAttachments;
25    private string $tableComments;
26    private string $tableUsers;
27
28    /**
29     * CommentRelationHydrator constructor.
30     *
31     * @param PDO    $pdo         Active database connection.
32     * @param string $tablePrefix Database table prefix (default 'c_').
33     */
34    public function __construct(
35        private PDO $pdo,
36        private string $tablePrefix = 'c_'
37    ) {
38        $this->tableAttachments = $this->tablePrefix . 'mod_comment_attachments_records';
39        $this->tableComments = $this->tablePrefix . 'mod_comments_records';
40        $this->tableUsers = $this->tablePrefix . 'mod_users_records';
41    }
42
43    private function getSelectCommentsBase(): string
44    {
45        return 'SELECT c.id, c.parent_id, c.content, c.is_pinned, c.is_verified, ' .
46            'c.target_module, c.target_record_id, c.related_party_ref, c.company_id, ' .
47            'c.partner_id, c.contact_id, c.process_ref, c.project_id, c.contract_id, ' .
48            'c.subprocess_ref, c.ticket_id, c.task_id, c.stage_id, c.owner, ' .
49            'c.created_at, c.updated_at, u.username, u.first_name, u.last_name ' .
50            "FROM `{$this->tableComments}` c " .
51            "LEFT JOIN `{$this->tableUsers}` u ON u.id = c.owner";
52    }
53
54    private function getSelectAttachmentsBase(): string
55    {
56        return 'SELECT id, comment_id, file_name, file_path, file_size, ' .
57            'file_extension, mime_type, token, created_by, created_at ' .
58            "FROM `{$this->tableAttachments}`";
59    }
60
61    /**
62     * Hydrates comments and batch-loads related attachments and parent threads.
63     *
64     * @param array<int, array<string, mixed>> $rows Database result rows.
65     * @return array<int, Comment> Hydrated comment domain models.
66     */
67    public function hydrateCommentsWithRelations(array $rows): array
68    {
69        $commentIds = array_map(static fn(array $r): int => (int) $r['id'], $rows);
70        $attachmentsMap = $this->loadAttachmentsForCommentIds($commentIds);
71        $parentMap = $this->loadParentsForRows($rows);
72
73        $comments = [];
74        foreach ($rows as $row) {
75            $id = (int) $row['id'];
76            $parentId = !empty($row['parent_id']) ? (int) $row['parent_id'] : null;
77            $parent = $parentId !== null ? ($parentMap[$parentId] ?? null) : null;
78            $row['author_name'] = $this->formatAuthorName($row);
79
80            $comments[] = Comment::fromArray($row, $attachmentsMap[$id] ?? [], $parent);
81        }
82
83        return $comments;
84    }
85
86    /**
87     * Eager loads attachments for given comment IDs.
88     *
89     * @param array<int, int> $commentIds
90     * @return array<int, array<int, CommentAttachment>>
91     */
92    public function loadAttachmentsForCommentIds(array $commentIds): array
93    {
94        if ($commentIds === []) {
95            return [];
96        }
97
98        $placeholders = implode(',', array_fill(0, count($commentIds), '?'));
99        $sql = $this->getSelectAttachmentsBase() . " WHERE comment_id IN ({$placeholders})";
100        $stmt = $this->pdo->prepare($sql);
101        $stmt->execute(array_values($commentIds));
102        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
103
104        $map = [];
105        foreach ($rows as $row) {
106            $cid = (int) $row['comment_id'];
107            $map[$cid][] = $this->hydrateAttachment($row);
108        }
109
110        return $map;
111    }
112
113    /**
114     * Eager loads parent comments for hierarchical thread display.
115     *
116     * @param array<int, array<string, mixed>> $rows
117     * @return array<int, Comment>
118     */
119    public function loadParentsForRows(array $rows): array
120    {
121        $parentIds = array_values(array_filter(array_unique(array_map(
122            static fn(array $r): int => (int) ($r['parent_id'] ?? 0),
123            $rows
124        ))));
125
126        if ($parentIds === []) {
127            return [];
128        }
129
130        $placeholders = implode(',', array_fill(0, count($parentIds), '?'));
131        $sql = $this->getSelectCommentsBase() . " WHERE c.id IN ({$placeholders})";
132
133        $stmt = $this->pdo->prepare($sql);
134        $stmt->execute($parentIds);
135        $pRows = $stmt->fetchAll(PDO::FETCH_ASSOC);
136
137        $map = [];
138        foreach ($pRows as $pRow) {
139            $pid = (int) $pRow['id'];
140            $map[$pid] = new Comment(
141                id:                $pid,
142                parentId:          null,
143                content:           (string) $pRow['content'],
144                isPinned:          (bool) ($pRow['is_pinned'] ?? false),
145                isVerified:        (bool) ($pRow['is_verified'] ?? false),
146                targetModule:      (string) $pRow['target_module'],
147                targetRecordId:    (int) $pRow['target_record_id'],
148                relatedPartyRef:   null,
149                companyId:         null,
150                partnerId:         null,
151                contactId:         null,
152                processRef:        null,
153                projectId:         null,
154                contractId:        null,
155                subprocessRef:     null,
156                ticketId:          null,
157                taskId:            null,
158                stageId:           null,
159                owner:             (int) $pRow['owner'],
160                authorName:        $this->formatAuthorName($pRow),
161                authorAvatar:      null,
162                createdAt:         (string) $pRow['created_at'],
163                updatedAt:         (string) $pRow['updated_at']
164            );
165        }
166
167        return $map;
168    }
169
170    /**
171     * Formats display name from author row.
172     *
173     * @param array<string, mixed> $row
174     */
175    public function formatAuthorName(array $row): string
176    {
177        $first = trim((string) ($row['first_name'] ?? ''));
178        $last = trim((string) ($row['last_name'] ?? ''));
179        $full = trim($first . ' ' . $last);
180
181        if ($full !== '') {
182            return $full;
183        }
184
185        return (string) ($row['username'] ?? 'User');
186    }
187
188    /**
189     * Hydrates a single database row into a CommentAttachment model.
190     *
191     * @param array<string, mixed> $row
192     */
193    public function hydrateAttachment(array $row): CommentAttachment
194    {
195        return new CommentAttachment(
196            id:            (int) $row['id'],
197            commentId:     (int) $row['comment_id'],
198            fileName:      (string) $row['file_name'],
199            filePath:      (string) $row['file_path'],
200            fileSize:      (int) $row['file_size'],
201            fileExtension: (string) $row['file_extension'],
202            mimeType:      (string) $row['mime_type'],
203            token:         (string) $row['token'],
204            createdBy:     (int) ($row['created_by'] ?? 1),
205            createdAt:     (string) ($row['created_at'] ?? date('Y-m-d H:i:s'))
206        );
207    }
208}