Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 93
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Category
0.00% covered (danger)
0.00%
0 / 93
0.00% covered (danger)
0.00%
0 / 7
930
0.00% covered (danger)
0.00%
0 / 1
 save
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
110
 update
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 delete
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 toArray
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 fromArray
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
 getById
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getAll
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace BuyerKiosk\Support;
4
5use \PDO;
6use \PDOException;
7
8/**
9 * Category - Support category model
10 *
11 * Manages support categories with hierarchical structure (parent/child),
12 * icons, and sorting for the knowledge base system.
13 */
14class Category
15{
16    /**
17     * @var int|null Category ID
18     */
19    public $id;
20
21    /**
22     * @var string Category name
23     */
24    public $name;
25
26    /**
27     * @var string URL-friendly slug
28     */
29    public $slug;
30
31    /**
32     * @var string|null Category description
33     */
34    public $description;
35
36    /**
37     * @var string|null Icon identifier (e.g., "book", "settings", "help")
38     */
39    public $icon;
40
41    /**
42     * @var int Sort order for display
43     */
44    public $sortOrder;
45
46    /**
47     * @var int|null Parent category ID (NULL for top-level categories)
48     */
49    public $parentId;
50
51    /**
52     * @var bool Whether category is active/visible
53     */
54    public $isActive;
55
56    /**
57     * @var string Created timestamp
58     */
59    public $createdAt;
60
61    /**
62     * @var string|null Updated timestamp
63     */
64    public $updatedAt;
65
66    /**
67     * Save category (insert or update)
68     *
69     * Uses INSERT or UPDATE based on whether ID is set
70     *
71     * @param PDO $db Database connection
72     * @return bool True if successful
73     */
74    public function save(PDO $db): bool
75    {
76        if (!isset($this->name) || !isset($this->slug)) {
77            error_log("Category::save error: name and slug are required");
78            return false;
79        }
80
81        // Set defaults
82        if (!isset($this->isActive)) {
83            $this->isActive = true;
84        }
85        if (!isset($this->sortOrder)) {
86            $this->sortOrder = 0;
87        }
88
89        try {
90            if ($this->id) {
91                // Update existing category
92                $stmt = $db->prepare("
93                    UPDATE support_categories
94                    SET name = :name,
95                        slug = :slug,
96                        description = :description,
97                        icon = :icon,
98                        sort_order = :sortOrder,
99                        parent_id = :parentId,
100                        is_active = :isActive
101                    WHERE id = :id
102                ");
103
104                return $stmt->execute([
105                    ':id' => $this->id,
106                    ':name' => $this->name,
107                    ':slug' => $this->slug,
108                    ':description' => $this->description,
109                    ':icon' => $this->icon,
110                    ':sortOrder' => $this->sortOrder,
111                    ':parentId' => $this->parentId,
112                    ':isActive' => $this->isActive ? 1 : 0
113                ]);
114            } else {
115                // Insert new category
116                $stmt = $db->prepare("
117                    INSERT INTO support_categories
118                    (name, slug, description, icon, sort_order, parent_id, is_active)
119                    VALUES
120                    (:name, :slug, :description, :icon, :sortOrder, :parentId, :isActive)
121                ");
122
123                $result = $stmt->execute([
124                    ':name' => $this->name,
125                    ':slug' => $this->slug,
126                    ':description' => $this->description,
127                    ':icon' => $this->icon,
128                    ':sortOrder' => $this->sortOrder,
129                    ':parentId' => $this->parentId,
130                    ':isActive' => $this->isActive ? 1 : 0
131                ]);
132
133                if ($result) {
134                    $this->id = (int) $db->lastInsertId();
135                }
136
137                return $result;
138            }
139        } catch (PDOException $e) {
140            error_log("Category::save error: " . $e->getMessage());
141            return false;
142        }
143    }
144
145    /**
146     * Update category
147     *
148     * @param PDO $db Database connection
149     * @return bool True if successful
150     */
151    public function update(PDO $db): bool
152    {
153        return $this->save($db);
154    }
155
156    /**
157     * Delete category
158     *
159     * Note: This will fail if there are articles referencing this category
160     * due to foreign key constraints
161     *
162     * @param PDO $db Database connection
163     * @return bool True if successful
164     */
165    public function delete(PDO $db): bool
166    {
167        if (!isset($this->id)) {
168            error_log("Category::delete error: id is required");
169            return false;
170        }
171
172        try {
173            $stmt = $db->prepare("DELETE FROM support_categories WHERE id = :id");
174            return $stmt->execute([':id' => $this->id]);
175        } catch (PDOException $e) {
176            error_log("Category::delete error: " . $e->getMessage());
177            return false;
178        }
179    }
180
181    /**
182     * Convert Category to array for API responses
183     *
184     * @return array Associative array representation
185     */
186    public function toArray(): array
187    {
188        return [
189            'id' => $this->id,
190            'name' => $this->name,
191            'slug' => $this->slug,
192            'description' => $this->description,
193            'icon' => $this->icon,
194            'sortOrder' => $this->sortOrder,
195            'parentId' => $this->parentId,
196            'isActive' => (bool) $this->isActive,
197            'createdAt' => $this->createdAt,
198            'updatedAt' => $this->updatedAt
199        ];
200    }
201
202    /**
203     * Create a Category object from an array
204     *
205     * Supports both snake_case (from database) and camelCase (from API) keys
206     *
207     * @param array $data Associative array of category data
208     * @return self
209     */
210    public static function fromArray(array $data): self
211    {
212        $category = new self();
213        $category->id = isset($data['id']) ? (int) $data['id'] : null;
214        $category->name = $data['name'];
215        $category->slug = $data['slug'];
216        $category->description = $data['description'] ?? null;
217        $category->icon = $data['icon'] ?? null;
218        $category->sortOrder = isset($data['sort_order']) ? (int) $data['sort_order'] : (isset($data['sortOrder']) ? (int) $data['sortOrder'] : 0);
219        $category->parentId = isset($data['parent_id']) ? (int) $data['parent_id'] : (isset($data['parentId']) ? (int) $data['parentId'] : null);
220        $category->isActive = isset($data['is_active']) ? filter_var($data['is_active'], FILTER_VALIDATE_BOOLEAN) : (isset($data['isActive']) ? filter_var($data['isActive'], FILTER_VALIDATE_BOOLEAN) : true);
221        $category->createdAt = $data['created_at'] ?? $data['createdAt'] ?? null;
222        $category->updatedAt = $data['updated_at'] ?? $data['updatedAt'] ?? null;
223
224        return $category;
225    }
226
227    /**
228     * Get category by ID
229     *
230     * @param PDO $db Database connection
231     * @param int $id Category ID
232     * @return self|null Category object or null if not found
233     */
234    public static function getById(PDO $db, int $id): ?self
235    {
236        try {
237            $stmt = $db->prepare("
238                SELECT * FROM support_categories
239                WHERE id = :id
240            ");
241            $stmt->execute([':id' => $id]);
242
243            $row = $stmt->fetch(PDO::FETCH_ASSOC);
244            if (!$row) {
245                return null;
246            }
247
248            return self::fromArray($row);
249        } catch (PDOException $e) {
250            error_log("Category::getById error: " . $e->getMessage());
251            return null;
252        }
253    }
254
255    /**
256     * Get all categories
257     *
258     * @param PDO $db Database connection
259     * @param bool $activeOnly Whether to return only active categories
260     * @return array Array of Category objects
261     */
262    public static function getAll(PDO $db, bool $activeOnly = true): array
263    {
264        try {
265            $sql = "SELECT * FROM support_categories";
266
267            if ($activeOnly) {
268                $sql .= " WHERE is_active = 1";
269            }
270
271            $sql .= " ORDER BY sort_order ASC, name ASC";
272
273            $stmt = $db->query($sql);
274            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
275
276            $categories = [];
277            foreach ($rows as $row) {
278                $categories[] = self::fromArray($row);
279            }
280
281            return $categories;
282        } catch (PDOException $e) {
283            error_log("Category::getAll error: " . $e->getMessage());
284            return [];
285        }
286    }
287}