Lines 89.83% 53 / 59
Methods 66.66% 6 / 9
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 createFromDatabaseDriver 100.00% 1 / 1 100.00% 1 / 1 1
 assertCanCreateFaq 100.00% 1 / 1 100.00% 1 / 1 1
 assertCanCreateCategory 100.00% 6 / 6 100.00% 1 / 1 1
 assertCanCreateUser 100.00% 1 / 1 100.00% 1 / 1 1
 assertCanStoreAttachment 92.85% 13 / 14 0.00% 0 / 1 4.01
 assertCountWithinLimit 100.00% 19 / 19 100.00% 1 / 1 4
 readAttachmentSizeBytes 70.00% 7 / 10 0.00% 0 / 1 2.11
 extractFirstInt 66.66% 4 / 6 0.00% 0 / 1 5.93
25final readonly class TenantQuotaEnforcer
26{
27    public function __construct(
28        private DatabaseDriver $databaseDriver,
29        private TenantContext $tenantContext,
30    ) {
31    }
32
33    public static function createFromDatabaseDriver(DatabaseDriver $databaseDriver): self
34    {
35        return new self($databaseDriver, new TenantContextResolver()->resolve());
36    }
37
38    public function assertCanCreateFaq(): void
39    {
40        $this->assertCountWithinLimit($this->tenantContext->getQuotas()->getMaxFaqs(), 'faqdata', 'maxFaqs', 'FAQs');
41    }
42
43    public function assertCanCreateCategory(): void
44    {
45        $this->assertCountWithinLimit(
46            $this->tenantContext->getQuotas()->getMaxCategories(),
47            'faqcategories',
48            'maxCategories',
49            'categories',
50        );
51    }
52
53    public function assertCanCreateUser(): void
54    {
55        $this->assertCountWithinLimit($this->tenantContext->getQuotas()->getMaxUsers(), 'faquser', 'maxUsers', 'users');
56    }
57
58    public function assertCanStoreAttachment(int $newAttachmentSizeBytes): void
59    {
60        $maxAttachmentSizeMb = $this->tenantContext->getQuotas()->getMaxAttachmentSize();
61        if ($maxAttachmentSizeMb === null) {
62            return;
63        }
64
65        $currentSizeBytes = $this->readAttachmentSizeBytes();
66        $maxSizeBytes = $maxAttachmentSizeMb > (int) (PHP_INT_MAX / (1024 * 1024))
67            ? PHP_INT_MAX
68            : $maxAttachmentSizeMb * 1024 * 1024;
69
70        if (($currentSizeBytes + $newAttachmentSizeBytes) > $maxSizeBytes) {
71            throw new QuotaExceededException(sprintf(
72                'Tenant quota exceeded for maxAttachmentSize: %d MB (used: %d bytes, requested: %d bytes)',
73                $maxAttachmentSizeMb,
74                $currentSizeBytes,
75                $newAttachmentSizeBytes,
76            ));
77        }
78    }
79
80    private function assertCountWithinLimit(?int $limit, string $table, string $quotaKey, string $resourceName): void
81    {
82        if ($limit === null) {
83            return;
84        }
85
86        $query = sprintf('SELECT COUNT(1) AS amount FROM %s%s', $this->tenantContext->getTablePrefix(), $table);
87        $result = $this->databaseDriver->query($query);
88
89        if (!$result) {
90            throw new RuntimeException(sprintf(
91                'Failed to evaluate tenant quota for %s: %s',
92                $resourceName,
93                $this->databaseDriver->error(),
94            ));
95        }
96
97        $row = $this->databaseDriver->fetchArray($result);
98        $currentCount = $this->extractFirstInt($row);
99
100        if ($currentCount >= $limit) {
101            throw new QuotaExceededException(sprintf(
102                'Tenant quota exceeded for %s: limit=%d current=%d',
103                $quotaKey,
104                $limit,
105                $currentCount,
106            ));
107        }
108    }
109
110    private function readAttachmentSizeBytes(): int
111    {
112        $query = sprintf(
113            'SELECT COALESCE(SUM(filesize), 0) AS amount FROM %sfaqattachment',
114            $this->tenantContext->getTablePrefix(),
115        );
116        $result = $this->databaseDriver->query($query);
117
118        if (!$result) {
119            throw new RuntimeException(
120                'Failed to evaluate tenant quota for attachments: ' . $this->databaseDriver->error(),
121            );
122        }
123
124        return $this->extractFirstInt($this->databaseDriver->fetchArray($result));
125    }
126
127    private function extractFirstInt(array|false|null $row): int
128    {
129        if ($row === false || $row === null || $row === []) {
130            return 0;
131        }
132
133        $firstValue = reset($row);
134        if ($firstValue === false) {
135            return 0;
136        }
137
138        return (int) $firstValue;
139    }
140}