Completed
Push — master ( 225222...7d085e )
by Schlaefer
05:18 queued 02:52
created

UserReadsTable::garbageCollection()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 0
dl 0
loc 16
rs 9.7333
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Saito - The Threaded Web Forum
7
 *
8
 * @copyright Copyright (c) the Saito Project Developers
9
 * @link https://github.com/Schlaefer/Saito
10
 * @license http://opensource.org/licenses/MIT
11
 */
12
13
namespace App\Model\Table;
14
15
use App\Model\Table\UsersTable;
16
use Cake\ORM\Table;
17
use Stopwatch\Lib\Stopwatch;
18
19
/**
20
 * @property UsersTable $Users
21
 */
22
class UserReadsTable extends Table
23
{
24
    /**
25
     * Period after garbage collection deletes old data
26
     */
27
    public const GC = '-31 days';
28
29
    /**
30
     * Caches user entries over multiple validations
31
     *
32
     * Esp. when many rows are set via Mix-view request
33
     *
34
     * @var array
35
     */
36
    protected $userCache = [];
37
38
    /**
39
     * {@inheritDoc}
40
     */
41
    public function initialize(array $config)
42
    {
43
        $this->addBehavior('Cron.Cron', [
44
            'garbageCollection' => [
45
                'id' => 'UserReadsTable.gc',
46
                'due' => '+12 hours',
47
            ]
48
        ]);
49
        $this->addBehavior('Timestamp');
50
51
        $this->belongsTo('Entries', ['foreignKey' => 'entry_id']);
52
        $this->belongsTo('Users', ['foreignKey' => 'user_id']);
53
    }
54
55
    /**
56
     * sets $entriesIds as read for user $userId
57
     *
58
     * @param array $entriesId [3, 4, 34]
59
     * @param int $userId user-ID
60
     * @return void
61
     */
62
    public function setEntriesForUser(array $entriesId, int $userId): void
63
    {
64
        // filter out duplicates
65
        $userEntries = $this->getUser($userId);
66
        $entriesToSave = array_diff($entriesId, $userEntries);
67
68
        if (empty($entriesToSave)) {
69
            return;
70
        }
71
72
        $data = [];
73
        foreach ($entriesToSave as $entryId) {
74
            $this->userCache[$userId][$entryId] = $entryId;
75
            $data[] = [
76
                'entry_id' => $entryId,
77
                'user_id' => $userId
78
            ];
79
        }
80
81
        $entities = $this->newEntities($data);
82
        // @performance is one transaction but multiple inserts
83
        $this->getConnection()->transactional(
84
            function () use ($entities) {
85
                foreach ($entities as $entity) {
86
                    $this->save($entity, ['atomic' => false]);
87
                }
88
            }
89
        );
90
    }
91
92
    /**
93
     * gets all read postings of user with id $userId
94
     *
95
     * @param int $userId user-ID
96
     * @return array [1 => 1, 3 => 3]
97
     */
98
    public function getUser(int $userId): array
99
    {
100
        if (isset($this->userCache[$userId])) {
101
            return $this->userCache[$userId];
102
        }
103
104
        Stopwatch::start('UserRead::getUser()');
105
        $readPostings = $this->find()
106
            ->where(['user_id' => $userId])
107
            ->order('entry_id');
108
        $read = [];
109
        foreach ($readPostings as $posting) {
110
            $id = $posting->get('entry_id');
111
            $read[$id] = $id;
112
        }
113
        $this->userCache[$userId] = $read;
114
        Stopwatch::stop('UserRead::getUser()');
115
116
        return $this->userCache[$userId];
117
    }
118
119
    /**
120
     * deletes entries with lower entry-ID than $entryId from user $userId
121
     *
122
     * @param int $userId user-ID
123
     * @param int $entryId entry-ID
124
     * @return void
125
     * @throws \InvalidArgumentException
126
     */
127
    public function deleteUserEntriesBefore(int $userId, int $entryId): void
128
    {
129
        if (empty($userId) || empty($entryId)) {
130
            throw new \InvalidArgumentException;
131
        }
132
        unset($this->userCache[$userId]);
133
        $this->deleteAll(['entry_id <' => $entryId, 'user_id' => $userId]);
134
    }
135
136
    /**
137
     * deletes entries from user $userId
138
     *
139
     * @param int $userId user-ID
140
     * @return void
141
     * @throws \InvalidArgumentException
142
     */
143
    public function deleteAllFromUser(int $userId): void
144
    {
145
        if (empty($userId)) {
146
            throw new \InvalidArgumentException;
147
        }
148
        unset($this->userCache[$userId]);
149
        $this->deleteAll(['user_id' => $userId]);
150
    }
151
152
    /**
153
     * Removes old read-posting data for all users.
154
     *
155
     * Prevent data of non-returning users to stay forever in the DB.
156
     *
157
     * @return void
158
     */
159
    public function garbageCollection(): void
160
    {
161
        $oldest = $this->find()->order(['id' => 'ASC'])->first();
162
        if (empty($oldest)) {
163
            // Usually a newly setup forum.
164
            return;
165
        }
166
167
        $cutoff = new \DateTimeImmutable(self::GC);
168
        if ($oldest->get('created') > $cutoff) {
169
            return;
170
        }
171
172
        $this->deleteAll(['created <' => $cutoff->sub(new \DateInterval('P1D'))]);
173
        $this->userCache = [];
174
    }
175
}
176