Completed
Branch BUG-11108-ticket-reserved-coun... (144d27)
by
unknown
14:21 queued 17s
created

CustomSelects   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 325
Duplicated Lines 8.31 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 27
loc 325
rs 9.3999
wmc 33
lcom 1
cbo 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B deriveType() 0 46 5
C deriveParts() 0 27 7
B validateSelectValueForType() 14 36 5
A expectedSelectPartCountForType() 0 8 3
A assembleSelectStringWithOperator() 13 19 2
A getDataTypeForSelectType() 0 11 3
A originalSelects() 0 4 1
A columnsToSelectExpression() 0 4 1
A columnAliases() 0 4 1
A type() 0 4 1
A getDataTypeForAlias() 0 9 3

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
namespace EventEspresso\core\domain\values\model;
3
4
use InvalidArgumentException;
5
6
/**
7
 * CustomSelects
8
 * VO for model system that receives a formatted array for custom select part of a a query and can be used by the model
9
 * to build the various query parts.
10
 *
11
 * This includes accomplishing things like `COUNT(Registration.REG_ID) as registration_cound` or
12
 * `SUM(Transaction.TXN_total) as TXN_sum`
13
 *
14
 * @package EventEspresso\core\domain\values\model
15
 * @author  Darren Ethier
16
 * @since   4.9.57.p
17
 */
