Passed
Pull Request — main (#59)
by Simon
01:14
created

ElasticConfigureTask::convertForJSON()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 2
eloc 9
nc 2
nop 1
dl 0
loc 14
rs 9.9666
c 1
b 1
f 0
1
<?php
2
3
namespace Firesphere\ElasticSearch\Tasks;
4
5
use Elastic\Elasticsearch\Exception\ClientResponseException;
6
use Elastic\Elasticsearch\Exception\MissingParameterException;
7
use Elastic\Elasticsearch\Exception\ServerResponseException;
8
use Elastic\Elasticsearch\Response\Elasticsearch;
9
use Exception;
10
use Firesphere\ElasticSearch\Helpers\Statics;
11
use Firesphere\ElasticSearch\Indexes\ElasticIndex;
12
use Firesphere\ElasticSearch\Services\ElasticCoreService;
13
use Firesphere\SearchBackend\Helpers\FieldResolver;
14
use Firesphere\SearchBackend\Traits\LoggerTrait;
15
use Psr\Container\NotFoundExceptionInterface;
16
use SilverStripe\Control\HTTPRequest;
17
use SilverStripe\Core\ClassInfo;
18
use SilverStripe\Core\Injector\Injector;
19
use SilverStripe\Dev\BuildTask;
20
use SilverStripe\ORM\DB;
21
22
class ElasticConfigureTask extends BuildTask
23
{
24
    use LoggerTrait;
25
26
    /**
27
     * @var string URLSegment
28
     */
29
    private static $segment = 'ElasticConfigureTask';
0 ignored issues
show
introduced by
The private property $segment is not used, and could be removed.
Loading history...
30
    /**
31
     * @var string Title
32
     */
33
    protected $title = 'Configure Elastic cores';
34
    /**
35
     * @var string Description
36
     */
37
    protected $description = 'Create or reload a Elastic Core by adding or reloading a configuration.';
38
    /**
39
     * @var ElasticCoreService $service
40
     */
41
    protected $service;
42
43
    /**
44
     * @throws NotFoundExceptionInterface
45
     */
46
    public function __construct()
47
    {
48
        parent::__construct();
49
        $this->service = Injector::inst()->get(ElasticCoreService::class);
50
    }
51
52
    /**
53
     * Run the config
54
     *
55
     * @param HTTPRequest $request
56
     * @return void|array
57
     * @throws NotFoundExceptionInterface
58
     */
59
    public function run($request)
60
    {
61
        $this->extend('onBeforeElasticConfigureTask', $request);
62
63
        $indexes = $this->service->getValidIndexes();
64
65
        $result = [];
66
67
        foreach ($indexes as $index) {
68
            try {
69
                /** @var ElasticIndex $instance */
70
                $instance = Injector::inst()->get($index, false);
71
72
                if ($request->getVar('clear') && $instance->indexExists()) {
73
                    $this->getLogger()->info(sprintf('Clearing index %s', $instance->getIndexName()));
74
                    $this->service->getClient()->indices()->delete(['index' => $instance->getIndexName()]);
75
                }
76
77
                $configResult = $this->configureIndex($instance);
78
                $result[] = $configResult->asBool();
79
            } catch (Exception $error) {
80
                // @codeCoverageIgnoreStart
81
                $this->getLogger()->error(sprintf('Core loading failed for %s', $index));
82
                $this->getLogger()->error($error->getMessage()); // in browser mode, it might not always show
83
                // Continue to the next index
84
                continue;
85
                // @codeCoverageIgnoreEnd
86
            }
87
            $this->extend('onAfterConfigureIndex', $index);
88
        }
89
90
        $this->extend('onAfterElasticConfigureTask');
91
92
        if ($request->getVar('istest')) {
93
            return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type array|boolean[] which is incompatible with the return type mandated by SilverStripe\Dev\BuildTask::run() of void.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
94
        }
95
    }
96
97
    /**
98
     * Update/create a store
99
     * @param ElasticIndex $instance
100
     * @return Elasticsearch
101
     * @throws ClientResponseException
102
     * @throws MissingParameterException
103
     * @throws ServerResponseException
104
     * @throws NotFoundExceptionInterface
105
     */
106
    protected function configureIndex($instance): Elasticsearch
107
    {
108
        $indexName = $instance->getIndexName();
109
110
111
        $instanceConfig = $this->createConfigForIndex($instance);
112
113
        $mappings = $this->convertForJSON($instanceConfig);
114
115
        $body['index'] = $indexName;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$body was never initialized. Although not strictly required by PHP, it is generally a good practice to add $body = array(); before regardless.
Loading history...
116
        $client = $this->service->getClient();
117
118
        $method = $this->getMethod($instance);
119
        $msg = "%s index %s";
120
        if ($method === 'update') {
121
            $body['body'] = $mappings;
122
            $msg = sprintf($msg, 'Updating', $indexName);
123
            DB::alteration_message($msg);
124
            $this->getLogger()->info($msg);
125
            return $client->indices()->putMapping($body);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $client->indices()->putMapping($body) could return the type Http\Promise\Promise which is incompatible with the type-hinted return Elastic\Elasticsearch\Response\Elasticsearch. Consider adding an additional type-check to rule them out.
Loading history...
126
        } else {
127
            $body['body']['mappings'] = $mappings;
128
            $msg = sprintf($msg, 'Creating', $indexName);
129
            DB::alteration_message($msg);
130
            $this->getLogger()->info($msg);
131
            return $client->indices()->create($body);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $client->indices()->create($body) could return the type Http\Promise\Promise which is incompatible with the type-hinted return Elastic\Elasticsearch\Response\Elasticsearch. Consider adding an additional type-check to rule them out.
Loading history...
132
        }
133
    }
134
135
    /**
136
     * @param ElasticIndex $instance
137
     * @return array
138
     * @throws NotFoundExceptionInterface
139
     */
140
    protected function createConfigForIndex(ElasticIndex $instance)
141
    {
142
        /** @var FieldResolver $resolver */
143
        $resolver = Injector::inst()->get(FieldResolver::class);
144
        $resolver->setIndex($instance);
145
        $result = [];
146
147
        foreach ($instance->getFulltextFields() as $field) {
148
            $field = $resolver->resolveField($field);
149
            $result = array_merge($result, $field);
150
        }
151
152
        return $result;
153
    }
154
155
    /**
156
     * Take the config from the resolver and build an array that's
157
     * ready to be converted to JSON for Elastic.
158
     *
159
     * @param $config
160
     * @return array[]
161
     */
162
    protected function convertForJSON($config)
163
    {
164
        $base = [];
165
        $typeMap = Statics::getTypeMap();
166
        foreach ($config as $key => &$conf) {
167
            $shortClass = ClassInfo::shortName($conf['origin']);
168
            $dotField = str_replace('_', '.', $conf['fullfield']);
169
            $conf['name'] = sprintf('%s.%s', $shortClass, $dotField);
170
            $base[$conf['name']] = [
171
                'type' => $typeMap[$conf['type'] ?? '*']
172
            ];
173
        }
174
175
        return ['properties' => $base];
176
    }
177
178
    /**
179
     * Get the method to use. Create or Update
180
     *
181
     * WARNING: Update often fails because Elastic does not allow changing
182
     * of mappings on-the-fly, it will commonly require a delete-and-recreate!
183
     *
184
     * @param ElasticIndex $index
185
     * @return string
186
     * @throws ClientResponseException
187
     * @throws MissingParameterException
188
     * @throws ServerResponseException
189
     */
190
    protected function getMethod(ElasticIndex $index): string
191
    {
192
        $check = $index->indexExists();
193
194
        if ($check) {
195
            return 'update';
196
        }
197
198
        return 'create';
199
    }
200
201
    /**
202
     * @return ElasticCoreService|mixed|object|Injector
203
     */
204
    public function getService(): mixed
205
    {
206
        return $this->service;
207
    }
208
209
    /**
210
     * @param ElasticCoreService|mixed|object|Injector $service
211
     */
212
    public function setService(mixed $service): void
213
    {
214
        $this->service = $service;
215
    }
216
}
217