FasterIDLists   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 242
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 100
c 4
b 0
f 0
dl 0
loc 242
rs 9.92
wmc 31

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A setIdList() 0 5 1
A filteredDatalist() 0 14 3
A setField() 0 5 1
A setTableName() 0 5 1
A setIsNumber() 0 5 1
A findRanges() 0 23 4
A getTableCount() 0 12 2
A getTableName() 0 6 2
C shortenIdList() 0 59 13
A excludeList() 0 13 2
1
<?php
2
3
namespace Sunnysideup\FasterIdLists;
4
use SilverStripe\ORM\DataList;
5
use SilverStripe\ORM\DB;
6
7
use SilverStripe\Core\Config\Config;
8
use SilverStripe\Core\Config\Configurable;
9
use SilverStripe\Core\Extensible;
10
use SilverStripe\Core\Injector\Injectable;
11
12
/**
13
 * turns a query statement of select from MyTable where ID IN (1,,2,3.......999999)
14
 * into something like:
15
 * - select from MyTable where ID between 0 and 99 or between 200 and 433
16
 * OR
17
 * - select from MyTable where ID NOT IN (4543)
18
 *
19
20
 */
21
22
class FasterIDLists
23
{
24
    use Configurable;
25
26
    protected static $table_count_cache = [];
27
28
    protected static $table_name_cache = [];
29
30
    /**
31
     *
32
     * @var int
33
     */
34
    private static $acceptable_max_number_of_select_statements = 1000;
35
36
    /**
37
     *
38
     * @var bool
39
     */
40
    private static $include_exclude_option = true;
41
42
    /**
43
     *
44
     * @var array
45
     */
46
    protected $idList = [];
47
48
    /**
49
     *
50
     * @var string
51
     */
52
    protected $className = '';
53
54
    /**
55
     *
56
     * @var string
57
     */
58
    protected $field = 'ID';
59
60
    /**
61
     *
62
     * @var bool
63
     */
64
    protected $isNumber = true;
65
66
    /**
67
     *
68
     * @param string  $className class name of Data Object being queried
69
     * @param array   $idList array of ids (or other field) to be selected from class name
70
     * @param string  $field usually the ID field, but could be another field
71
     * @param boolean $isNumber is the field a number type (so that we can do ranges OR something else)
72
     */
73
    public function __construct(string $className, array $idList, $field = 'ID', $isNumber = true)
74
    {
75
        $this->className = $className;
76
        $this->idList = $idList;
77
        $this->field = $field;
78
        $this->isNumber = $isNumber;
79
    }
80
81
    public function setIdList(array $idList) : FasterIDLists
82
    {
83
        $this->idList = $idList;
84
85
        return $this;
86
    }
87
88
    public function setField(string $field) : FasterIDLists
89
    {
90
        $this->field = $field;
91
92
        return $this;
93
    }
94
95
    public function setIsNumber(bool $isNumber) : FasterIDLists
96
    {
97
        $this->isNumber = $isNumber;
98
99
        return $this;
100
    }
101
102
    public function setTableName(string $tableName) : FasterIDLists
103
    {
104
        self::$table_name_cache[$this->className] = $tableName;
105
106
        return $this;
107
    }
108
109
    public function filteredDatalist(): DataList
110
    {
111
        $className = $this->className;
112
        if(count($this->idList) <= $this->Config()->acceptable_max_number_of_select_statements) {
113
            return $className::get()->filter([$this->field => $this->idList]);
114
        } else {
115
            $whereStatement = $this->shortenIdList();
116
            if($whereStatement) {
117
                return $className::get()->where($whereStatement);
118
            }
119
        }
120
121
        //default option ...
122
        return $className::get()->filter([$this->field => $this->idList]);
123
124
    }
125
126
    public function shortenIdList() : string
127
    {
128
        $finalArray = [];
129
        $operator = '';
130
        $glue = 'OR';
131
        $myIDList = $this->idList;
132
        $countMyIDList = count($myIDList);
133
        if($countMyIDList > 0) {
134
            if($this->isNumber) {
135
                $otherArray = [];
136
137
                //simplify select statement
138
                $ranges = $this->findRanges($myIDList);
139
                $rangesCount = count($ranges);
140
141
                //if it is long, then see if exclude is a better solution ...
142
                if($this->Config()->include_exclude_option) {
143
                    $excludeList = $this->excludeList();
144
                    if($excludeList) {
145
                        $excludeRanges = $this->findRanges($excludeList);
146
                        if(count($excludeRanges) < $rangesCount) {
147
                            $ranges = $excludeRanges;
148
                            $glue = 'AND';
149
                            $operator = 'NOT';
150
                        }
151
                    }
152
                }
153
                foreach($ranges as $range) {
154
                    $min = min($range);
155
                    $max = max($range);
156
                    if($min === $max) {
157
                        $otherArray[$min] = $min;
158
                    } else {
159
                        $finalArray[] = '"'.$this->getTableName().'"."'.$this->field.'" '.$operator.' BETWEEN '.$min.' AND '.$max;
160
                    }
161
                }
162
                if(count($otherArray)) {
163
                    $finalArray[] = '"'.$this->getTableName().'"."'.$this->field.'" '.$operator.'  IN('.implode(',', $otherArray).')';
164
                }
165
            } else {
166
                //if it is long, then see if exclude is a better solution ...
167
                if($this->Config()->include_exclude_option) {
168
                    $excludeList = $this->excludeList();
169
                    if($excludeList) {
170
                        if(count($excludeList) < $countMyIDList) {
171
                            $myIDList = $excludeList;
172
                            $glue = 'AND';
173
                            $operator = 'NOT';
174
                        }
175
                    }
176
                    $finalArray[] = '"'.$this->getTableName().'"."'.$this->field.'" '.$operator.'  IN(\''.implode('\',\'', $myIDList).'\')';
177
                }
178
            }
179
        }
180
        if(count($finalArray) === 0) {
181
            $finalArray[] = '"'.$this->getTableName().'"."'.$this->field.'" '.$operator.'  IN(-1)';
182
        }
183
184
        return '('.implode(') '.$glue.' (', $finalArray).')';
185
    }
186
187
    public function excludeList() : ?array
188
    {
189
        $className = $this->className;
190
        $countOfList = count($this->idList);
191
        //only run exclude if there is clear win
192
        $tableCount = $this->getTableCount();
193
        //there is more items in the list then
194
        if($countOfList > ($tableCount / 2)) {
195
            $fullList = $className::get()->column($this->field);
196
197
            return array_diff($fullList, $this->idList);
198
        }
199
        return null;
200
    }
201
202
    /**
203
     * get table name for query
204
     * @return string
205
     */
206
    protected function getTableName() : string
207
    {
208
        if(! isset(self::$table_name_cache[$this->className])) {
209
            self::$table_name_cache[$this->className] = Config::inst()->get($this->className, 'table_name');
210
        }
211
        return self::$table_name_cache[$this->className];
212
    }
213
214
    protected function getTableCount() : int
215
    {
216
        $tableName = $this->getTableName();
217
        if(! isset(self::$table_count_cache[$tableName])) {
218
            self::$table_count_cache[$tableName] = DB::query('
219
                SELECT COUNT(*)
220
                FROM "'.$this->getTableName().'"
221
                WHERE "'.$this->getTableName().'"."ID" IS NOT NULL'
222
            )->value();
223
        }
224
225
        return self::$table_count_cache[$tableName];
226
    }
227
228
    /**
229
     * return value looks like this:
230
     *      [
231
     *          0: 3,4,5,6,
232
     *          1: 8,9,10,
233
     *          2: 91
234
     *          3: 100,101
235
     *          etc...
236
     *      ]
237
     *
238
     *
239
     * @return array
240
     */
241
    protected function findRanges($idList) : array
242
    {
243
        $ranges = [];
244
        $lastOne = 0;
245
        $currentRangeKey = 0;
246
        sort($idList);
247
        foreach($idList as $key => $id) {
248
            //important
249
            $id = intval($id);
250
            if($id === ($lastOne + 1)) {
251
                // do nothing
252
            } else {
253
                $currentRangeKey++;
254
255
            }
256
            if(! isset($ranges[$currentRangeKey])) {
257
                $ranges[$currentRangeKey] = [];
258
            }
259
            $ranges[$currentRangeKey][$id] = $id;
260
            $lastOne = $id;
261
        }
262
263
        return $ranges;
264
    }
265
266
}
267