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 classes like ALERT 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 ALERT, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 39 | class ALERT { |
||
|
|
|||
| 40 | |||
| 41 | public $token_checked = null; |
||
| 42 | private $alert_id = ""; |
||
| 43 | public $email = ""; |
||
| 44 | public $criteria = ""; // Sets the terms that are used to produce the search results. |
||
| 45 | |||
| 46 | private $db; |
||
| 47 | |||
| 48 | 22 | public function __construct() { |
|
| 49 | 22 | $this->db = new ParlDB; |
|
| 50 | 22 | } |
|
| 51 | |||
| 52 | // FUNCTION: fetch_between |
||
| 53 | |||
| 54 | 1 | public function fetch_between($confirmed, $deleted, $start_date, $end_date) { |
|
| 55 | // Return summary data on all the alerts that were created between $start_date |
||
| 56 | // and $end_date (inclusive) and whose confirmed and deleted values match the booleans |
||
| 57 | // passed in $confirmed and $deleted |
||
| 58 | 1 | $q = $this->db->query("SELECT criteria, count(*) as cnt |
|
| 59 | FROM alerts |
||
| 60 | WHERE confirmed = :confirmed |
||
| 61 | AND deleted = :deleted |
||
| 62 | AND created >= :start_date |
||
| 63 | AND created <= :end_date |
||
| 64 | 1 | GROUP BY criteria", array( |
|
| 65 | 1 | ':confirmed' => $confirmed, |
|
| 66 | 1 | ':deleted' => $deleted, |
|
| 67 | 1 | ':start_date' => $start_date, |
|
| 68 | ':end_date' => $end_date |
||
| 69 | 1 | )); |
|
| 70 | 1 | $data = array(); |
|
| 71 | 1 | for ($row=0; $row<$q->rows(); $row++) { |
|
| 72 | 1 | $contents = array('criteria' => $q->field($row, 'criteria'), 'count' => $q->field($row, 'cnt')); |
|
| 73 | 1 | $data[] = $contents; |
|
| 74 | 1 | } |
|
| 75 | 1 | $data = array ('alerts' => $data); |
|
| 76 | |||
| 77 | 1 | return $data; |
|
| 78 | } |
||
| 79 | |||
| 80 | // FUNCTION: fetch |
||
| 81 | |||
| 82 | 2 | public function fetch($confirmed, $deleted) { |
|
| 83 | // Pass it an alert id and it will fetch data about alerts from the db |
||
| 84 | // and put it all in the appropriate variables. |
||
| 85 | // Normal usage is for $confirmed variable to be set to true |
||
| 86 | // and $deleted variable to be set to false |
||
| 87 | // so that only live confirmed alerts are chosen. |
||
| 88 | |||
| 89 | // Look for this alert_id's details. |
||
| 90 | 2 | $q = $this->db->query("SELECT alert_id, |
|
| 91 | email, |
||
| 92 | criteria, |
||
| 93 | registrationtoken, |
||
| 94 | deleted, |
||
| 95 | confirmed |
||
| 96 | FROM alerts |
||
| 97 | 2 | WHERE confirmed =" . $confirmed . |
|
| 98 | 2 | " AND deleted=" . $deleted . |
|
| 99 | 2 | ' ORDER BY email'); |
|
| 100 | |||
| 101 | 2 | $data = array(); |
|
| 102 | |||
| 103 | 2 | for ($row=0; $row<$q->rows(); $row++) { |
|
| 104 | $contents = array( |
||
| 105 | 2 | 'alert_id' => $q->field($row, 'alert_id'), |
|
| 106 | 2 | 'email' => $q->field($row, 'email'), |
|
| 107 | 2 | 'criteria' => $q->field($row, 'criteria'), |
|
| 108 | 2 | 'registrationtoken' => $q->field($row, 'registrationtoken'), |
|
| 109 | 2 | 'confirmed' => $q->field($row, 'confirmed'), |
|
| 110 | 2 | 'deleted' => $q->field($row, 'deleted') |
|
| 111 | 2 | ); |
|
| 112 | 2 | $data[] = $contents; |
|
| 113 | 2 | } |
|
| 114 | 2 | $info = "Alert"; |
|
| 115 | 2 | $data = array ('info' => $info, 'data' => $data); |
|
| 116 | |||
| 117 | |||
| 118 | 2 | return $data; |
|
| 119 | } |
||
| 120 | |||
| 121 | 3 | public function add($details, $confirmation_email=false, $instantly_confirm=true) { |
|
| 122 | |||
| 123 | // Adds a new alert's info into the database. |
||
| 124 | // Then calls another function to send them a confirmation email. |
||
| 125 | // $details is an associative array of all the alert's details, of the form: |
||
| 126 | // array ( |
||
| 127 | // "email" => "[email protected]", |
||
| 128 | // "criteria" => "speaker:521", |
||
| 129 | // etc... using the same keys as the object variable names. |
||
| 130 | // ) |
||
| 131 | |||
| 132 | 3 | $criteria = \MySociety\TheyWorkForYou\Utility\Alert::detailsToCriteria($details); |
|
| 133 | |||
| 134 | 3 | $q = $this->db->query("SELECT * FROM alerts |
|
| 135 | WHERE email = :email |
||
| 136 | AND criteria = :criteria |
||
| 137 | 3 | AND confirmed=1", array( |
|
| 138 | 3 | ':email' => $details['email'], |
|
| 139 | ':criteria' => $criteria |
||
| 140 | 3 | )); |
|
| 141 | 3 | if ($q->rows() > 0) { |
|
| 142 | 2 | $deleted = $q->field(0, 'deleted'); |
|
| 143 | 2 | if ($deleted) { |
|
| 144 | 1 | $this->db->query("UPDATE alerts SET deleted=0 |
|
| 145 | WHERE email = :email |
||
| 146 | AND criteria = :criteria |
||
| 147 | 1 | AND confirmed=1", array( |
|
| 148 | 1 | ':email' => $details['email'], |
|
| 149 | ':criteria' => $criteria |
||
| 150 | 1 | )); |
|
| 151 | 1 | return 1; |
|
| 152 | } else { |
||
| 153 | 1 | return -2; |
|
| 154 | } |
||
| 155 | } |
||
| 156 | |||
| 157 | 1 | $q = $this->db->query("INSERT INTO alerts ( |
|
| 158 | email, criteria, postcode, deleted, confirmed, created |
||
| 159 | ) VALUES ( |
||
| 160 | :email, |
||
| 161 | :criteria, |
||
| 162 | :pc, |
||
| 163 | '0', '0', NOW() |
||
| 164 | ) |
||
| 165 | 1 | ", array( |
|
| 166 | 1 | ':email' => $details['email'], |
|
| 167 | 1 | ':criteria' => $criteria, |
|
| 168 | 1 | ':pc' => $details['pc'], |
|
| 169 | 1 | )); |
|
| 170 | |||
| 171 | 1 | if ($q->success()) { |
|
| 172 | |||
| 173 | // Get the alert id so that we can perform the updates for confirmation |
||
| 174 | |||
| 175 | 1 | $this->alert_id = $q->insert_id(); |
|
| 176 | 1 | $this->criteria = $criteria; |
|
| 177 | |||
| 178 | // We have to set the alert's registration token. |
||
| 179 | // This will be sent to them via email, so we can confirm they exist. |
||
| 180 | // The token will be the first 16 characters of a hash. |
||
| 181 | |||
| 182 | // This gives a code for their email address which is then joined |
||
| 183 | // to the timestamp so as to provide a unique ID for each alert. |
||
| 184 | |||
| 185 | 1 | $token = substr( password_hash($details["email"] . microtime(), PASSWORD_BCRYPT), 29, 16 ); |
|
| 186 | |||
| 187 | // Full stops don't work well at the end of URLs in emails, |
||
| 188 | // so replace them. We won't be doing anything clever with the hash |
||
| 189 | // stuff, just need to match this token. |
||
| 190 | |||
| 191 | 1 | $this->registrationtoken = strtr($token, '.', 'X'); |
|
| 192 | |||
| 193 | // Add that to the database. |
||
| 194 | |||
| 195 | 1 | $r = $this->db->query("UPDATE alerts |
|
| 196 | SET registrationtoken = :registration_token |
||
| 197 | WHERE alert_id = :alert_id |
||
| 198 | 1 | ", array( |
|
| 199 | 1 | ':registration_token' => $this->registrationtoken, |
|
| 200 | 1 | ':alert_id' => $this->alert_id |
|
| 201 | 1 | )); |
|
| 202 | |||
| 203 | 1 | if ($r->success()) { |
|
| 204 | // Updated DB OK. |
||
| 205 | |||
| 206 | 1 | if ($confirmation_email) { |
|
| 207 | // Right, send the email... |
||
| 208 | $success = $this->send_confirmation_email($details); |
||
| 209 | |||
| 210 | if ($success) { |
||
| 211 | // Email sent OK |
||
| 212 | return 1; |
||
| 213 | } else { |
||
| 214 | // Couldn't send the email. |
||
| 215 | return -1; |
||
| 216 | } |
||
| 217 | 1 | } elseif ($instantly_confirm) { |
|
| 218 | // No confirmation email needed. |
||
| 219 | 1 | $s = $this->db->query("UPDATE alerts |
|
| 220 | SET confirmed = '1' |
||
| 221 | WHERE alert_id = :alert_id |
||
| 222 | 1 | ", array( |
|
| 223 | 1 | ':alert_id' => $this->alert_id |
|
| 224 | 1 | )); |
|
| 225 | 1 | return 1; |
|
| 226 | } |
||
| 227 | } else { |
||
| 228 | // Couldn't add the registration token to the DB. |
||
| 229 | return -1; |
||
| 230 | } |
||
| 231 | |||
| 232 | } else { |
||
| 233 | // Couldn't add the user's data to the DB. |
||
| 234 | return -1; |
||
| 235 | } |
||
| 236 | } |
||
| 237 | |||
| 238 | // FUNCTION: send_confirmation_email |
||
| 239 | |||
| 240 | public function send_confirmation_email($details) { |
||
| 241 | |||
| 242 | // After we've add()ed an alert we'll be sending them |
||
| 243 | // a confirmation email with a link to confirm their address. |
||
| 244 | // $details is the array we just sent to add(), and which it's |
||
| 245 | // passed on to us here. |
||
| 246 | // A brief check of the facts... |
||
| 247 | if (!is_numeric($this->alert_id) || |
||
| 248 | !isset($details['email']) || |
||
| 249 | $details['email'] == '') { |
||
| 250 | return false; |
||
| 251 | } |
||
| 252 | |||
| 253 | // We prefix the registration token with the alert's id and '-'. |
||
| 254 | // Not for any particularly good reason, but we do. |
||
| 255 | |||
| 256 | $urltoken = $this->alert_id . '-' . $this->registrationtoken; |
||
| 257 | |||
| 258 | if ( isset($details['confirm_base']) && $details['confirm_base'] !== '' ) { |
||
| 259 | $confirmurl = $details['confirm_base'] . $urltoken; |
||
| 260 | } else { |
||
| 261 | $confirmurl = 'https://' . DOMAIN . '/A/' . $urltoken; |
||
|
1 ignored issue
–
show
|
|||
| 262 | } |
||
| 263 | |||
| 264 | // Arrays we need to send a templated email. |
||
| 265 | $data = array ( |
||
| 266 | 'to' => $details['email'], |
||
| 267 | 'template' => 'alert_confirmation' |
||
| 268 | ); |
||
| 269 | |||
| 270 | $merge = array ( |
||
| 271 | 'FIRSTNAME' => 'THEY WORK FOR YOU', |
||
| 272 | 'LASTNAME' => ' ALERT CONFIRMATION', |
||
| 273 | 'CONFIRMURL' => $confirmurl, |
||
| 274 | 'CRITERIA' => $this->criteria_pretty() |
||
| 275 | ); |
||
| 276 | |||
| 277 | $success = send_template_email($data, $merge); |
||
| 278 | if ($success) { |
||
| 279 | return true; |
||
| 280 | } else { |
||
| 281 | return false; |
||
| 282 | } |
||
| 283 | } |
||
| 284 | |||
| 285 | public function send_already_signedup_email($details) { |
||
| 286 | $data = array ( |
||
| 287 | 'to' => $details['email'], |
||
| 288 | 'template' => 'alert_already_signedup' |
||
| 289 | ); |
||
| 290 | |||
| 291 | $criteria = \MySociety\TheyWorkForYou\Utility\Alert::detailsToCriteria($details); |
||
| 292 | $this->criteria = $criteria; |
||
| 293 | |||
| 294 | $merge = array ( |
||
| 295 | 'FIRSTNAME' => 'THEY WORK FOR YOU', |
||
| 296 | 'LASTNAME' => ' ALERT ALREADY SIGNED UP', |
||
| 297 | 'CRITERIA' => $this->criteria_pretty() |
||
| 298 | ); |
||
| 299 | |||
| 300 | $success = send_template_email($data, $merge); |
||
| 301 | if ($success) { |
||
| 302 | return true; |
||
| 303 | } else { |
||
| 304 | return false; |
||
| 305 | } |
||
| 306 | } |
||
| 307 | |||
| 308 | 2 | public function fetch_by_mp($email, $pid) { |
|
| 309 | 2 | $q = $this->db->query("SELECT alert_id FROM alerts |
|
| 310 | WHERE confirmed AND NOT deleted |
||
| 311 | AND email = :email |
||
| 312 | 2 | AND criteria = :criteria", array( |
|
| 313 | 2 | ':email' => $email, |
|
| 314 | ':criteria' => 'speaker:' . $pid |
||
| 315 | 2 | )); |
|
| 316 | 2 | if ($q->rows() > 0) { |
|
| 317 | 1 | return true; |
|
| 318 | } else { |
||
| 319 | 1 | return false; |
|
| 320 | } |
||
| 321 | } |
||
| 322 | |||
| 323 | 2 | public function email_exists($email) { |
|
| 324 | // Returns true if there's a user with this email address. |
||
| 325 | |||
| 326 | 2 | if ($email != "") { |
|
| 327 | 2 | $q = $this->db->query("SELECT alert_id FROM alerts |
|
| 328 | 2 | WHERE email = :email", array( |
|
| 329 | ':email' => $email |
||
| 330 | 2 | )); |
|
| 331 | 2 | if ($q->rows() > 0) { |
|
| 332 | 1 | return true; |
|
| 333 | } else { |
||
| 334 | 1 | return false; |
|
| 335 | } |
||
| 336 | } else { |
||
| 337 | return false; |
||
| 338 | } |
||
| 339 | |||
| 340 | } |
||
| 341 | |||
| 342 | 12 | public function check_token($token) { |
|
| 374 | } |
||
| 375 | |||
| 376 | public function fetch_by_token($confirmation) { |
||
| 377 | $q = $this->db->query("SELECT alert_id, email, criteria |
||
| 378 | FROM alerts |
||
| 379 | WHERE registrationtoken = :registration_token |
||
| 380 | ", array( |
||
| 381 | ':registration_token' => $confirmation |
||
| 382 | ) |
||
| 383 | ); |
||
| 384 | |||
| 385 | if (!$q->rows()) { |
||
| 386 | return false; |
||
| 387 | } else { |
||
| 388 | return array( |
||
| 389 | 'id' => $q->field(0, 'alert_id'), |
||
| 390 | 'email' => $q->field(0, 'email'), |
||
| 391 | 'criteria' => $q->field(0, 'criteria'), |
||
| 392 | ); |
||
| 393 | } |
||
| 394 | } |
||
| 395 | |||
| 396 | // The user has clicked the link in their confirmation email |
||
| 397 | // and the confirm page has passed the token from the URL to here. |
||
| 398 | // If all goes well the alert will be confirmed. |
||
| 399 | // The alert will be active when scripts run each day to send the actual emails. |
||
| 400 | 2 | public function confirm($token) { |
|
| 401 | 2 | if (!($alert = $this->check_token($token))) return false; |
|
| 402 | 1 | $this->criteria = $alert['criteria']; |
|
| 403 | 1 | $this->email = $alert['email']; |
|
| 404 | 1 | $r = $this->db->query("UPDATE alerts SET confirmed = 1, deleted = 0 WHERE alert_id = :alert_id", array( |
|
| 405 | 1 | ':alert_id' => $alert['id'] |
|
| 406 | 1 | )); |
|
| 407 | |||
| 408 | 1 | return $r->success(); |
|
| 409 | } |
||
| 410 | |||
| 411 | // The user has clicked the link in their delete confirmation email |
||
| 412 | // and the deletion page has passed the token from the URL to here. |
||
| 413 | // If all goes well the alert will be deleted. |
||
| 414 | 2 | public function delete($token) { |
|
| 415 | 2 | if (!($alert = $this->check_token($token))) return false; |
|
| 416 | 1 | $r = $this->db->query("DELETE FROM alerts WHERE alert_id = :alert_id", array( |
|
| 417 | 1 | ':alert_id' => $alert['id'] |
|
| 418 | 1 | )); |
|
| 419 | |||
| 420 | 1 | return $r->success(); |
|
| 421 | } |
||
| 422 | |||
| 423 | 2 | public function suspend($token) { |
|
| 424 | 2 | if (!($alert = $this->check_token($token))) return false; |
|
| 425 | 1 | $r = $this->db->query("UPDATE alerts SET deleted = 2 WHERE alert_id = :alert_id", array( |
|
| 426 | 1 | ':alert_id' => $alert['id'] |
|
| 427 | 1 | )); |
|
| 428 | |||
| 429 | 1 | return $r->success(); |
|
| 430 | } |
||
| 431 | |||
| 432 | 2 | public function resume($token) { |
|
| 433 | 2 | if (!($alert = $this->check_token($token))) return false; |
|
| 434 | 1 | $r = $this->db->query("UPDATE alerts SET deleted = 0 WHERE alert_id = :alert_id", array( |
|
| 435 | 1 | ':alert_id' => $alert['id'] |
|
| 436 | 1 | )); |
|
| 437 | |||
| 438 | 1 | return $r->success(); |
|
| 439 | } |
||
| 440 | |||
| 441 | // Getters |
||
| 442 | public function email() { return $this->email; } |
||
| 444 | public function criteria_pretty($html = false) { |
||
| 445 | $criteria = explode(' ',$this->criteria); |
||
| 446 | $spokenby = array_values(\MySociety\TheyWorkForYou\Utility\Search::speakerNamesForIDs($this->criteria)); |
||
| 447 | $words = array(); |
||
| 448 | foreach ($criteria as $c) { |
||
| 449 | if (!preg_match('#^speaker:(\d+)#',$c,$m)) { |
||
| 450 | $words[] = $c; |
||
| 451 | } |
||
| 452 | } |
||
| 457 | } |
||
| 458 | |||
| 459 | } |
||
| 460 |
You can fix this by adding a namespace to your class:
When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.