Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
RecordPinService
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
5 / 5
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 togglePin
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 isPinned
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 hasPinnedColumn
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 assertSafeTable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
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\Core\Engine\Application\Service;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use InvalidArgumentException;
12use PDO;
13use Throwable;
14
15/**
16 * Manages shared record pin flags (is_pinned column).
17 *
18 * @package App\Core\Engine\Application\Service
19 */
20final readonly class RecordPinService
21{
22    private const string IDENTIFIER_PATTERN = '/^\w+$/';
23
24    /**
25     * @param PDO $pdo Active database connection.
26     */
27    public function __construct(
28        private PDO $pdo
29    ) {
30    }
31
32    /**
33     * Toggles pinned state for a given record.
34     *
35     * @param string $tableName Database table name.
36     * @param int    $recordId  Target record ID.
37     * @return bool True if record is now pinned, false if unpinned.
38     * @throws InvalidArgumentException When table identifier is invalid.
39     */
40    public function togglePin(string $tableName, int $recordId): bool
41    {
42        $this->assertSafeTable($tableName);
43
44        $currentState = $this->isPinned($tableName, $recordId);
45        $newState = $currentState ? 0 : 1;
46
47        $sql = "UPDATE `{$tableName}` SET `is_pinned` = :state WHERE `id` = :id";
48        $stmt = $this->pdo->prepare($sql);
49        $stmt->execute([
50            ':state' => $newState,
51            ':id'    => $recordId,
52        ]);
53
54        return $newState === 1;
55    }
56
57    /**
58     * Checks whether a record is pinned.
59     *
60     * @param string $tableName Database table name.
61     * @param int    $recordId  Target record ID.
62     * @return bool True if pinned.
63     */
64    public function isPinned(string $tableName, int $recordId): bool
65    {
66        $this->assertSafeTable($tableName);
67
68        $sql = "SELECT `is_pinned` FROM `{$tableName}` WHERE `id` = :id LIMIT 1";
69        $stmt = $this->pdo->prepare($sql);
70        $stmt->execute([':id' => $recordId]);
71        $val = $stmt->fetchColumn();
72
73        return $val !== false && (int) $val === 1;
74    }
75
76    /**
77     * Checks if the table possesses an is_pinned column.
78     *
79     * @param string $tableName Database table name.
80     * @return bool True if is_pinned column exists.
81     */
82    public function hasPinnedColumn(string $tableName): bool
83    {
84        if (!preg_match(self::IDENTIFIER_PATTERN, $tableName)) {
85            return false;
86        }
87
88        try {
89            $stmt = $this->pdo->prepare("SELECT `is_pinned` FROM `{$tableName}` LIMIT 0");
90            $stmt->execute();
91            return true;
92        } catch (Throwable) {
93            return false;
94        }
95    }
96
97    /**
98     * Asserts that the table name is a safe SQL identifier.
99     *
100     * @param string $tableName Table name to validate.
101     * @throws InvalidArgumentException If identifier is unsafe.
102     */
103    private function assertSafeTable(string $tableName): void
104    {
105        if (!preg_match(self::IDENTIFIER_PATTERN, $tableName)) {
106            throw new InvalidArgumentException("Invalid table identifier: {$tableName}");
107        }
108    }
109}