Completed
Branch FET/4986/11317/order-by-count (40fb33)
by
unknown
59:10 queued 47:34
created

CustomSelects::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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