Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.00% covered (success)
94.00%
47 / 50
81.82% covered (success)
81.82%
9 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Glossary
94.00% covered (success)
94.00%
47 / 50
81.82% covered (success)
81.82%
9 / 11
22.10
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
 insertItemsIntoContent
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
 setTooltip
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 fetch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fetchAll
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 create
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 update
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 delete
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getLanguage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setLanguage
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 currentLanguage
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * The main glossary class.
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2005-2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2005-09-15
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ;
21
22use phpMyFAQ\Glossary\GlossaryHelper;
23use phpMyFAQ\Glossary\GlossaryRepository;
24use phpMyFAQ\Glossary\GlossaryRepositoryInterface;
25
26/**
27 * Class Glossary
28 *
29 * @package phpMyFAQ
30 */
31class Glossary
32{
33    private string $definition = '';
34
35    private string $language = '';
36
37    /** @var array<string, array<int, array{id:int, language:string, item:string, definition:string}>> */
38    private array $cachedItems = [];
39
40    // Repository to access storage
41    private GlossaryRepositoryInterface $glossaryRepository;
42
43    private GlossaryHelper $glossaryHelper;
44
45    public function __construct(
46        private readonly Configuration $configuration,
47        ?GlossaryRepositoryInterface $glossaryRepository = null,
48    ) {
49        $this->glossaryRepository = $glossaryRepository ?? new GlossaryRepository($this->configuration);
50        $this->glossaryHelper = new GlossaryHelper();
51    }
52
53    /**
54     * Fill the passed string with the current Glossary items.
55     *
56     * @param string $content Content
57     */
58    public function insertItemsIntoContent(string $content = ''): string
59    {
60        // Lazy init in case a test created a mock without running the constructor
61        /* @mago-expect lint:no-isset - typed property may be uninitialized in constructor-less mocks */
62        if (!isset($this->glossaryHelper)) {
63            $this->glossaryHelper = new GlossaryHelper();
64        }
65
66        if ($content === '') {
67            return '';
68        }
69
70        foreach ($this->fetchAll() as $item) {
71            $this->definition = $item['definition'];
72            $quotedItem = preg_quote($item['item'], delimiter: '/');
73            $pattern = '/(^|\W)(' . $quotedItem . ')(\W|$)/';
74
75            $replaced = Strings::preg_replace_callback(
76                pattern: $pattern,
77                callback: $this->setTooltip(...),
78                subject: $content,
79                limit: 1,
80            );
81            $content = is_string($replaced) ? $replaced : $content;
82        }
83
84        return $content;
85    }
86
87    /**
88     * Callback function for filtering HTML from URLs and images.
89     *
90     * @param array<array-key, string> $matches Matches
91     */
92    public function setTooltip(array $matches): string
93    {
94        /* @mago-expect lint:no-isset - typed property may be uninitialized in constructor-less mocks */
95        if (!isset($this->glossaryHelper)) {
96            $this->glossaryHelper = new GlossaryHelper();
97        }
98
99        [$prefix, $item, $postfix] = $this->glossaryHelper->extractMatchParts(array_values($matches));
100        if ($item === '') {
101            return $matches[0];
102        }
103
104        return $this->glossaryHelper->formatTooltip($this->definition, $item, $prefix, $postfix);
105    }
106
107    /**
108     * Gets one item and definition from the database.
109     *
110     * @param int $id Glossary ID
111     */
112    public function fetch(int $id): array
113    {
114        return $this->glossaryRepository->fetch($id, $this->currentLanguage());
115    }
116
117    /**
118     * Gets all items and definitions from the database.
119     *
120     * @return array<int, array{id:int, language:string, item:string, definition:string}>
121     */
122    public function fetchAll(): array
123    {
124        $language = $this->currentLanguage();
125
126        if (($this->cachedItems[$language] ?? null) !== null) {
127            return $this->cachedItems[$language];
128        }
129
130        $items = $this->glossaryRepository->fetchAll($language);
131
132        $this->cachedItems[$language] = $items;
133
134        return $items;
135    }
136
137    /**
138     * Inserts an item and definition into the database.
139     *
140     * @param string $item       Item
141     * @param string $definition Definition
142     */
143    public function create(string $item, string $definition): bool
144    {
145        $ok = $this->glossaryRepository->create($this->currentLanguage(), $item, $definition);
146        if ($ok) {
147            unset($this->cachedItems[$this->currentLanguage()]);
148        }
149
150        return $ok;
151    }
152
153    /**
154     * Updates an item and definition into the database.
155     *
156     * @param int    $id         Glossary ID
157     * @param string $item       Item
158     * @param string $definition Definition
159     */
160    public function update(int $id, string $item, string $definition): bool
161    {
162        $ok = $this->glossaryRepository->update($id, $this->currentLanguage(), $item, $definition);
163        if ($ok) {
164            unset($this->cachedItems[$this->currentLanguage()]);
165        }
166
167        return $ok;
168    }
169
170    /**
171     * Deletes an item and definition into the database.
172     *
173     * @param int $id Glossary ID
174     */
175    public function delete(int $id): bool
176    {
177        $ok = $this->glossaryRepository->delete($id, $this->currentLanguage());
178        if ($ok) {
179            unset($this->cachedItems[$this->currentLanguage()]);
180        }
181
182        return $ok;
183    }
184
185    public function getLanguage(): string
186    {
187        return $this->language;
188    }
189
190    public function setLanguage(string $language): Glossary
191    {
192        $this->language = $language;
193        // Reset cache when language changes
194        $this->cachedItems = [];
195        return $this;
196    }
197
198    /**
199     * Returns explicitly set language or falls back to the configuration language.
200     */
201    private function currentLanguage(): string
202    {
203        if ($this->language !== '') {
204            return $this->language;
205        }
206
207        return $this->configuration->getLanguage()->getLanguage();
208    }
209}