Passed
Push — master ( 8cd61e...d930f3 )
by Nicolaas
01:47
created

FasterIDLists::excludeList()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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