Tracker_List_Model   F
last analyzed

Complexity

Total Complexity 67

Size/Duplication

Total Lines 353
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 1.49%

Importance

Changes 0
Metric Value
dl 0
loc 353
ccs 3
cts 201
cp 0.0149
rs 3.04
c 0
b 0
f 0
wmc 67
lcom 1
cbo 8

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
C update() 0 57 12
A updateByID() 0 11 2
A ignoreByID() 0 11 2
A deleteByID() 0 11 1
A deleteByIDList() 0 18 4
B getMalID() 0 50 7
A setMalID() 0 13 2
F get() 0 172 36

How to fix   Complexity   

Complex Class

Complex classes like Tracker_List_Model 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 Tracker_List_Model, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1); defined('BASEPATH') OR exit('No direct script access allowed');
2
3
class Tracker_List_Model extends Tracker_Base_Model {
4 96
	public function __construct() {
5 96
		parent::__construct();
6 96
	}
7
8
	public function get(?int $userID = NULL, string $category = 'all') {
9
		$userID = (is_null($userID) ? (int) $this->User->id : $userID);
10
		$enabledCategories = $this->getEnabledCategories($userID);
11
12
		$query = $this->db
13
			->select('tracker_chapters.*, CONVERT_TZ(tracker_chapters.last_updated, @@session.time_zone, \'+00:00\') AS utc_last_updated,
14
			          tracker_titles.site_id, tracker_titles.title, tracker_titles.title_url, tracker_titles.latest_chapter, tracker_titles.last_updated AS title_last_updated, tracker_titles.status AS title_status, tracker_titles.mal_id AS title_mal_id, tracker_titles.last_checked > DATE_SUB(NOW(), INTERVAL 1 WEEK) AS title_active, tracker_titles.failed_checks AS title_failed_checks,
15
			          tracker_sites.site, tracker_sites.site_class, tracker_sites.status AS site_status', FALSE)
16
			->from('tracker_chapters')
17
			->join('tracker_titles', 'tracker_chapters.title_id = tracker_titles.id', 'left')
18
			->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
19
			->where('tracker_chapters.user_id', $userID)
20
			->where('tracker_chapters.active', 'Y');
21
		if($category !== 'all' && in_array($category, array_keys($enabledCategories))) {
22
			$query->where('tracker_chapters.category', $category);
23
			$enabledCategories = [$category => $enabledCategories[$category]]; //hack
24
		}
25
		$result = $query->get();
26
27
		$arr = ['series' => [], 'extra_data' => ['inactive_titles' => []]];
28
		foreach($enabledCategories as $category => $name) {
29
			$arr['series'][$category] = [
30
				'name'         => $name,
31
				'manga'        => [],
32
				'unread_count' => 0
33
			];
34
		}
35
		if($result->num_rows() > 0) {
36
			foreach ($result->result() as $row) {
37
				$is_unread = intval((is_null($row->latest_chapter)) || ($row->latest_chapter == $row->ignore_chapter) || ($row->latest_chapter == $row->current_chapter) ? '1' : '0');
38
				$arr['series'][$row->category]['unread_count'] = (($arr['series'][$row->category]['unread_count'] ?? 0) + !$is_unread);
39
				$data = [
40
					'id' => $row->id,
41
					'generated_current_data' => $this->sites->{$row->site_class}->getChapterData($row->title_url, $row->current_chapter),
42
					'generated_latest_data'  => !is_null($row->latest_chapter) ? $this->sites->{$row->site_class}->getChapterData($row->title_url, $row->latest_chapter) : ['url' => '#', 'number' => 'No chapters found'] ,
43
					'generated_ignore_data'  => ($row->ignore_chapter ? $this->sites->{$row->site_class}->getChapterData($row->title_url, $row->ignore_chapter) : NULL),
44
45
					'full_title_url'        => $this->sites->{$row->site_class}->getFullTitleURL($row->title_url),
46
47
					'new_chapter_exists'    => $is_unread,
48
					'tag_list'              => $row->tags,
49
					'has_tags'              => !empty($row->tags),
50
51
					//TODO: We should have an option so chapter mal_id can take priority.
52
					'mal_id'                => $row->mal_id ?? $row->title_mal_id, //TODO: This should have an option
53
					'mal_type'              => (!is_null($row->mal_id) ? 'chapter' : (!is_null($row->title_mal_id) ? 'title' : 'none')),
54
55
					'last_updated' => $row->utc_last_updated,
56
57
					'title_data' => [
58
						'id'              => $row->title_id,
59
						'title'           => $row->title,
60
						'title_url'       => $row->title_url,
61
						'latest_chapter'  => $row->latest_chapter,
62
						'current_chapter' => $row->current_chapter,
63
						'ignore_chapter'  => $row->ignore_chapter,
64
						'last_updated'    => $row->title_last_updated,
65
						'time_class'      => get_time_class($row->title_last_updated),
66
						'status'          => (int) $row->title_status,
67
						'failed_checks'   => (int) $row->title_failed_checks,
68
						//NOTE: active is used to warn the user if a title hasn't updated (Maybe due to nobody active tracking it or other reasons).
69
						//      This will ONLY be false when an actively updating series (site enabled & title status = 0) hasn't updated within the past week.
70
						'active'          => ($row->site_status == 'disabled' || in_array($row->title_status, [/*complete*/ 1, /* one-shot */ 2, /* ignored */ 255]) || $row->title_active == 1)
71
					],
72
					'site_data' => [
73
						'id'         => $row->site_id,
74
						'site'       => $row->site,
75
						'status'     => $row->site_status
76
					]
77
				];
78
				$data['mal_icon'] = (!is_null($data['mal_id']) ? ($data['mal_id'] !== '0' ? "<a href=\"https://myanimelist.net/manga/{$data['mal_id']}\" class=\"mal-link\"><i class=\"sprite-site sprite-myanimelist-net\" title=\"{$data['mal_id']}\"></i></a>" : "<a><i class=\"sprite-site sprite-myanimelist-net-none\" title=\"none\"></i></a>") : '');
79
80
				$arr['series'][$row->category]['manga'][] = $data;
81
82
				if(!$data['title_data']['active']) {
83
					$arr['extra_data']['inactive_titles'][$data['full_title_url']] = $data['title_data']['title'];
84
				}
85
			}
86
87
			//FIXME: This is not good for speed, but we're kind of required to do this for UX purposes.
88
			//       Tablesorter has a weird sorting algorithm and has a delay before sorting which is why I'd like to avoid it.
89
			//FIXME: Is it possible to reduce duplication here without reducing speed?
90
			$sortOrder = $this->User_Options->get('list_sort_order', $userID);
91
			switch($this->User_Options->get('list_sort_type', $userID)) {
92
				case 'unread':
93
					foreach (array_keys($arr['series']) as $category) {
94
						usort($arr['series'][$category]['manga'], function ($a, $b) use($sortOrder) {
95
							$a_text = strtolower("{$a['new_chapter_exists']} - {$a['title_data']['title']}");
96
							$b_text = strtolower("{$b['new_chapter_exists']} - {$b['title_data']['title']}");
97
98
							if($sortOrder == 'asc') {
99
								return $a_text <=> $b_text;
100
							} else {
101
								return $b_text <=> $a_text;
102
							}
103
						});
104
					}
105
					break;
106
107
				case 'unread_latest':
108
					foreach (array_keys($arr['series']) as $category) {
109
						usort($arr['series'][$category]['manga'], function ($a, $b) use($sortOrder) {
110
							$a_text = $a['new_chapter_exists'];
111
							$b_text = $b['new_chapter_exists'];
112
113
							$a_text2 = new DateTime("{$a['title_data']['last_updated']}");
114
							$b_text2 = new DateTime("{$b['title_data']['last_updated']}");
115
116
							if($sortOrder == 'asc') {
117
								$unreadSort = ($a_text <=> $b_text);
118
								if($unreadSort) return $unreadSort;
119
								return $a_text2 <=> $b_text2;
120
							} else {
121
								$unreadSort = ($a_text <=> $b_text);
122
								if($unreadSort) return $unreadSort;
123
								return $b_text2 <=> $a_text2;
124
							}
125
						});
126
					}
127
					break;
128
129
				case 'alphabetical':
130
					foreach (array_keys($arr['series']) as $category) {
131
						usort($arr['series'][$category]['manga'], function($a, $b) use($sortOrder) {
132
							$a_text = strtolower("{$a['title_data']['title']}");
133
							$b_text = strtolower("{$b['title_data']['title']}");
134
135
							if($sortOrder == 'asc') {
136
								return $a_text <=> $b_text;
137
							} else {
138
								return $b_text <=> $a_text;
139
							}
140
						});
141
					}
142
					break;
143
144
				case 'my_status':
145
					foreach (array_keys($arr['series']) as $category) {
146
						usort($arr['series'][$category]['manga'], function($a, $b) use($sortOrder) {
147
							$a_text = strtolower("{$a['generated_current_data']['number']}");
148
							$b_text = strtolower("{$b['generated_current_data']['number']}");
149
150
							if($sortOrder == 'asc') {
151
								return $a_text <=> $b_text;
152
							} else {
153
								return $b_text <=> $a_text;
154
							}
155
						});
156
					}
157
					break;
158
159
				case 'latest':
160
					foreach (array_keys($arr['series']) as $category) {
161
						usort($arr['series'][$category]['manga'], function($a, $b) use($sortOrder) {
162
							$a_text = new DateTime("{$a['title_data']['last_updated']}");
163
							$b_text = new DateTime("{$b['title_data']['last_updated']}");
164
165
							if($sortOrder == 'asc') {
166
								return $a_text <=> $b_text;
167
							} else {
168
								return $b_text <=> $a_text;
169
							}
170
						});
171
					}
172
					break;
173
174
				default:
175
					break;
176
			}
177
		}
178
		return $arr;
179
	}
180
181
	public function update(int $userID, string $site, string $title, string $chapter, bool $active = TRUE, bool $returnTitleID = FALSE) {
182
		$success = FALSE;
183
		if($siteData = $this->Tracker->title->getSiteDataFromURL($site)) {
184
			//Validate user input
185
			if(!$this->sites->{$siteData->site_class}) {
186
				log_message('error', "{$siteData->site_class} Class doesn't exist?");
0 ignored issues
show
Unused Code introduced by
The call to the function log_message() seems unnecessary as the function has no side-effects.
Loading history...
187
				return FALSE;
188
			}
189
			if(!$this->sites->{$siteData->site_class}->isValidTitleURL($title)) {
190
				//Error is already logged via isValidTitleURL
191
				return FALSE;
192
			}
193
			if(!$this->sites->{$siteData->site_class}->isValidChapter($chapter)) {
194
				//Error is already logged via isValidChapter
195
				return FALSE;
196
			}
197
198
			//NOTE: If the title doesn't exist it will be created. This maybe isn't perfect, but it works for now.
199
			$titleID = $this->Tracker->title->getID($title, (int) $siteData->id);
200
			if($titleID === 0) {
201
				//Something went wrong.
202
				log_message('error', "TitleID = 0 for {$title} @ {$siteData->id}");
0 ignored issues
show
Unused Code introduced by
The call to the function log_message() seems unnecessary as the function has no side-effects.
Loading history...
203
				return FALSE;
204
			}
205
206
			$idQuery = $this->db->select('id')
207
			                    ->where('user_id', $userID)
208
			                    ->where('title_id', $titleID)
209
			                    ->get('tracker_chapters');
210
			if($idQuery->num_rows() > 0) {
211
				$success = (bool) $this->db->set(['current_chapter' => $chapter, 'active' => 'Y', 'last_updated' => NULL, 'ignore_chapter' => NULL])
212
				                           ->where('user_id', $userID)
213
				                           ->where('title_id', $titleID)
214
				                           ->update('tracker_chapters');
215
216
				if($success) {
217
					$idQueryRow = $idQuery->row();
218
					$this->History->userUpdateTitle((int) $idQueryRow->id, $chapter);
219
				}
220
			} else {
221
				$category = $this->User_Options->get('default_series_category', $userID);
222
223
				$success = (bool) $this->db->insert('tracker_chapters', [
224
					'user_id'         => $userID,
225
					'title_id'        => $titleID,
226
					'current_chapter' => $chapter,
227
					'category'        => $category,
228
					'active'          => ($active ? 'Y' : 'N')
229
				]);
230
231
				if($success) {
232
					$this->History->userAddTitle((int) $this->db->insert_id(), $chapter, $category);
0 ignored issues
show
Bug introduced by
The method insert_id() does not exist on CI_DB_query_builder. Did you maybe mean insert()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
233
				}
234
			}
235
		}
236
		return ($returnTitleID ? ($success ? ['id' => $titleID, 'chapter' => $this->sites->{$siteData->site_class}->getChapterData($title, $chapter)['number']] : $success) : $success);
0 ignored issues
show
Bug introduced by
The variable $titleID 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...
237
	}
238
	public function updateByID(int $userID, int $chapterID, string $chapter) : bool {
239
		$success = (bool) $this->db->set(['current_chapter' => $chapter, 'active' => 'Y', 'last_updated' => NULL])
240
		                           ->where('user_id', $userID)
241
		                           ->where('id', $chapterID)
242
		                           ->update('tracker_chapters');
243
244
		if($success) {
245
			$this->History->userUpdateTitle($chapterID, $chapter);
246
		}
247
		return  $success;
248
	}
249
250
	public function ignoreByID(int $userID, int $chapterID, string $chapter) : bool {
251
		$success = (bool) $this->db->set(['ignore_chapter' => $chapter, 'active' => 'Y', 'last_updated' => NULL])
252
		                           ->where('user_id', $userID)
253
		                           ->where('id', $chapterID)
254
		                           ->update('tracker_chapters');
255
256
		if($success) {
257
			$this->History->userIgnoreTitle($chapterID, $chapter);
258
		}
259
		return  $success;
260
	}
261
262
	public function deleteByID(int $userID, int $chapterID) {
263
		//Series are not fully deleted, they are just marked as inactive as to hide them from the user.
264
		//This is to allow user history to function properly.
265
266
		$success = $this->db->set(['active' => 'N', 'last_updated' => NULL])
267
		                    ->where('user_id', $userID)
268
		                    ->where('id', $chapterID)
269
		                    ->update('tracker_chapters');
270
271
		return (bool) $success;
272
	}
273
	public function deleteByIDList(array $idList) : array {
274
		/*
275
		 * 0 = Success
276
		 * 1 = Invalid IDs
277
		 */
278
		$status = ['code' => 0];
279
280
		foreach($idList as $id) {
281
			if(!(ctype_digit($id) && $this->deleteByID($this->User->id, (int) $id))) {
282
				$status['code'] = 1;
283
			} else {
284
				//Delete was successful, update history too.
285
				$this->History->userRemoveTitle((int) $id);
286
			}
287
		}
288
289
		return $status;
290
	}
291
292
	public function getMalID(int $userID, int $titleID) : ?array{
293
		$malIDArr = NULL;
294
295
		//NEW METHOD
296
		//TODO: OPTION, USE BACKEND MAL ID DB WHERE POSSIBLE (DEFAULT TRUE)
297
298
		$queryC = $this->db->select('mal_id')
299
		                   ->where('user_id', $userID)
300
		                   ->where('title_id', $titleID)
301
		                   ->where('mal_id IS NOT NULL', NULL, FALSE)
302
		                   ->get('tracker_chapters');
303
304
		if($queryC->num_rows() > 0 && ($rowC = $queryC->row())) {
305
			$malIDArr = [
306
				'id'   => ($rowC->mal_id == '0' ? 'none' : $rowC->mal_id),
307
				'type' => 'chapter'
308
			];
309
		} else {
310
			$queryT = $this->db->select('mal_id')
311
			                   ->where('id', $titleID)
312
			                   ->get('tracker_titles');
313
314
			if($queryT->num_rows() > 0 && ($rowT = $queryT->row())) {
315
				$malIDArr = [
316
					'id'   => ($rowT->mal_id == '0' ? 'none' : $rowT->mal_id),
317
					'type' => 'title'
318
				];
319
			}
320
		}
321
322
		//Old Method.
323
		/*if(is_null($malIDArr)) {
324
			$queryC2 = $this->db->select('tags')
325
			                  ->where('user_id', $userID)
326
			                  ->where('title_id', $titleID)
327
			                  ->get('tracker_chapters');
328
329
			if($queryC2->num_rows() > 0 && ($tag_string = $queryC2->row()->tags) && !is_null($tag_string)) {
330
				$arr   = preg_grep('/^mal:([0-9]+|none)$/', explode(',', $tag_string));
331
				if(!empty($arr)) {
332
					$malIDArr = [
333
						'id'   => explode(':', $arr[0])[1],
334
						'type' => 'chapter'
335
					];
336
				}
337
			}
338
		}*/
339
340
		return $malIDArr;
341
	}
342
	public function setMalID(int $userID, int $chapterID, ?int $malID) : bool {
343
		//TODO: Handle NULL?
344
		$success = (bool) $this->db->set(['mal_id' => $malID, 'active' => 'Y', 'last_updated' => NULL])
345
		                           ->where('user_id', $userID)
346
		                           ->where('id', $chapterID)
347
		                           ->update('tracker_chapters');
348
349
		if($success) {
350
			//MAL id update was successful, update history
351
			$this->History->userSetMalID($chapterID, $malID);
352
		}
353
		return $success;
354
	}
355
}
356