Completed
Push — master ( c28fba...9b84d0 )
by Tim
15s queued 11s
created

IndexerService::generateSlugAndInsert()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
/**
4
 * Index the given events.
5
 */
6
declare(strict_types=1);
7
8
namespace HDNET\Calendarize\Service;
9
10
use HDNET\Calendarize\Register;
11
use HDNET\Calendarize\Service\Url\SlugService;
12
use HDNET\Calendarize\Utility\ArrayUtility;
13
use HDNET\Calendarize\Utility\DateTimeUtility;
14
use HDNET\Calendarize\Utility\HelperUtility;
15
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
16
use TYPO3\CMS\Core\Utility\GeneralUtility;
17
use TYPO3\CMS\Core\Utility\MathUtility;
18
use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
19
20
/**
21
 * Index the given events.
22
 */
23
class IndexerService extends AbstractService
24
{
25
    /**
26
     * Index table name.
27
     */
28
    const TABLE_NAME = 'tx_calendarize_domain_model_index';
29
30
    /**
31
     * @var Dispatcher
32
     */
33
    protected $signalSlot;
34
35
    /**
36
     * @var IndexPreparationService
37
     */
38
    protected $preparationService;
39
40
    /**
41
     * @var SlugService
42
     */
43
    protected $slugService;
44
45
    public function __construct(
46
        Dispatcher $dispatcher,
47
        IndexPreparationService $preparationService,
48
        SlugService $slugService
49
    ) {
50
        $this->signalSlot = $dispatcher;
51
        $this->preparationService = $preparationService;
52
        $this->slugService = $slugService;
53
    }
54
55
    /**
56
     * Reindex all elements.
57
     */
58
    public function reindexAll()
59
    {
60
        $this->signalSlot->dispatch(__CLASS__, __FUNCTION__ . 'Pre', [$this]);
61
62
        $this->removeInvalidConfigurationIndex();
63
64
        foreach (Register::getRegister() as $key => $configuration) {
65
            $tableName = $configuration['tableName'];
66
            $this->removeInvalidRecordIndex($tableName);
67
68
            $q = HelperUtility::getDatabaseConnection($tableName)->createQueryBuilder();
69
            $q->getRestrictions()
70
                ->removeAll()
71
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
72
73
            $q->select('uid')
74
                ->from($tableName);
75
76
            $transPointer = $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] ?? false; // e.g. l10n_parent
77
            if ($transPointer) {
78
                // Note: In localized tables, it is important, that the "default language records" are indexed first, so the
79
                // overlays can connect with l10n_parent to the right default record.
80
                $q->orderBy((string)$transPointer);
81
            }
82
            $rows = $q->execute()->fetchAll();
83
            foreach ($rows as $row) {
84
                $this->updateIndex($key, $tableName, (int)$row['uid']);
85
            }
86
        }
87
88
        $this->signalSlot->dispatch(__CLASS__, __FUNCTION__ . 'Post', [$this]);
89
    }
90
91
    /**
92
     * Reindex the given element.
93
     *
94
     * @param string $configurationKey
95
     * @param string $tableName
96
     * @param int    $uid
97
     */
98
    public function reindex(string $configurationKey, string $tableName, int $uid)
99
    {
100
        $this->signalSlot->dispatch(__CLASS__, __FUNCTION__ . 'Pre', [$configurationKey, $tableName, $uid, $this]);
101
102
        $this->removeInvalidConfigurationIndex();
103
        $this->removeInvalidRecordIndex($tableName);
104
        $this->updateIndex($configurationKey, $tableName, $uid);
105
106
        $this->signalSlot->dispatch(__CLASS__, __FUNCTION__ . 'Post', [$configurationKey, $tableName, $uid, $this]);
107
    }
108
109
    /**
110
     * Get index count.
111
     *
112
     * @param string $tableName
113
     * @param int    $uid
114
     *
115
     * @return int
116
     */
117
    public function getIndexCount(string $tableName, $uid): int
118
    {
119
        // Note: "uid" could be e.g. NEW6273482 in DataHandler process
120
        if (MathUtility::canBeInterpretedAsInteger($uid)) {
121
            return (int)$this->getCurrentItems($tableName, (int)$uid)->rowCount();
122
        }
123
124
        return 0;
125
    }
126
127
    /**
128
     * Get the next events.
129
     *
130
     * @param string $table
131
     * @param int    $uid
132
     * @param int    $limit
133
     *
134
     * @return array|null
135
     */
136
    public function getNextEvents($table, $uid, $limit = 5)
137
    {
138
        $q = HelperUtility::getDatabaseConnection($table)->createQueryBuilder();
139
140
        $q->getRestrictions()
141
            ->removeAll()
142
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
143
144
        $q->select('*')
145
            ->from(self::TABLE_NAME)
146
            ->where(
147
                $q->expr()->andX(
148
                    $q->expr()->gte('start_date', $q->createNamedParameter(DateTimeUtility::getNow()->format('Y-m-d'))),
149
                    $q->expr()->eq('foreign_table', $q->createNamedParameter($table)),
150
                    $q->expr()->eq('foreign_uid', $q->createNamedParameter((int)$uid, \PDO::PARAM_INT))
151
                )
152
            )
153
            ->addOrderBy('start_date', 'ASC')
154
            ->addOrderBy('start_time', 'ASC')
155
            ->setMaxResults($limit);
156
157
        return $q->execute()->fetchAll();
158
    }
159
160
    /**
161
     * Build the index for one element.
162
     *
163
     * @param string $configurationKey
164
     * @param string $tableName
165
     * @param int    $uid
166
     */
