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 Job 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 Job, and based on these observations, apply Extract Interface, too.
| 1 | <?php | ||
| 45 | class Job extends Model | ||
| 46 | { | ||
| 47 | use SoftDeletes, ForUser; | ||
| 48 |     use Searchable { | ||
| 49 | getIndexBody as parentGetIndexBody; | ||
| 50 | } | ||
| 51 | |||
| 52 | const MONTH = 1; | ||
| 53 | const YEAR = 2; | ||
| 54 | const WEEK = 3; | ||
| 55 | const HOUR = 4; | ||
| 56 | |||
| 57 | /** | ||
| 58 | * Filling each field adds points to job offer score. | ||
| 59 | */ | ||
| 60 | const SCORE_CONFIG = [ | ||
| 61 | 'job' => ['description' => 10, 'salary_from' => 25, 'salary_to' => 25, 'city' => 15], | ||
| 62 | 'firm' => ['name' => 15, 'logo' => 5, 'website' => 1, 'description' => 5] | ||
| 63 | ]; | ||
| 64 | |||
| 65 | /** | ||
| 66 | * The attributes that are mass assignable. | ||
| 67 | * | ||
| 68 | * @var array | ||
| 69 | */ | ||
| 70 | protected $fillable = [ | ||
| 71 | 'title', | ||
| 72 | 'description', | ||
| 73 | 'requirements', | ||
| 74 | 'recruitment', | ||
| 75 | 'is_remote', | ||
| 76 | 'remote_range', | ||
| 77 | 'country_id', | ||
| 78 | 'salary_from', | ||
| 79 | 'salary_to', | ||
| 80 | 'currency_id', | ||
| 81 | 'rate_id', | ||
| 82 | 'employment_id', | ||
| 83 | 'deadline_at', | ||
| 84 | 'email', | ||
| 85 | 'enable_apply' | ||
| 86 | ]; | ||
| 87 | |||
| 88 | /** | ||
| 89 | * Default fields values. | ||
| 90 | * | ||
| 91 | * @var array | ||
| 92 | */ | ||
| 93 | protected $attributes = [ | ||
| 94 | 'enable_apply' => true, | ||
| 95 | 'is_remote' => false, | ||
| 96 | 'title' => '' | ||
| 97 | ]; | ||
| 98 | |||
| 99 | /** | ||
| 100 | * Cast to when calling toArray() (for example before index in elasticsearch). | ||
| 101 | * | ||
| 102 | * @var array | ||
| 103 | */ | ||
| 104 | protected $casts = ['is_remote' => 'boolean', 'enable_apply' => 'boolean']; | ||
| 105 | |||
| 106 | /** | ||
| 107 | * @var string | ||
| 108 | */ | ||
| 109 | protected $dateFormat = 'Y-m-d H:i:se'; | ||
| 110 | |||
| 111 | /** | ||
| 112 | * @var array | ||
| 113 | */ | ||
| 114 | protected $appends = ['deadline']; | ||
| 115 | |||
| 116 | /** | ||
| 117 | * Elasticsearch type mapping | ||
| 118 | * | ||
| 119 | * @var array | ||
| 120 | */ | ||
| 121 | protected $mapping = [ | ||
| 122 | "id" => [ | ||
| 123 | "type" => "long" | ||
| 124 | ], | ||
| 125 | "locations" => [ | ||
| 126 | "type" => "nested", | ||
| 127 | "properties" => [ | ||
| 128 | "city" => [ | ||
| 129 | "type" => "string", | ||
| 130 | "analyzer" => "keyword_asciifolding_analyzer", | ||
| 131 | "fields" => [ | ||
| 132 | "original" => ["type" => "text", "analyzer" => "keyword_analyzer", "fielddata" => true] | ||
| 133 | ] | ||
| 134 | ], | ||
| 135 | "coordinates" => [ | ||
| 136 | "type" => "geo_point" | ||
| 137 | ] | ||
| 138 | ] | ||
| 139 | ], | ||
| 140 | "title" => [ | ||
| 141 | "type" => "text", | ||
| 142 | "analyzer" => "default_analyzer" | ||
| 143 | ], | ||
| 144 | "description" => [ | ||
| 145 | "type" => "text", | ||
| 146 | "analyzer" => "default_analyzer" | ||
| 147 | ], | ||
| 148 | "requirements" => [ | ||
| 149 | "type" => "text", | ||
| 150 | "analyzer" => "default_analyzer" | ||
| 151 | ], | ||
| 152 | "is_remote" => [ | ||
| 153 | "type" => "boolean" | ||
| 154 | ], | ||
| 155 | "remote_range" => [ | ||
| 156 | "type" => "integer" | ||
| 157 | ], | ||
| 158 | "tags" => [ | ||
| 159 | "type" => "text", | ||
| 160 | "fields" => [ | ||
| 161 | "original" => ["type" => "keyword"] | ||
| 162 | ] | ||
| 163 | ], | ||
| 164 | "firm" => [ | ||
| 165 | "type" => "object", | ||
| 166 | "properties" => [ | ||
| 167 | "name" => [ | ||
| 168 | "type" => "text", | ||
| 169 | "analyzer" => "default_analyzer", | ||
| 170 | "fields" => [ | ||
| 171 | // filtrujemy firmy po tym polu | ||
| 172 | "original" => ["type" => "text", "analyzer" => "keyword_analyzer", "fielddata" => true] | ||
| 173 | ] | ||
| 174 | ] | ||
| 175 | ] | ||
| 176 | ], | ||
| 177 | "created_at" => [ | ||
| 178 | "type" => "date", | ||
| 179 | "format" => "yyyy-MM-dd HH:mm:ss" | ||
| 180 | ], | ||
| 181 | "updated_at" => [ | ||
| 182 | "type" => "date", | ||
| 183 | "format" => "yyyy-MM-dd HH:mm:ss" | ||
| 184 | ], | ||
| 185 | "deadline_at" => [ | ||
| 186 | "type" => "date", | ||
| 187 | "format" => "yyyy-MM-dd HH:mm:ss" | ||
| 188 | ], | ||
| 189 | "salary" => [ | ||
| 190 | "type" => "float" | ||
| 191 | ], | ||
| 192 | "score" => [ | ||
| 193 | "type" => "long" | ||
| 194 | ], | ||
| 195 | "rank" => [ | ||
| 196 | "type" => "float" | ||
| 197 | ] | ||
| 198 | ]; | ||
| 199 | |||
| 200 | /** | ||
| 201 | * We need to set firm id to null offer is private | ||
| 202 | */ | ||
| 203 | public static function boot() | ||
| 225 | |||
| 226 | /** | ||
| 227 | * @return string[] | ||
| 228 | */ | ||
| 229 | public static function getRatesList() | ||
| 233 | |||
| 234 | /** | ||
| 235 | * @return string[] | ||
| 236 | */ | ||
| 237 | public static function getEmploymentList() | ||
| 241 | |||
| 242 | /** | ||
| 243 | * @return array | ||
| 244 | */ | ||
| 245 | public static function getRemoteRangeList() | ||
| 255 | |||
| 256 | /** | ||
| 257 | * @return int | ||
| 258 | */ | ||
| 259 | public function getScore() | ||
| 289 | |||
| 290 | /** | ||
| 291 | * Scope for currently active job offers | ||
| 292 | * | ||
| 293 | * @param \Illuminate\Database\Query\Builder $query | ||
| 294 | * @return \Illuminate\Database\Query\Builder | ||
| 295 | */ | ||
| 296 | public function scopePriorDeadline($query) | ||
| 300 | |||
| 301 | /** | ||
| 302 | * @return \Illuminate\Database\Eloquent\Relations\HasMany | ||
| 303 | */ | ||
| 304 | public function locations() | ||
| 308 | |||
| 309 | /** | ||
| 310 | * @return \Illuminate\Database\Eloquent\Relations\MorphOne | ||
| 311 | */ | ||
| 312 | public function page() | ||
| 316 | |||
| 317 | /** | ||
| 318 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo | ||
| 319 | */ | ||
| 320 | public function firm() | ||
| 324 | |||
| 325 | /** | ||
| 326 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo | ||
| 327 | */ | ||
| 328 | public function currency() | ||
| 332 | |||
| 333 | /** | ||
| 334 | * @return \Illuminate\Database\Eloquent\Relations\HasMany | ||
| 335 | */ | ||
| 336 | public function referers() | ||
| 340 | |||
| 341 | /** | ||
| 342 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany | ||
| 343 | */ | ||
| 344 | public function tags() | ||
| 348 | |||
| 349 | /** | ||
| 350 | * @return \Illuminate\Database\Eloquent\Relations\HasMany | ||
| 351 | */ | ||
| 352 | public function subscribers() | ||
| 356 | |||
| 357 | /** | ||
| 358 | * @return \Illuminate\Database\Eloquent\Relations\HasMany | ||
| 359 | */ | ||
| 360 | public function applications() | ||
| 364 | |||
| 365 | /** | ||
| 366 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo | ||
| 367 | */ | ||
| 368 | public function user() | ||
| 372 | |||
| 373 | /** | ||
| 374 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo | ||
| 375 | */ | ||
| 376 | public function country() | ||
| 380 | |||
| 381 | /** | ||
| 382 | * @param string $title | ||
| 383 | */ | ||
| 384 | public function setTitleAttribute($title) | ||
| 391 | |||
| 392 | /** | ||
| 393 | * @param string $value | ||
| 394 | */ | ||
| 395 | public function setSalaryFromAttribute($value) | ||
| 399 | |||
| 400 | /** | ||
| 401 | * @param string $value | ||
| 402 | */ | ||
| 403 | public function setSalaryToAttribute($value) | ||
| 407 | |||
| 408 | /** | ||
| 409 | * @param int $value | ||
| 410 | */ | ||
| 411 | public function setDeadlineAttribute($value) | ||
| 415 | |||
| 416 | /** | ||
| 417 | * @return int | ||
| 418 | */ | ||
| 419 | public function getDeadlineAttribute() | ||
| 423 | |||
| 424 | /** | ||
| 425 | * @return mixed | ||
| 426 | */ | ||
| 427 | public function getCityAttribute() | ||
| 431 | |||
| 432 | /** | ||
| 433 | * @param int $userId | ||
| 434 | */ | ||
| 435 | public function setDefaultUserId($userId) | ||
| 441 | |||
| 442 | /** | ||
| 443 | * @param string $url | ||
| 444 | */ | ||
| 445 | public function addReferer($url) | ||
| 457 | |||
| 458 | /** | ||
| 459 | * Check if user has applied for this job offer. | ||
| 460 | * | ||
| 461 | * @param int|null $userId | ||
| 462 | * @param string $sessionId | ||
| 463 | * @return boolean | ||
| 464 | */ | ||
| 465 | public function hasApplied($userId, $sessionId) | ||
| 473 | |||
| 474 | /** | ||
| 475 | * @return array | ||
| 476 | */ | ||
| 477 | protected function getIndexBody() | ||
| 525 | |||
| 526 | /** | ||
| 527 | * @param float|null $salary | ||
| 528 | * @return float|null | ||
| 529 | */ | ||
| 530 | private function monthlySalary($salary) | ||
| 547 | } | ||
| 548 | 
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.