18
class CustomSelects
19
{
20
    const TYPE_SIMPLE = 'simple';
21
    const TYPE_COMPLEX = 'complex';
22
    const TYPE_STRUCTURED = 'structured';
23
24
    private $valid_operators = array('COUNT', 'SUM');
25
26
27
    /**
28
     * Original incoming select array
29
     * @var array
30
     */
31
    private $original_selects;
32
33
    /**
34
     * Select string that can be added to the query
35
     * @var string
36
     */
37
    private $columns_to_select_expression;
38
39
40
    /**
41
     * An array of aliases for the columns included in the incoming select array.
42
     * @var array
43
     */
44
    private $column_aliases_in_select;
45
46
47
    /**
48
     * Enum representation of the "type" of array coming into this value object.
49
     * @var string
50
     *
51
     */
52
    private $type = '';
53
54
55
    /**
56
     * CustomSelects constructor.
57
     * Incoming selects can be in one of the following formats:
58
     * ---- self::TYPE_SIMPLE array ----
59
     * This is considered the "simple" type. In this case the array is an numerically indexed array with single or
60
     * multiple columns to select as the values.
61
     * eg. array( 'ATT_ID', 'REG_ID' )
62
     * eg. array( '*' )
63
     * If you want to use the columns in any WHERE, GROUP BY, or HAVING clauses, you must instead use the "complex" or
64
     * "structured" method.
65
     * ---- self::TYPE_COMPLEX array ----
66
     * This is considered the "complex" type.  In this case the array is indexed by arbitrary strings that serve as
67
     * column alias, and the value is an numerically indexed array where there are two values.  The first value (0) is
68
     * the selection and the second value (1) is the data type.  Data types must be one of the types defined in
69
     * EEM_Base::$_valid_wpdb_data_types.
70
     * eg. array( 'count' => array('count(REG_ID)', '%d') )
71
     * Complex array configuration allows for using the column alias in any WHERE, GROUP BY, or HAVING clauses.
72
     * ---- self::TYPE_STRUCTURED array ---
73
     * This is considered the "structured" type. This type is similar to the complex type except that the array attached
74
     * to the column alias contains three values.  The first value is the qualified column name (which can include
75
     * join syntax for models).  The second value is the operator performed on the column (i.e. 'COUNT', 'SUM' etc).,
76
     * the third value is the data type.  Note, if the select does not have an operator, you can use an empty string for
77
     * the second value.
78
     * Note: for now SUM is only for simple single column expressions (i.e. SUM(Transaction.TXN_total))
79
     * eg. array( 'registration_count' => array('Registration.REG_ID', 'count', '%d') );
80
     *
81
     * NOTE: mixing array types in the incoming $select will cause errors.
82
     *
83
     * @param array $selects
84
     * @throws InvalidArgumentException
85
     */
86
    public function __construct(array $selects)
87
    {
88
        $this->original_selects = $selects;
89
        $this->deriveType($selects);
90
        $this->deriveParts($selects);
91
    }
92
93
94
    /**
95
     * Derives what type of custom select has been sent in.
96
     * @param array $selects
97
     * @throws InvalidArgumentException
98
     */
99
    private function deriveType(array $selects)
100
    {
101
        //first if the first key for this array is an integer then its coming in as a simple format, so we'll also
102
        // ensure all elements of the array are simple.
103
        if (is_int(key($selects))) {
104
            //let's ensure all keys are ints
105
            $invalid_keys = array_filter(
106
                array_keys($selects),
107
                function ($value) {
108
                    return ! is_int($value);
109
                }
110
            );
111
            if (! empty($invalid_keys)) {
112
                throw new InvalidArgumentException(
113
                    sprintf(
114
                        esc_html__(
115
                            'Incoming array looks like its formatted for "%1$s" type selects, however it has elements that are not indexed numerically',
116
                            'event_espresso'
117
                        ),
118
                        self::TYPE_SIMPLE
119
                    )
120
                );
121
            }
122
            $this->type = self::TYPE_SIMPLE;
123
            return;
124
        }
125
        //made it here so that means we've got either complex or structured selects.  Let's find out which by popping
126
        //the first array element off.
127
        $first_element = reset($selects);
128
129
        if (! is_array($first_element)) {
130
            throw new InvalidArgumentException(
131
                sprintf(
132
                    esc_html__(
133
                        'Incoming array looks like its formatted as a "%1$s" or "%2$%s" type.  However, the values in the array must be arrays themselves and they are not.',
134
                        'event_espresso'
135
                    ),
136
                    self::TYPE_COMPLEX,
137
                    self::TYPE_STRUCTURED
138
                )
139
            );
140
        }
141
        $this->type = count($first_element) === 2
142
            ? self::TYPE_COMPLEX
143
            : self::TYPE_STRUCTURED;
144
    }
145
146
147
    /**
148
     * Sets up the various properties for the vo depending on type.
149
     * @param array $selects
150
     * @throws InvalidArgumentException
151
     */
152
    private function deriveParts(array $selects)
153
    {
154
        $column_parts = array();
155
        switch ($this->type) {
156
            case self::TYPE_SIMPLE:
157
                $column_parts = $selects;
158
                $this->column_aliases_in_select = $selects;
159
                break;
160
            case self::TYPE_COMPLEX:
161
                foreach ($selects as $alias => $parts) {
162
                    $this->validateSelectValueForType($parts, $alias);
163
                    $column_parts[] = "{$parts[0]} AS {$alias}";
164
                    $this->column_aliases_in_select[] = $alias;
165
                }
166
                break;
167
            case self::TYPE_STRUCTURED:
168
                foreach ($selects as $alias => $parts) {
169
                    $this->validateSelectValueForType($parts, $alias);
170
                    $column_parts[] = $parts[1] !== ''
171
                        ? $this->assembleSelectStringWithOperator($parts, $alias)
172
                        : "{$parts[0]} AS {$alias}";
173
                    $this->column_aliases_in_select[] = $alias;
174
                }
175
                break;
176
        }
177
        $this->columns_to_select_expression = implode(', ', $column_parts);
178
    }
179
180
181
    /**
182
     * Validates self::TYPE_COMPLEX and self::TYPE_STRUCTURED select statement parts.
183
     * @param array $select_parts
184
     * @param string      $alias
185
     * @throws InvalidArgumentException
186
     */
187
    private function validateSelectValueForType(array $select_parts, $alias)
188
    {
189
        $valid_data_types = array('%d', '%s', '%f');
190
        if (count($select_parts) !== $this->expectedSelectPartCountForType()) {
191
            throw new InvalidArgumentException(
192
                sprintf(
193
                    esc_html__(
194
                        'The provided select part array for the %1$s column is expected to have a count of %2$d because the incoming select array is of type %3$s.  However the count was %4$d.',
195
                        'event_espresso'
196
                    ),
197
                    $alias,
198
                    $this->expectedSelectPartCountForType(),
199
                    $this->type,
200
                    count($select_parts)
201
                )
202
            );
203
        }
204
        //validate data type.
205
        $data_type = $this->type === self::TYPE_COMPLEX ? $select_parts[1] : '';
206
        $data_type = $this->type === self::TYPE_STRUCTURED ? $select_parts[2] : $data_type;
207
208 View Code Duplication
        if (! in_array($data_type, $valid_data_types, true)) {
209
            throw new InvalidArgumentException(
210
                sprintf(
211
                    esc_html__(
212
                        'Datatype %1$s (for selection "%2$s" and alias "%3$s") is not a valid wpdb datatype (eg %%s)',
213
                        'event_espresso'
214
                    ),
215
                    $data_type,
216
                    $select_parts[0],
217
                    $alias,
218
                    implode(', ', $valid_data_types)
219
                )
220
            );
221
        }
222
    }
223
224
225
    /**
226
     * Each type will have an expected count of array elements, this returns what that expected count is.
227
     * @param string $type
228
     * @return int
229
     */
230
    private function expectedSelectPartCountForType($type = '') {
231
        $type = $type === '' ? $this->type : $type;
232
        $types_count_map = array(
233
            self::TYPE_COMPLEX => 2,
234
            self::TYPE_STRUCTURED => 3
235
        );
236
        return isset($types_count_map[$type]) ? $types_count_map[$type] : 0;
237
    }
238
239
240
    /**
241
     * Prepares the select statement part for for structured type selects.
242
     * @param array  $select_parts
243
     * @param string $alias
244
     * @return string
245
     * @throws InvalidArgumentException
246
     */
247
    private function assembleSelectStringWithOperator(array $select_parts, $alias)
248
    {
249
        $operator = strtoupper($select_parts[1]);
250
        //validate operator
251 View Code Duplication
        if (! in_array($operator, $this->valid_operators, true)) {
252
            throw new InvalidArgumentException(
253
                sprintf(
254
                    esc_html__(
255
                        'An invalid operator has been provided (%1$s) for the column %2$s.  Valid operators must be one of the following: %3$s.',
256
                        'event_espresso'
257
                    ),
258
                    $operator,
259
                    $alias,
260
                    implode(', ', $this->valid_operators)
261
                )
262
            );
263
        }
264
        return $operator . '(' . $select_parts[0] . ') AS ' . $alias;
265
    }
266
267
268
    /**
269
     * Return the datatype from the given select part.
270
     * Remember the select_part has already been validated on object instantiation.
271
     * @param array $select_part
272
     * @return string
273
     */
274
    private function getDataTypeForSelectType(array $select_part)
275
    {
276
        switch ($this->type) {
277
            case self::TYPE_COMPLEX:
278
                return $select_part[1];
279
            case self::TYPE_STRUCTURED:
280
                return $select_part[2];
281
            default:
282
                return '';
283
        }
284
    }
285
286
287
    /**
288
     * Returns the original select array sent into the VO.
289
     * @return array
290
     */
291
    public function originalSelects()
292
    {
293
        return $this->original_selects;
294
    }
295
296
297
    /**
298
     * Returns the final assembled select expression derived from the incoming select array.
299
     * @return string
300
     */
301
    public function columnsToSelectExpression()
302
    {
303
        return $this->columns_to_select_expression;
304
    }
305
306
307
    /**
308
     * Returns all the column aliases derived from the incoming select array.
309
     * @return array
310
     */
311
    public function columnAliases()
312
    {
313
        return $this->column_aliases_in_select;
314
    }
315
316
317
    /**
318
     * Returns the enum type for the incoming select array.
319
     * @return string
320
     */
321
    public function type()
322
    {
323
        return $this->type;
324
    }
325
326
327
328
    /**
329
     * Return the datatype for the given column_alias
330
     * @param string $column_alias
331
     * @return string  (if there's no data type we return string as the default).
332
     */
333
    public function getDataTypeForAlias($column_alias)
334
    {
335
        if (isset($this->original_selects[$column_alias])
336
            && in_array($column_alias, $this->columnAliases(), true)
337
        ) {
338
            return $this->getDataTypeForSelectType($this->original_selects[$column_alias]);
339
        }
340
        return '%s';
341
    }
342
}
343