167
    protected function updateIndex(string $configurationKey, string $tableName, int $uid)
168
    {
169
        $neededItems = $this->preparationService->prepareIndex($configurationKey, $tableName, $uid);
170
        $this->insertAndUpdateNeededItems($neededItems, $tableName, $uid);
171
    }
172
173
    /**
174
     * Get the current items (ignore enable fields).
175
     *
176
     * @param string $tableName
177
     * @param int    $uid
178
     *
179
     * @return \Doctrine\DBAL\Driver\Statement|int
180
     */
181
    protected function getCurrentItems(string $tableName, int $uid)
182
    {
183
        $q = HelperUtility::getDatabaseConnection(self::TABLE_NAME)->createQueryBuilder();
184
        $q->getRestrictions()
185
            ->removeAll()
186
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
187
        $q->select('*')
188
            ->from(self::TABLE_NAME)
189
            ->where(
190
                $q->expr()->eq('foreign_table', $q->createNamedParameter($tableName)),
191
                $q->expr()->eq('foreign_uid', $q->createNamedParameter($uid, \PDO::PARAM_INT))
192
            );
193
194
        return $q->execute();
195
    }
196
197
    /**
198
     * Insert and/or update the needed index records.
199
     *
200
     * @param array  $neededItems
201
     * @param string $tableName
202
     * @param int    $uid
203
     */
204
    protected function insertAndUpdateNeededItems(array $neededItems, string $tableName, int $uid)
205
    {
206
        $databaseConnection = HelperUtility::getDatabaseConnection(self::TABLE_NAME);
207
        $currentItems = $this->getCurrentItems($tableName, $uid)->fetchAll();
208
209
        $this->signalSlot->dispatch(__CLASS__, __FUNCTION__ . 'Pre', [$neededItems, $tableName, $uid]);
210
211
        foreach ($neededItems as $neededKey => $neededItem) {
212
            foreach ($currentItems as $currentKey => $currentItem) {
213
                if (ArrayUtility::isEqualArray($neededItem, $currentItem, ['tstamp', 'crdate', 'slug'])) {
214
                    // Check if the current slug starts with the new slug
215
                    // Prevents regeneration for slugs with counting suffixes (added before insertion)
216
                    // False positives are possible (e.g. single event where a part gets removed)
217
                    if (0 !== mb_stripos($currentItem['slug'] ?? '', $neededItem['slug'], 0, 'utf-8')) {
218
                        // Slug changed
219
                        continue;
220
                    }
221
222
                    unset($neededItems[$neededKey], $currentItems[$currentKey]);
223
224
                    break;
225
                }
226
            }
227
        }
228
        foreach ($currentItems as $item) {
229
            $databaseConnection->delete(self::TABLE_NAME, ['uid' => $item['uid']]);
230
        }
231
232
        $this->generateSlugAndInsert($neededItems);
233
    }
234
235
    /**
236
     * Generates a slug and inserts the records in the db.
237
     *
238
     * @param array $neededItems
239
     */
240
    protected function generateSlugAndInsert(array $neededItems): void
241
    {
242
        $db = HelperUtility::getDatabaseConnection(self::TABLE_NAME);
243
        foreach ($neededItems as $key => $item) {
244
            $item['slug'] = $this->slugService->makeSlugUnique($item);
245
            // We need to insert after each index, so subsequent indices do not get the same slug
246
            $db->insert(self::TABLE_NAME, $item);
247
        }
248
    }
249
250
    /**
251
     * Remove Index items of the given table of records
252
     * that are deleted or do not exists anymore.
253
     *
254
     * @param string $tableName
255
     */
256
    protected function removeInvalidRecordIndex($tableName)
257
    {
258
        $q = HelperUtility::getDatabaseConnection($tableName)->createQueryBuilder();
259
        $q->getRestrictions()
260
            ->removeAll()
261
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
262
263
        $q->select('uid')
264
            ->from($tableName);
265
266
        $rows = $q->execute()->fetchAll();
267
268
        $q = HelperUtility::getDatabaseConnection(self::TABLE_NAME)->createQueryBuilder();
269
        $q->delete(self::TABLE_NAME)
270
            ->where(
271
                $q->expr()->eq('foreign_table', $q->createNamedParameter($tableName))
272
            );
273
274
        $ids = [];
275
        foreach ($rows as $row) {
276
            $ids[] = $row['uid'];
277
        }
278
        if ($ids) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ids of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
279
            $q->andWhere(
280
                $q->expr()->notIn('foreign_uid', $ids)
281
            );
282
        }
283
284
        $q->execute();
285
    }
286
287
    /**
288
     * Remove index Items of configurations that are not valid anymore.
289
     *
290
     * @return bool
291
     */
292
    protected function removeInvalidConfigurationIndex()
293
    {
294
        $db = HelperUtility::getDatabaseConnection(self::TABLE_NAME);
295
        $q = $db->createQueryBuilder();
296
297
        $validKeys = array_keys(Register::getRegister());
298
        if ($validKeys) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $validKeys of type array<integer|string> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
299
            foreach ($validKeys as $key => $value) {
300
                $validKeys[$key] = $q->createNamedParameter($value);
301
            }
302
303
            $q->delete(self::TABLE_NAME)
304
                ->where(
305
                    $q->expr()->notIn('unique_register_key', $validKeys)
306
                )->execute();
307
308
            return (bool)$q->execute();
309
        }
310
311
        return (bool)$db->truncate(self::TABLE_NAME);
312
    }
313
}
314