GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — integration ( 45cc9f...98bc42 )
by Brendan
05:52
created

EntryManager   D

Complexity

Total Complexity 117

Size/Duplication

Total Lines 724
Duplicated Lines 4.01 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 5
Bugs 1 Features 0
Metric Value
c 5
b 1
f 0
dl 29
loc 724
rs 4.4444
wmc 117
lcom 1
cbo 9

13 Methods

Rating   Name   Duplication   Size   Complexity  
A setFetchSortingDirection() 0 10 3
A setFetchSortingField() 0 4 1
A setFetchSorting() 0 5 1
A getFetchSorting() 0 7 1
D add() 9 44 9
C edit() 9 66 12
D delete() 0 90 19
F fetch() 11 101 37
D __buildEntries() 0 106 21
A fetchEntrySectionID() 0 6 1
B fetchCount() 0 25 4
C fetchByPage() 0 43 7
A create() 0 4 1

How to fix   Duplicated Code    Complexity   

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:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like EntryManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EntryManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @package toolkit
5
 */
6
/**
7
 * The `EntryManager` is responsible for all `Entry` objects in Symphony.
8
 * Entries are stored in the database in a cluster of tables. There is a
9
 * parent entry row stored in `tbl_entries` and then each field's data is
10
 * stored in a separate table, `tbl_entries_data_{field_id}`. Where Field ID
11
 * is generated when the Section is saved. This Manager provides basic
12
 * add, edit, delete and fetching methods for Entries.
13
 */
