Passed
Pull Request — main (#3)
by
unknown
15:02
created

ObjectSearchIndex::reindex()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 36
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 7
eloc 23
c 2
b 1
f 0
nc 12
nop 2
dl 0
loc 36
rs 8.6186
1
<?php
2
declare(strict_types=1);
3
4
namespace BEdita\ElasticSearch\Model\Index;
5
6
use BEdita\Core\Model\Entity\ObjectEntity;
7
use Cake\Core\Configure;
8
use Cake\Datasource\EntityInterface;
9
use Cake\ElasticSearch\Query;
10
use Cake\ElasticSearch\QueryBuilder;
11
use Cake\I18n\FrozenTime;
12
use Cake\Log\Log;
13
use Cake\ORM\Exception\PersistenceFailedException;
14
use Cake\Validation\Validator;
15
use Elastica\Exception\ResponseException;
16
use Elastica\Query\AbstractQuery;
17
use InvalidArgumentException;
18
19
/**
20
 * Base search index for BEdita objects in ElasticSearch.
21
 */
22
class ObjectSearchIndex extends SearchIndex
23
{
24
    /**
25
     * @inheritDoc
26
     */
27
    protected static array $_properties = [
28
        'uname' => ['type' => 'text'],
29
        'type' => ['type' => 'text'],
30
        'status' => ['type' => 'text'],
31
        'deleted' => ['type' => 'boolean'],
32
        'publish_start' => ['type' => 'date'],
33
        'publish_end' => ['type' => 'date'],
34
        'title' => ['type' => 'text'],
35
        'description' => ['type' => 'text'],
36
        'body' => ['type' => 'text'],
37
    ];
38
39
    /**
40
     * {@inheritDoc}
41
     *
42
     * @codeCoverageIgnore
43
     */
44
    public function validationDefault(Validator $validator): Validator
45
    {
46
        return $validator
47
            ->notEmptyString('uname')
48
            ->requirePresence('uname', 'create')
49
50
            ->notEmptyString('type')
51
            ->requirePresence('type', 'create')
52
53
            ->inList('status', ['on', 'draft', 'off'])
54
            ->requirePresence('status', 'create')
55
56
            ->boolean('deleted')
57
58
            ->dateTime('publish_start')
59
            ->allowEmptyDateTime('publish_start')
60
61
            ->dateTime('publish_end')
62
            ->allowEmptyDateTime('publish_end')
63
64
            ->scalar('title')
65
            ->allowEmptyString('title')
66
67
            ->scalar('description')
68
            ->allowEmptyString('description')
69
70
            ->scalar('body')
71
            ->allowEmptyString('body');
72
    }
73
74
    /**
75
     * @inheritDoc
76
     */
77
    public function reindex(EntityInterface $entity, string $operation): void
78
    {
79
        if (!$entity instanceof ObjectEntity) {
80
            Log::warning(sprintf(
81
                '%s index is supposed to be used only with sub-types of "%s", got "%s" instead',
82
                static::class,
83
                ObjectEntity::class,
84
                get_debug_type($entity),
85
            ));
86
            parent::reindex($entity, $operation);
87
88
            return;
89
        }
90
91
        switch ($operation) {
92
            case 'softDelete':
93
            case 'softDeleteRestore':
94
                try {
95
                    $id = (string)$entity->id;
96
                    if (!$this->set($id, 'deleted', $entity->deleted)) {
97
                        throw new PersistenceFailedException($this->get($id), ['set']);
98
                    }
99
                } catch (ResponseException $e) {
100
                    $fullError = (array)$e->getResponse()->getFullError();
101
                    if ($fullError['type'] !== 'document_missing_exception') {
102
                        throw $e;
103
                    }
104
105
                    // This scenario might be caused by an object that was not yet added to the index.
106
                    // Rather than updating a single field on an existing document, we should add the document instead.
107
                    parent::reindex($entity, 'edit');
108
                }
109
                break;
110
111
            default:
112
                parent::reindex($entity, $operation);
113
        }
114
    }
115
116
    /**
117
     * {@inheritDoc}
118
     *
119
     * @param \Cake\ElasticSearch\Query $query Query object instance.
120
     * @param array{query: string, type?: string|string[]} $options Search options.
121
     * @return \Cake\ElasticSearch\Query
122
     */
123
    public function findQuery(Query $query, array $options): Query
124
    {
125
        if (isset($options['type'])) {
126
            $query = $query->find('type', ['type' => $options['type']]);
127
        }
128
129
        return $query
130
            ->find('available')
131
            ->queryMust(
132
                fn (QueryBuilder $builder): AbstractQuery => $builder
133
                    ->simpleQueryString(['title', 'description', 'body'], $options['query']),
134
            );
135
    }
136
137
    /**
138
     * Find "available" documents, i.e. respective of deletion, status and publication constraints.
139
     *
140
     * @param \Cake\ElasticSearch\Query $query Query object instance.
141
     * @return \Cake\ElasticSearch\Query
142
     */
143
    protected function findAvailable(Query $query): Query
144
    {
145
        return $query->andWhere(function (QueryBuilder $builder): AbstractQuery {
146
            $conditions = [
147
                // Filter objects that are not deleted.
148
                $builder->term('deleted', 'false'),
149
            ];
150
151
            // Filter by object status.
152
            $statusLevel = Configure::read('Status.level', 'all');
153
            if ($statusLevel === 'on') {
154
                $conditions[] = $builder->term('status', 'on');
155
            } elseif ($statusLevel === 'draft') {
156
                $conditions[] = $builder->terms('status', ['on', 'draft']);
157
            }
158
159
            // Filter by publication date.
160
            if ((bool)Configure::read('Publish.checkDate', false)) {
161
                $now = FrozenTime::now();
162
163
                $conditions[] = $builder
164
                    ->or($builder->not($builder->exists('publish_start')), $builder->lte('publish_start', $now))
165
                    ->setMinimumShouldMatch(1);
166
                $conditions[] = $builder
167
                    ->or($builder->not($builder->exists('publish_end')), $builder->gte('publish_end', $now))
168
                    ->setMinimumShouldMatch(1);
169
            }
170
171
            return $builder->and(...$conditions);
172
        });
173
    }
174
175
    /**
176
     * Filter by object type.
177
     *
178
     * @param \Cake\ElasticSearch\Query $query Query object instance.
179
     * @param array{type: string|string[]} $options Finder options.
180
     * @return \Cake\ElasticSearch\Query
181
     */
182
    protected function findType(Query $query, array $options): Query
183
    {
184
        if (empty($options['type'])) {
185
            throw new InvalidArgumentException('Missing or empty `type` option');
186
        }
187
188
        return $query->andWhere(
189
            fn (QueryBuilder $builder): AbstractQuery => $builder->terms('type', (array)$options['type']),
190
        );
191
    }
192
}
193