Test Failed
Push — master ( e25fd0...2f7acb )
by Mathieu
06:17
created

SearchRunner::searchResultsToLogResults()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 1
1
<?php
2
3
namespace Charcoal\Search;
4
5
use \Exception;
6
use \InvalidArgumentException;
7
8
use \Psr\Log\LoggerAwareInterface;
9
use \Psr\Log\LoggerAwareTrait;
10
11
use \Charcoal\Factory\FactoryInterface;
12
13
use \Charcoal\Search\CustomSearch;
14
use \Charcoal\Search\SearchRunnerConfig;
15
use \Charcoal\Search\SearchInterface;
16
use \Charcoal\Search\SearchLog;
17
use \Charcoal\Search\SearchRunnerInterface;
18
19
/**
20
 * A basic search mediator
21
 */
22
class SearchRunner implements SearchRunnerInterface, LoggerAwareInterface
23
{
24
    use LoggerAwareTrait;
25
26
    /**
27
     * @var FactoryInterface $modelFactory
28
     */
29
    private $modelFactory;
30
31
    /**
32
     * @var SearchRunnerConfig $searchConfig
33
     */
34
    private $searchConfig;
35
36
    /**
37
     * @var SearchLog $searchLog
38
     */
39
    private $searchLog;
40
41
    /**
42
     * @var array $results
43
     */
44
    private $results;
45
46
    /**
47
     * @var boolean $logDisabled
48
     */
49
    public $logDisabled = false;
50
51
    /**
52
     * @param array $data The constructor options.
53
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
54
     */
55
    public function __construct(array $data)
56
    {
57
        $this->setSearchConfig($data['search_config']);
58
        $this->setModelFactory($data['model_factory']);
59
        $this->setLogger($data['logger']);
60
    }
61
62
    /**
63
     * @param FactoryInterface $factory The factory used to create logs and models.
64
     * @return void
65
     */
66
    private function setModelFactory(FactoryInterface $factory)
67
    {
68
        $this->modelFactory = $factory;
69
    }
70
71
    /**
72
     * @throws Exception If the model factory was not properly set.
73
     * @return FactoryInterface
74
     */
75
    protected function modelFactory()
76
    {
77
        if ($this->modelFactory === null) {
78
            throw new Exception(
79
                'Can not access model factory, the dependency has not been set.'
80
            );
81
        }
82
        return $this->modelFactory;
83
    }
84
85
    /**
86
     * @param array|SearchRunnerConfig $searchConfig The search options / configuration.
87
     * @return  SearchRunner Chainable
88
     */
89
    protected function setSearchConfig($searchConfig)
90
    {
91
        if (!($searchConfig instanceof SearchRunnerConfig)) {
92
            $searchConfig = new SearchRunnerConfig($searchConfig);
93
        }
94
        $this->searchConfig = $searchConfig;
95
        return $this;
96
    }
97
98
    /**
99
     * Public access to the search config.
100
     * @return SearchRunnerConfig
101
     */
102
    public function searchConfig()
103
    {
104
        return $this->searchConfig;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->searchConfig; (Charcoal\Search\SearchRunnerConfig) is incompatible with the return type declared by the interface Charcoal\Search\SearchRu...Interface::searchConfig of type Charcoal\Search\SearchConfig.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
105
    }
106
107
    /**
108
     * Public access to the search log.
109
     * @return SearchLog
110
     */
111
    public function searchLog()
112
    {
113
        return $this->searchLog;
114
    }
115
116
    /**
117
     * @return array
118
     */
119
    public function results()
120
    {
121
        return $this->results;
122
    }
123
124
    /**
125
     * @param  string $keyword       The searched query.
126
     * @param  array  $searchOptions Optional settings passed to each search objects.
127
     * @param  array  $logOptions    Optional data passed to the search log.
128
     * @throws InvalidArgumentException If the query is not a string.
129
     * @return array The results.
130
     */
131
    final public function search($keyword, array $searchOptions = [], array $logOptions = [])
132
    {
133
        if (!is_string($keyword)) {
134
            throw new InvalidArgumentException(
135
                'Search query must be a string.'
136
            );
137
        }
138
139
        if ($keyword == '') {
140
            throw new InvalidArgumentException(
141
                'Keyword can not be empty.'
142
            );
143
        }
144
145
        // Reset results
146
        $this->results = [];
147
148
        $searchConfig = $this->searchConfig();
149
150
        if (!isset($searchConfig['searches'])) {
151
            throw new InvalidArgumentException(
152
                'No searches defined in search config.'
153
            );
154
        }
155
156
        $numResults    = 0;
157
        $searchObjects = $searchConfig['searches'];
158
        $searchDeps    = [
159
            'logger'        => $this->logger,
160
            'model_factory' => $this->modelFactory()
161
        ];
162
163
        foreach ($searchObjects as $searchIdent => $searchObj) {
164
            if ($searchObj instanceof SearchInterface) {
165
                $results = $searchObj->search($keyword, $searchObjects);
166
            } else {
167
                $search  = new CustomSearch(array_merge($searchObj, $searchDeps));
168
                $results = $search->search($keyword, $searchObjects);
169
            }
170
171
            $this->results[$searchIdent] = $results;
172
            $numResults += count($results);
173
        }
174
175
176
        $logResults = [];
177
        foreach($this->results as $k=>$v) {
178
            $logResults[$k] = $this->searchResultsToLogResults($v);
179
        }
180
181
        $logData = [
182
            'search_ident'    => isset($searchConfig['ident']) ? $searchConfig['ident'] : '',
183
            'search_options'  => $searchOptions,
184
            'keyword'         => $keyword,
185
            'num_results'     => $numResults,
186
            'results'         => $logResults
187
        ];
188
189
        if ($logOptions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $logOptions 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...
190
            $logOptions = array_diff_key($logOptions, array_keys($logData));
191
            $logData    = array_merge($logData, $logOptions);
192
        }
193
194
        $this->searchLog = $this->createLog($logData);
195
196
        return $this->results;
197
    }
198
199
    /**
200
     * @param array $logData Log data.
201
     * @return SearchLog
202
     */
203
    private function createLog(array $logData)
204
    {
205
        $log = $this->modelFactory()->create(SearchLog::class);
206
        $log->setData($logData);
207
208
        if ($this->logDisabled === false) {
209
            $log->save();
210
        }
211
212
        return $log;
213
    }
214
215
    /**
216
     * @param array $results
217
     * @return array
218
     */
219
    private function searchResultsToLogResults(array $results)
220
    {
221
        $res = [];
222
        foreach($results as $result) {
223
            if (isset($result['id'])) {
224
                $res[] = $result['id'];
225
                continue;
226
            }
227
            $res[] = $result;
228
229
        }
230
        return $res;
231
    }
232
}
233