14
15
class EntryManager
16
{
17
    /**
18
     * The Field ID that will be used to sort when fetching Entries, defaults
19
     * to null, which implies the Entry ID (id column in `tbl_entries`)
20
     * @var integer
21
     */
22
    protected static $_fetchSortField = null;
23
24
    /**
25
     * The direction that entries should be sorted in, available options are
26
     * RAND, ASC or DESC. Defaults to null, which implies ASC
27
     * @var string
28
     */
29
    protected static $_fetchSortDirection = null;
30
31
    /**
32
     * Setter function for the default sorting direction of the Fetch
33
     * function. Available options are RAND, ASC or DESC.
34
     *
35
     * @param string $direction
36
     *  The direction that entries should be sorted in, available options
37
     *  are RAND, ASC or DESC.
38
     */
39
    public static function setFetchSortingDirection($direction)
40
    {
41
        $direction = strtoupper($direction);
42
43
        if ($direction === 'RANDOM') {
44
            $direction = 'RAND';
45
        }
46
47
        self::$_fetchSortDirection = (in_array($direction, array('RAND', 'ASC', 'DESC')) ? $direction : null);
48
    }
49
50
    /**
51
     * Sets the field to applying the sorting direction on when fetching
52
     * entries
53
     *
54
     * @param integer $field_id
55
     *  The ID of the Field that should be sorted on
56
     */
57
    public static function setFetchSortingField($field_id)
58
    {
59
        self::$_fetchSortField = $field_id;
60
    }
61
62
    /**
63
     * Convenience function that will set sorting field and direction
64
     * by calling `setFetchSortingField()` & `setFetchSortingDirection()`
65
     *
66
     * @see toolkit.EntryManager#setFetchSortingField()
67
     * @see toolkit.EntryManager#setFetchSortingDirection()
68
     * @param integer $field_id
69
     *  The ID of the Field that should be sorted on
70
     * @param string $direction
71
     *  The direction that entries should be sorted in, available options
72
     *  are RAND, ASC or DESC. Defaults to ASC
73
     */
74
    public static function setFetchSorting($field_id, $direction = 'ASC')
75
    {
76
        self::setFetchSortingField($field_id);
77
        self::setFetchSortingDirection($direction);
78
    }
79
80
    /**
81
     * Returns an object representation of the sorting for the
82
     * EntryManager, with the field and direction provided
83
     *
84
     * @return StdClass
85
     */
86
    public static function getFetchSorting()
87
    {
88
        return (object)array(
89
            'field' => self::$_fetchSortField,
90
            'direction' => self::$_fetchSortDirection
91
        );
92
    }
93
94
    /**
95
     * Given an Entry object, iterate over all of the fields in that object
96
     * an insert them into their relevant entry tables.
97
     *
98
     * @param Entry $entry
99
     *  An Entry object to insert into the database
100
     * @throws DatabaseException
101
     * @return boolean
102
     */
103
    public static function add(Entry $entry)
104
    {
105
        $fields = $entry->get();
106
        Symphony::Database()->insert($fields, 'tbl_entries');
107
108
        if (!$entry_id = Symphony::Database()->getInsertID()) {
109
            return false;
110
        }
111
112
        foreach ($entry->getData() as $field_id => $field) {
113
            if (!is_array($field) || empty($field)) {
114
                continue;
115
            }
116
117
            Symphony::Database()->delete('tbl_entries_data_' . $field_id, " `entry_id` = ?", array($entry_id));
118
119
            $data = array(
120
                'entry_id' => $entry_id
121
            );
122
123
            $fields = array();
124
125 View Code Duplication
            foreach ($field as $key => $value) {
126
                if (is_array($value)) {
127
                    foreach ($value as $ii => $v) {
128
                        $fields[$ii][$key] = $v;
129
                    }
130
                } else {
131
                    $fields[max(0, count($fields) - 1)][$key] = $value;
132
                }
133
            }
134
135
            $fieldCount = count($fields);
136
            for ($ii = 0; $ii < $fieldCount; $ii++) {
137
                $fields[$ii] = array_merge($data, $fields[$ii]);
138
            }
139
140
            Symphony::Database()->insert($fields, 'tbl_entries_data_' . $field_id);
141
        }
142
143
        $entry->set('id', $entry_id);
144
145
        return true;
146
    }
147
148
    /**
149
     * Update an existing Entry object given an Entry object
150
     *
151
     * @param Entry $entry
152
     *  An Entry object
153
     * @throws DatabaseException
154
     * @return boolean
155
     */
156
    public static function edit(Entry $entry)
157
    {
158
        // Update modification date.
159
        Symphony::Database()->update(
160
            array(
161
                'modification_date' => $entry->get('modification_date'),
162
                'modification_date_gmt' => $entry->get('modification_date_gmt')
163
            ),
164
            'tbl_entries',
165
            ' `id` = ?',
166
            array(
167
                $entry->get('id')
168
            )
169
        );
170
171
        // Iterate over all data for this entry, deleting existing data first
172
        // then inserting a new row for the data
173
        foreach ($entry->getData() as $field_id => $field) {
174
            if (empty($field_id)) {
175
                continue;
176
            }
177
178
            $did_lock = false;
179
            try {
180
                $did_lock = Symphony::Database()->query('LOCK TABLES tbl_entries_data_' . $field_id . ' WRITE');
181
                Symphony::Database()->delete('tbl_entries_data_' . $field_id, '`entry_id` = ?', array($entry->get('id')));
182
183
                if (!is_array($field) || empty($field)) {
184
                    if ($did_lock) {
185
                        Symphony::Database()->query('UNLOCK TABLES');
186
                    }
187
                    continue;
188
                }
189
190
                $data = array(
191
                    'entry_id' => $entry->get('id')
192
                );
193
194
                $fields = array();
195
196 View Code Duplication
                foreach ($field as $key => $value) {
197
                    if (is_array($value)) {
198
                        foreach ($value as $ii => $v) {
199
                            $fields[$ii][$key] = $v;
200
                        }
201
                    } else {
202
                        $fields[max(0, count($fields) - 1)][$key] = $value;
203
                    }
204
                }
205
206
                foreach ($fields as $index => $field_data) {
207
                    $fields[$index] = array_merge($data, $field_data);
208
                }
209
210
                Symphony::Database()->insert($fields, 'tbl_entries_data_' . $field_id);
211
            } catch (Exception $e) {
212
                Symphony::Log()->pushExceptionToLog($e, true);
0 ignored issues
show
Unused Code introduced by
The call to Log::pushExceptionToLog() has too many arguments starting with true.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
213
            }
214
215
            if ($did_lock) {
216
                Symphony::Database()->query('UNLOCK TABLES');
217
            }
218
        }
219
220
        return true;
221
    }
222
223
    /**
224
     * Given an Entry ID, or an array of Entry ID's, delete all
225
     * data associated with this Entry using a Field's `entryDataCleanup()`
226
     * function, and then remove this Entry from `tbl_entries`. If the `$entries`
227
     * all belong to the same section, passing `$section_id` will improve
228
     * performance
229
     *
230
     * @param array|integer $entries
231
     *  An entry_id, or an array of entry id's to delete
232
     * @param integer $section_id (optional)
233
     *  If possible, the `$section_id` of the the `$entries`. This parameter
234
     *  should be left as null if the `$entries` array contains entry_id's for
235
     *  multiple sections.
236
     * @throws DatabaseException
237
     * @throws Exception
238
     * @return boolean
239
     */
240
    public static function delete($entries, $section_id = null)
241
    {
242
        $needs_data = true;
243
244
        if (!is_array($entries)) {
245
            $entries = array($entries);
246
        }
247
248
        // Get the section's schema
249
        if (!is_null($section_id)) {
250
            $section = SectionManager::fetch($section_id);
251
252
            if ($section instanceof Section) {
253
                $fields = $section->fetchFields();
254
                $data = array();
255
256
                foreach ($fields as $field) {
257
                    $reflection = new ReflectionClass($field);
258
                    // This field overrides the default implementation, so pass it data.
259
                    $data[$field->get('element_name')] = $reflection->getMethod('entryDataCleanup')->class === 'Field' ? false : true;
260
                }
261
262
                $data = array_filter($data);
263
264
                if (empty($data)) {
265
                    $needs_data = false;
266
                }
267
            }
268
        }
269
270
        // We'll split $entries into blocks of 2500 (random number)
271
        // and process the deletion in chunks.
272
        $chunks = array_chunk($entries, 2500);
273
274
        foreach ($chunks as $chunk) {
275
            // If we weren't given a `section_id` we'll have to process individually
276
            // If we don't need data for any field, we can process the whole chunk
277
            // without building Entry objects, otherwise we'll need to build
278
            // Entry objects with data
279
            if (is_null($section_id) || !$needs_data) {
280
                $entries = $chunk;
281
            } elseif ($needs_data) {
282
                $entries = self::fetch($chunk, $section_id);
283
            }
284
285
            if ($needs_data) {
286
                foreach ($entries as $id) {
287
                    // Handles the case where `section_id` was not provided
288
                    if (is_null($section_id)) {
289
                        $e = self::fetch($id);
290
291
                        if (!is_array($e)) {
292
                            continue;
293
                        }
294
295
                        $e = current($e);
296
297
                        if (!$e instanceof Entry) {
298
                            continue;
299
                        }
300
301
                        // If we needed data, whole Entry objects will exist
302
                    } elseif ($needs_data) {
303
                        $e = $id;
304
                        $id = $e->get('id');
305
                    }
306
307
                    // Time to loop over it and send it to the fields.
308
                    // Note we can't rely on the `$fields` array as we may
309
                    // also be dealing with the case where `section_id` hasn't
310
                    // been provided
311
                    $entry_data = $e->getData();
0 ignored issues
show
Bug introduced by
The variable $e does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
312
313
                    foreach ($entry_data as $field_id => $data) {
314
                        $field = FieldManager::fetch($field_id);
315
                        $field->entryDataCleanup($id, $data);
0 ignored issues
show
Bug introduced by
The method entryDataCleanup cannot be called on $field (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
316
                    }
317
                }
318
            } else {
319
                foreach ($fields as $field) {
0 ignored issues
show
Bug introduced by
The variable $fields does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
320
                    $field->entryDataCleanup($chunk);
321
                }
322
            }
323
324
            $placeholders = Database::addPlaceholders($chunk);
325
            Symphony::Database()->delete('tbl_entries', " `id` IN ($placeholders) ", $chunk);
326
        }
327
328
        return true;
329
    }
330
331
    /**
332
     * This function will return an array of Entry objects given an ID or an array of ID's.
333
     * Do not provide `$entry_id` as an array if not specifying the `$section_id`. This function
334
     * is commonly passed custom SQL statements through the `$where` and `$join` parameters
335
     * that is generated by the fields of this section
336
     *
337
     * @param integer|array $entry_id
338
     *  An array of Entry ID's or an Entry ID to return
339
     * @param integer $section_id
340
     *  The ID of the Section that these entries are contained in
341
     * @param integer $limit
342
     *  The limit of entries to return
343
     * @param integer $start
344
     *  The starting offset of the entries to return
345
     * @param string $where
346
     *  Any custom WHERE clauses. The tbl_entries alias is `e`
347
     * @param string $joins
348
     *  Any custom JOIN's
349
     * @param boolean $group
350
     *  Whether the entries need to be grouped by Entry ID or not
351
     * @param boolean $buildentries
352
     *  Whether to return an array of entry ID's or Entry objects. Defaults to
353
     *  true, which will return Entry objects
354
     * @param array $element_names
355
     *  Choose whether to get data from a subset of fields or all fields in a section,
356
     *  by providing an array of field names. Defaults to null, which will load data
357
     *  from all fields in a section.
358
     * @param boolean $enable_sort
359
     *  Defaults to true, if false this function will not apply any sorting
360
     * @throws Exception
361
     * @return array
362
     *  If `$buildentries` is true, this function will return an array of Entry objects,
363
     *  otherwise it will return an associative array of Entry data from `tbl_entries`
364
     */
365
    public static function fetch($entry_id = null, $section_id = null, $limit = null, $start = null, $where = null, $joins = null, $group = false, $buildentries = true, $element_names = null, $enable_sort = true)
366
    {
367
        $sort = null;
368
369
        if (!$entry_id && !$section_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $section_id of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
370
            return false;
371
        }
372
373
        if (!$section_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $section_id of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
374
            $section_id = self::fetchEntrySectionID($entry_id);
0 ignored issues
show
Bug introduced by
It seems like $entry_id defined by parameter $entry_id on line 365 can also be of type array or null; however, EntryManager::fetchEntrySectionID() does only seem to accept integer, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
375
        }
376
377
        $section = SectionManager::fetch($section_id);
378
        if (!is_object($section)) {
379
            return false;
380
        }
381
382
        // SORTING
383
        // A single $entry_id doesn't need to be sorted on, or if it's explicitly disabled
384
        if ((!is_array($entry_id) && !is_null($entry_id) && is_int($entry_id)) || !$enable_sort) {
385
            $sort = null;
386
387
        // Check for RAND first, since this works independently of any specific field
388
        } elseif (self::$_fetchSortDirection === 'RAND') {
389
            $sort = 'ORDER BY RAND() ';
390
391
        // Handle Creation Date or the old Date sorting
392
        } elseif (self::$_fetchSortField === 'system:creation-date' || self::$_fetchSortField === 'date') {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of self::$_fetchSortField (integer) and 'system:creation-date' (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of self::$_fetchSortField (integer) and 'date' (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
393
            $sort = sprintf('ORDER BY `e`.`creation_date_gmt` %s', self::$_fetchSortDirection);
394
395
        // Handle Modification Date sorting
396
        } elseif (self::$_fetchSortField === 'system:modification-date') {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of self::$_fetchSortField (integer) and 'system:modification-date' (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
397
            $sort = sprintf('ORDER BY `e`.`modification_date_gmt` %s', self::$_fetchSortDirection);
398
399
        // Handle sorting for System ID
400 View Code Duplication
        } elseif (self::$_fetchSortField === 'system:id' || self::$_fetchSortField === 'id') {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of self::$_fetchSortField (integer) and 'system:id' (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of self::$_fetchSortField (integer) and 'id' (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
401
            $sort = sprintf('ORDER BY `e`.`id` %s', self::$_fetchSortDirection);
402
403
        // Handle when the sort field is an actual Field
404
        } elseif (self::$_fetchSortField && $field = FieldManager::fetch(self::$_fetchSortField)) {
405
            if ($field->isSortable()) {
0 ignored issues
show
Bug introduced by
The method isSortable cannot be called on $field (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
406
                $field->buildSortingSQL($joins, $where, $sort, self::$_fetchSortDirection);
0 ignored issues
show
Bug introduced by
The method buildSortingSQL cannot be called on $field (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
407
            }
408
409
        // Handle if the section has a default sorting field
410
        } elseif ($section->getSortingField() && $field = FieldManager::fetch($section->getSortingField())) {
0 ignored issues
show
Bug introduced by
It seems like $section->getSortingField() targeting Section::getSortingField() can also be of type string; however, FieldManager::fetch() does only seem to accept integer|array|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
411
            if ($field->isSortable()) {
0 ignored issues
show
Bug introduced by
The method isSortable cannot be called on $field (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
412
                $field->buildSortingSQL($joins, $where, $sort, $section->getSortingOrder());
0 ignored issues
show
Bug introduced by
The method buildSortingSQL cannot be called on $field (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
413
            }
414
415
            if (!empty($field) && !$group) {
416
                $group = $field->requiresSQLGrouping();
0 ignored issues
show
Bug introduced by
The method requiresSQLGrouping cannot be called on $field (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
417
            }
418 View Code Duplication
        } else if (self::$_fetchSortField === 'system:id' || self::$_fetchSortField === 'id') {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of self::$_fetchSortField (integer) and 'system:id' (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of self::$_fetchSortField (integer) and 'id' (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
419
            $sort = 'ORDER BY `e`.`id` ' . self::$_fetchSortDirection;
420
421
        } else {
422
            $sort = sprintf('ORDER BY `e`.`id` %s', self::$_fetchSortDirection);
423
        }
424
425
        if ($field && !$group) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $field 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...
426
            $group = $field->requiresSQLGrouping();
0 ignored issues
show
Bug introduced by
The variable $field does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The method requiresSQLGrouping cannot be called on $field (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
427
        }
428
429
        if ($entry_id && !is_array($entry_id)) {
430
            $entry_id = array($entry_id);
431
        }
432
433
        $sql = sprintf("
434
            SELECT %s`e`.`id`, `e`.section_id, `e`.`author_id`,
435
                `e`.`creation_date` AS `creation_date`,
436
                `e`.`modification_date` AS `modification_date`
437
            FROM `tbl_entries` AS `e`
438
            %s
439
            WHERE 1
440
            %s
441
            %s
442
            %s
443
            %s
444
            %s
445
            ",
446
            $group ? 'DISTINCT ' : '',
447
            $joins,
448
            $entry_id ? "AND `e`.`id` IN ('".implode("', '", $entry_id)."') " : '',
449
            $section_id ? sprintf("AND `e`.`section_id` = %d", $section_id) : '',
450
            $where,
451
            $sort,
452
            $limit ? sprintf('LIMIT %d, %d', $start, $limit) : ''
453
        );
454
455
        $rows = Symphony::Database()->fetch($sql);
456
457
        // Create UNIX timestamps, as it has always been (Re: #2501)
458
        foreach ($rows as &$entry) {
0 ignored issues
show
Bug introduced by
The expression $rows of type array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
459
            $entry['creation_date'] = DateTimeObj::get('U', $entry['creation_date']);
460
            $entry['modification_date'] = DateTimeObj::get('U', $entry['modification_date']);
461
        }
462
        unset($entry);
463
464
        return ($buildentries && (is_array($rows) && !empty($rows)) ? self::__buildEntries($rows, $section_id, $element_names) : $rows);
465
    }
466
467
    /**
468
     * Given an array of Entry data from `tbl_entries` and a section ID, return an
469
     * array of Entry objects. For performance reasons, it's possible to pass an array
470
     * of field handles via `$element_names`, so that only a subset of the section schema
471
     * will be queried. This function currently only supports Entry from one section at a
472
     * time.
473
     *
474
     * @param array $rows
475
     *  An array of Entry data from `tbl_entries` including the Entry ID, Entry section,
476
     *  the ID of the Author who created the Entry, and a Unix timestamp of creation
477
     * @param integer $section_id
478
     *  The section ID of the entries in the `$rows`
479
     * @param array $element_names
480
     *  Choose whether to get data from a subset of fields or all fields in a section,
481
     *  by providing an array of field names. Defaults to null, which will load data
482
     *  from all fields in a section.
483
     * @throws DatabaseException
484
     * @return array
485
     *  An array of Entry objects
486
     */
487
    public static function __buildEntries(array $rows, $section_id, $element_names = null)
488
    {
489
        $entries = array();
490
491
        if (empty($rows)) {
492
            return $entries;
493
        }
494
495
        $schema = FieldManager::fetchFieldIDFromElementName($element_names, $section_id);
0 ignored issues
show
Bug introduced by
It seems like $element_names defined by parameter $element_names on line 487 can also be of type null; however, FieldManager::fetchFieldIDFromElementName() does only seem to accept string|array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
496
497
        if (is_int($schema)) {
498
            $schema = array($schema);
499
        }
500
501
        $raw = array();
502
        $rows_string = '';
503
504
        // Append meta data:
505
        foreach ($rows as $entry) {
506
            $raw[$entry['id']]['meta'] = $entry;
507
            $rows_string .= $entry['id'] . ',';
508
        }
509
510
        $rows_string = trim($rows_string, ',');
511
512
        // Append field data:
513
        if (is_array($schema)) {
514
            foreach ($schema as $field_id) {
515
                try {
516
                    $row = Symphony::Database()->fetch("SELECT * FROM `tbl_entries_data_{$field_id}` WHERE `entry_id` IN ($rows_string) ORDER BY `id` ASC");
517
                } catch (Exception $e) {
518
                    // No data due to error
519
                    continue;
520
                }
521
522
                if (!is_array($row) || empty($row)) {
523
                    continue;
524
                }
525
526
                foreach ($row as $r) {
527
                    $entry_id = $r['entry_id'];
528
529
                    unset($r['id']);
530
                    unset($r['entry_id']);
531
532
                    if (!isset($raw[$entry_id]['fields'][$field_id])) {
533
                        $raw[$entry_id]['fields'][$field_id] = $r;
534
                    } else {
535
                        foreach (array_keys($r) as $key) {
536
                            // If this field already has been set, we need to take the existing
537
                            // value and make it array, adding the current value to it as well
538
                            // There is a special check incase the the field's value has been
539
                            // purposely set to null in the database.
540
                            if (
541
                                (
542
                                    isset($raw[$entry_id]['fields'][$field_id][$key])
543
                                    || is_null($raw[$entry_id]['fields'][$field_id][$key])
544
                                )
545
                                && !is_array($raw[$entry_id]['fields'][$field_id][$key])
546
                            ) {
547
                                $raw[$entry_id]['fields'][$field_id][$key] = array(
548
                                    $raw[$entry_id]['fields'][$field_id][$key],
549
                                    $r[$key]
550
                                );
551
552
                                // This key/value hasn't been set previously, so set it
553
                            } elseif (!isset($raw[$entry_id]['fields'][$field_id][$key])) {
554
                                $raw[$entry_id]['fields'][$field_id] = array($r[$key]);
555
556
                                // This key has been set and it's an array, so just append
557
                                // this value onto the array
558
                            } else {
559
                                $raw[$entry_id]['fields'][$field_id][$key][] = $r[$key];
560
                            }
561
                        }
562
                    }
563
                }
564
            }
565
        }
566
567
        // Loop over the array of entry data and convert it to an array of Entry objects
568
        foreach ($raw as $entry) {
569
            $obj = self::create();
570
571
            $obj->set('id', $entry['meta']['id']);
572
            $obj->set('author_id', $entry['meta']['author_id']);
573
            $obj->set('section_id', $entry['meta']['section_id']);
574
            $obj->set('creation_date', DateTimeObj::get('c', $entry['meta']['creation_date']));
575
576
            if (isset($entry['meta']['modification_date'])) {
577
                $obj->set('modification_date', DateTimeObj::get('c', $entry['meta']['modification_date']));
578
            } else {
579
                $obj->set('modification_date', $obj->get('creation_date'));
580
            }
581
582
            if (isset($entry['fields']) && is_array($entry['fields'])) {
583
                foreach ($entry['fields'] as $field_id => $data) {
584
                    $obj->setData($field_id, $data);
585
                }
586
            }
587
588
            $entries[] = $obj;
589
        }
590
591
        return $entries;
592
    }
593
594
595
    /**
596
     * Given an Entry ID, return the Section ID that it belongs to
597
     *
598
     * @param integer $entry_id
599
     *  The ID of the Entry to return it's section
600
     * @return integer
601
     *  The Section ID for this Entry's section
602
     */
603
    public static function fetchEntrySectionID($entry_id)
604
    {
605
        return Symphony::Database()->fetchVar('section_id', 0, "SELECT `section_id` FROM `tbl_entries` WHERE `id` = ? LIMIT 1",
606
            array($entry_id)
607
        );
608
    }
609
610
    /**
611
     * Return the count of the number of entries in a particular section.
612
     *
613
     * @param integer $section_id
614
     *  The ID of the Section where the Entries are to be counted
615
     * @param string $where
616
     *  Any custom WHERE clauses
617
     * @param string $joins
618
     *  Any custom JOIN's
619
     * @param boolean $group
620
     *  Whether the entries need to be grouped by Entry ID or not
621
     * @return integer
622
     */
623
    public static function fetchCount($section_id = null, $where = null, $joins = null, $group = false)
624
    {
625
        if (is_null($section_id)) {
626
            return false;
627
        }
628
629
        $section = SectionManager::fetch($section_id);
630
631
        if (!is_object($section)) {
632
            return false;
633
        }
634
635
        return Symphony::Database()->fetchVar('count', 0, sprintf("
636
                SELECT COUNT(%s`e`.id) as `count`
637
                FROM `tbl_entries` AS `e`
638
                %s
639
                WHERE `e`.`section_id` = %d
640
                %s
641
            ",
642
            $group ? 'DISTINCT ' : '',
643
            $joins,
644
            $section_id,
645
            $where
646
        ));
647
    }
648
649
    /**
650
     * Returns an array of Entry objects, with some basic pagination given
651
     * the number of Entry's to return and the current starting offset. This
652
     * function in turn calls the fetch function that does alot of the heavy
653
     * lifting. For instance, if there are 60 entries in a section and the pagination
654
     * dictates that per page, 15 entries are to be returned, by passing 2 to
655
     * the $page parameter you could return entries 15-30
656
     *
657
     * @param integer $page
658
     *  The page to return, defaults to 1
659
     * @param integer $section_id
660
     *  The ID of the Section that these entries are contained in
661
     * @param integer $entriesPerPage
662
     *  The number of entries to return per page.
663
     * @param string $where
664
     *  Any custom WHERE clauses
665
     * @param string $joins
666
     *  Any custom JOIN's
667
     * @param boolean $group
668
     *  Whether the entries need to be grouped by Entry ID or not
669
     * @param boolean $records_only
670
     *  If this is set to true, an array of Entry objects will be returned
671
     *  without any basic pagination information. Defaults to false
672
     * @param boolean $buildentries
673
     *  Whether to return an array of entry ID's or Entry objects. Defaults to
674
     *  true, which will return Entry objects
675
     * @param array $element_names
676
     *  Choose whether to get data from a subset of fields or all fields in a section,
677
     *  by providing an array of field names. Defaults to null, which will load data
678
     *  from all fields in a section.
679
     * @throws Exception
680
     * @return array
681
     *  Either an array of Entry objects, or an associative array containing
682
     *  the total entries, the start position, the entries per page and the
683
     *  Entry objects
684
     */
685
    public static function fetchByPage($page = 1, $section_id, $entriesPerPage, $where = null, $joins = null, $group = false, $records_only = false, $buildentries = true, array $element_names = null)
686
    {
687
        if ($entriesPerPage !== null && !is_string($entriesPerPage) && !is_numeric($entriesPerPage)) {
688
            throw new Exception(__('Entry limit specified was not a valid type. String or Integer expected.'));
689
        } elseif ($entriesPerPage === null) {
690
            $records = self::fetch(null, $section_id, null, null, $where, $joins, $group, $buildentries, $element_names);
691
692
            $count = self::fetchCount($section_id, $where, $joins, $group);
693
694
            $entries = array(
695
                'total-entries' => $count,
696
                'total-pages' => 1,
697
                'remaining-pages' => 0,
698
                'remaining-entries' => 0,
699
                'start' => 1,
700
                'limit' => $count,
701
                'records' => $records
702
            );
703
704
            return $entries;
705
        } else {
706
            $start = (max(1, $page) - 1) * $entriesPerPage;
707
708
            $records = ($entriesPerPage === '0' ? null : self::fetch(null, $section_id, $entriesPerPage, $start, $where, $joins, $group, $buildentries, $element_names));
709
710
            if ($records_only) {
711
                return array('records' => $records);
712
            }
713
714
            $entries = array(
715
                'total-entries' => self::fetchCount($section_id, $where, $joins, $group),
716
                'records' => $records,
717
                'start' => max(1, $start),
718
                'limit' => $entriesPerPage
719
            );
720
721
            $entries['remaining-entries'] = max(0, $entries['total-entries'] - ($start + $entriesPerPage));
722
            $entries['total-pages'] = max(1, ceil($entries['total-entries'] * (1 / $entriesPerPage)));
723
            $entries['remaining-pages'] = max(0, $entries['total-pages'] - $page);
724
725
            return $entries;
726
        }
727
    }
728
729
    /**
730
     * Creates a new Entry object using this class as the parent.
731
     *
732
     * @return Entry
733
     */
734
    public static function create()
735
    {
736
        return new Entry;
737
    }
738
}
739