Complex classes like EmailReminder_NotificationSchedule 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 EmailReminder_NotificationSchedule, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
3 | class EmailReminder_NotificationSchedule extends DataObject |
||
4 | { |
||
5 | |||
6 | /** |
||
7 | * @var int |
||
8 | */ |
||
9 | private static $grace_days = 3; |
||
10 | |||
11 | /** |
||
12 | * @var string |
||
13 | */ |
||
14 | private static $default_data_object = 'Member'; |
||
15 | |||
16 | /** |
||
17 | * @var string |
||
18 | */ |
||
19 | private static $default_date_field = ''; |
||
20 | |||
21 | /** |
||
22 | * @var string |
||
23 | */ |
||
24 | private static $default_email_field = ''; |
||
25 | |||
26 | /** |
||
27 | * @var string |
||
28 | */ |
||
29 | private static $replaceable_record_fields = array('FirstName', 'Surname', 'Email'); |
||
30 | |||
31 | /** |
||
32 | * @var string |
||
33 | */ |
||
34 | private static $include_method = 'EmailReminderInclude'; |
||
35 | |||
36 | /** |
||
37 | * @var string |
||
38 | */ |
||
39 | private static $exclude_method = 'EmailReminderExclude'; |
||
40 | |||
41 | /** |
||
42 | * @var string |
||
43 | */ |
||
44 | private static $mail_out_class = 'EmailReminder_DailyMailOut'; |
||
45 | |||
46 | private static $singular_name = 'Email Reminder Schedule'; |
||
47 | public function i18n_singular_name() |
||
48 | { |
||
49 | return self::$singular_name; |
||
50 | } |
||
51 | |||
52 | private static $plural_name = 'Email Reminder Schedules'; |
||
53 | public function i18n_plural_name() |
||
54 | { |
||
55 | return self::$plural_name; |
||
56 | } |
||
57 | |||
58 | private static $db = array( |
||
59 | 'DataObject' => 'Varchar(100)', |
||
60 | 'EmailField' => 'Varchar(100)', |
||
61 | 'DateField' => 'Varchar(100)', |
||
62 | 'Days' => 'Int', |
||
63 | 'RepeatDays' => 'Int', |
||
64 | 'BeforeAfter' => "Enum('before,after,immediately','before')", |
||
65 | 'EmailFrom' => 'Varchar(100)', |
||
66 | 'EmailSubject' => 'Varchar(100)', |
||
67 | 'Content' => 'HTMLText', |
||
68 | 'Disable' => 'Boolean', |
||
69 | 'SendTestTo' => 'Text' |
||
70 | ); |
||
71 | |||
72 | |||
73 | private static $has_many = array( |
||
74 | 'EmailsSent' => 'EmailReminder_EmailRecord' |
||
75 | ); |
||
76 | |||
77 | private static $summary_fields = array( |
||
78 | 'EmailSubject', |
||
79 | 'Days' |
||
80 | ); |
||
81 | |||
82 | private static $field_labels = array( |
||
83 | 'DataObject' => 'Class/Table', |
||
84 | 'Days' => 'Days from Expiry', |
||
85 | 'RepeatDays' => 'Repeat cycle days' |
||
86 | ); |
||
87 | |||
88 | public function populateDefaults() |
||
89 | { |
||
90 | parent::populateDefaults(); |
||
91 | $this->DataObject = $this->Config()->get('default_data_object'); |
||
92 | $this->EmailField = $this->Config()->get('default_email_field'); |
||
93 | $this->DateField = $this->Config()->get('default_date_field'); |
||
94 | $this->Days = 7; |
||
95 | $this->RepeatDays = 300; |
||
96 | $this->BeforeAfter = 'before'; |
||
97 | $this->EmailFrom = Config::inst()->get('Email', 'admin_email'); |
||
98 | $this->EmailSubject = 'Your memberships expires in [days] days'; |
||
99 | } |
||
100 | |||
101 | public function getCMSFields() |
||
102 | { |
||
103 | $fields = parent::getCMSFields(); |
||
104 | |||
105 | $emailsSentField = $fields->dataFieldByName('EmailsSent'); |
||
106 | $fields->removeFieldFromTab('Root', 'EmailsSent'); |
||
107 | |||
108 | $fields->addFieldToTab( |
||
109 | 'Root.Main', |
||
110 | CheckboxField::create('Disable') |
||
111 | ); |
||
112 | $fields->addFieldToTab( |
||
113 | 'Root.Main', |
||
114 | $dataObjecField = DropdownField::create( |
||
115 | 'DataObject', |
||
116 | 'Table/Class Name', |
||
117 | $this->dataObjectOptions() |
||
118 | ) |
||
119 | ->setRightTitle('Type a valid table/class name') |
||
120 | ); |
||
121 | if ($this->Config()->get('default_data_object')) { |
||
122 | $fields->replaceField('DataObject', $dataObjecField->performReadonlyTransformation()); |
||
123 | } |
||
124 | |||
125 | |||
126 | |||
127 | $fields->addFieldToTab( |
||
128 | 'Root.Main', |
||
129 | $emailFieldField = DropdownField::create( |
||
130 | 'EmailField', |
||
131 | 'Email Field', |
||
132 | $this->emailFieldOptions() |
||
133 | ) |
||
134 | ->setRightTitle('Select the field that will contain a valid email address') |
||
135 | ->setEmptyString('[ Please select ]') |
||
136 | ); |
||
137 | if ($this->Config()->get('default_email_field')) { |
||
138 | $fields->replaceField('EmailField', $emailFieldField->performReadonlyTransformation()); |
||
139 | } |
||
140 | |||
141 | $fields->addFieldToTab( |
||
142 | 'Root.Main', |
||
143 | $dateFieldField = DropdownField::create( |
||
144 | 'DateField', |
||
145 | 'Date Field', |
||
146 | $this->dateFieldOptions() |
||
147 | ) |
||
148 | ->setRightTitle('Select a valid Date field to calculate when reminders should be sent') |
||
149 | ->setEmptyString('[ Please select ]') |
||
150 | ); |
||
151 | if ($this->Config()->get('default_date_field')) { |
||
152 | $fields->replaceField('DateField', $dateFieldField->performReadonlyTransformation()); |
||
153 | } |
||
154 | |||
155 | $fields->removeFieldsFromTab( |
||
156 | 'Root.Main', |
||
157 | array('Days', 'BeforeAfter', 'RepeatDays') |
||
158 | ); |
||
159 | $fields->addFieldsToTab( |
||
160 | 'Root.Main', |
||
161 | array( |
||
162 | DropdownField::create('BeforeAfter', 'Before / After Expiration', array('before' => 'before', 'after' => 'after', 'immediately' => 'immediately')) |
||
163 | ->setRightTitle('Are the days listed above before or after the actual expiration date.'), |
||
164 | NumericField::create('Days', 'Days') |
||
165 | ->setRightTitle('How many days in advance (before) or in arrears (after) of the expiration date should this email be sent? </br>This field is ignored if set to send immediately.'), |
||
166 | NumericField::create('RepeatDays', 'Repeat Cycle Days') |
||
167 | ->setRightTitle( |
||
168 | ' |
||
169 | Number of days after which the same reminder can be sent to the same email address. |
||
170 | <br />We allow an e-mail to be sent to one specific email address for one specific reminder only once. |
||
171 | <br />In this field you can indicate for how long we will apply this rule.' |
||
172 | ) |
||
173 | ) |
||
174 | ); |
||
175 | $fields->addFieldsToTab( |
||
176 | 'Root.EmailContent', |
||
177 | array( |
||
178 | TextField::create('EmailFrom', 'Email From Address') |
||
179 | ->setRightTitle('The email from address, eg: "My Company <[email protected]>"'), |
||
180 | $subjectField = TextField::create('EmailSubject', 'Email Subject Line') |
||
181 | ->setRightTitle('The subject of the email'), |
||
182 | $contentField = HTMLEditorField::create('Content', 'Email Content') |
||
183 | ->SetRows(20) |
||
184 | ) |
||
185 | ); |
||
186 | if ($obj = $this->getReplacerObject()) { |
||
187 | $html = $obj->replaceHelpList($asHTML = true); |
||
188 | $otherFieldsThatCanBeUsed = $this->getFieldsFromDataObject(array('*')); |
||
189 | $replaceableFields = $this->Config()->get('replaceable_record_fields'); |
||
190 | if (count($otherFieldsThatCanBeUsed)) { |
||
191 | $html .= '<h3>You can also use the record fields (not replaced in tests):</h3><ul>'; |
||
192 | foreach ($otherFieldsThatCanBeUsed as $key => $value) { |
||
193 | if (in_array($key, $replaceableFields)) { |
||
194 | $html .= '<li><strong>$'.$key.'</strong> <span>'.$value.'</span></li>'; |
||
195 | } |
||
196 | } |
||
197 | } |
||
198 | $html .= '</ul>'; |
||
199 | $subjectField->setRightTitle('for replacement options, please see below ...'); |
||
200 | $contentField->setRightTitle($html); |
||
201 | } |
||
202 | $fields->addFieldsToTab( |
||
203 | 'Root.Sent', |
||
204 | array( |
||
205 | TextareaField::create('SendTestTo', 'Send test email to ...') |
||
206 | ->setRightTitle( |
||
207 | ' |
||
208 | Separate emails by commas, a test email will be sent every time you save this Email Reminder, if you do not want test emails to be sent make sure this field is empty |
||
209 | ' |
||
210 | ) |
||
211 | ->SetRows(3) |
||
212 | ) |
||
213 | ); |
||
214 | if ($emailsSentField) { |
||
215 | $config = $emailsSentField->getConfig(); |
||
216 | $config->removeComponentsByType('GridFieldAddExistingAutocompleter'); |
||
217 | $fields->addFieldToTab( |
||
218 | 'Root.Sent', |
||
219 | $emailsSentField |
||
220 | ); |
||
221 | } |
||
222 | $records = $this->CurrentRecords(); |
||
223 | if ($records) { |
||
224 | $fields->addFieldsToTab( |
||
225 | 'Root.Review', |
||
226 | array( |
||
227 | GridField::create( |
||
228 | 'CurrentRecords', |
||
229 | 'Today we are sending to ...', |
||
230 | $records |
||
231 | ), |
||
232 | LiteralField::create( |
||
233 | 'SampleSelectStatement', |
||
234 | '<h3>Here is a sample statement used to select records:</h3> |
||
235 | <pre>'.$this->whereStatementForDays().'</pre>' |
||
236 | ), |
||
237 | LiteralField::create( |
||
238 | 'SampleFieldDataForRecords', |
||
239 | '<h3>sample of '.$this->DateField.' field values:</h3> |
||
240 | <li>'.implode('</li><li>', $this->SampleFieldDataForRecords()).'</li>' |
||
241 | ) |
||
242 | ) |
||
243 | ); |
||
244 | } |
||
245 | return $fields; |
||
246 | } |
||
247 | |||
248 | /** |
||
249 | * @return array |
||
250 | */ |
||
251 | protected function dataObjectOptions() |
||
252 | { |
||
253 | return ClassInfo::subclassesFor("DataObject"); |
||
254 | } |
||
255 | |||
256 | /** |
||
257 | * @return array |
||
258 | */ |
||
259 | protected function emailFieldOptions() |
||
260 | { |
||
261 | return $this->getFieldsFromDataObject(array('Varchar', 'Email')); |
||
262 | } |
||
263 | |||
264 | /** |
||
265 | * @return array |
||
266 | */ |
||
267 | protected function dateFieldOptions() |
||
268 | { |
||
269 | return $this->getFieldsFromDataObject(array('Date')); |
||
270 | } |
||
271 | |||
272 | |||
273 | /** |
||
274 | * list of database fields available |
||
275 | * @param array $fieldTypeMatchArray - strpos filter |
||
276 | * @return array |
||
277 | */ |
||
278 | protected function getFieldsFromDataObject($fieldTypeMatchArray = array()) |
||
279 | { |
||
280 | $array = array(); |
||
281 | if ($this->hasValidDataObject()) { |
||
282 | $object = Injector::inst()->get($this->DataObject); |
||
283 | if ($object) { |
||
284 | $allOptions = $object->stat('db'); |
||
285 | $fieldLabels = $object->fieldLabels(); |
||
286 | foreach ($allOptions as $fieldName => $fieldType) { |
||
287 | foreach ($fieldTypeMatchArray as $matchString) { |
||
288 | if ((strpos($fieldType, $matchString) !== false) || $matchString == '*') { |
||
289 | if (isset($fieldLabels[$fieldName])) { |
||
290 | $label = $fieldLabels[$fieldName]; |
||
291 | } else { |
||
292 | $label = $fieldName; |
||
293 | } |
||
294 | $array[$fieldName] = $label; |
||
295 | } |
||
296 | } |
||
297 | } |
||
298 | } |
||
299 | } |
||
300 | return $array; |
||
301 | } |
||
302 | |||
303 | |||
304 | /** |
||
305 | * Test if valid classname has been set |
||
306 | * @param null |
||
307 | * @return Boolean |
||
308 | */ |
||
309 | public function hasValidDataObject() |
||
310 | { |
||
311 | return (! $this->DataObject) || ClassInfo::exists($this->DataObject) ? true : false; |
||
312 | } |
||
313 | |||
314 | /** |
||
315 | * Test if valid fields have been set |
||
316 | * @param null |
||
317 | * @return Boolean |
||
318 | */ |
||
319 | public function hasValidDataObjectFields() |
||
320 | { |
||
321 | if (!$this->hasValidDataObject()) { |
||
322 | return false; |
||
323 | } |
||
324 | $emailFieldOptions = $this->emailFieldOptions(); |
||
325 | if (!isset($emailFieldOptions[$this->EmailField])) { |
||
326 | return false; |
||
327 | } |
||
328 | $dateFieldOptions = $this->dateFieldOptions(); |
||
329 | if (!isset($dateFieldOptions[$this->DateField])) { |
||
330 | return false; |
||
331 | } |
||
332 | return true; |
||
333 | } |
||
334 | |||
335 | /** |
||
336 | * @return string |
||
337 | */ |
||
338 | public function getTitle() |
||
339 | { |
||
340 | $niceTitle = '[' . $this->EmailSubject . '] // send '; |
||
341 | $niceTitle .= ($this->BeforeAfter === 'immediately') ? $this->BeforeAfter : $this->Days . ' days '.$this->BeforeAfter.' Expiration Date'; |
||
342 | return ($this->hasValidDataObjectFields()) ? $niceTitle : 'uncompleted'; |
||
343 | } |
||
344 | |||
345 | /** |
||
346 | * @return boolean |
||
347 | */ |
||
348 | public function hasValidFields() |
||
349 | { |
||
350 | if (!$this->hasValidDataObject()) { |
||
351 | return false; |
||
352 | } |
||
353 | if (!$this->hasValidDataObjectFields()) { |
||
354 | return false; |
||
355 | } |
||
356 | if ($this->EmailFrom && $this->EmailSubject && $this->Content) { |
||
357 | return true; |
||
358 | } |
||
359 | return false; |
||
360 | } |
||
361 | |||
362 | /** |
||
363 | * @return boolean |
||
364 | */ |
||
365 | public function validate() |
||
366 | { |
||
367 | $valid = parent::validate(); |
||
368 | if ($this->exists()) { |
||
369 | if (! $this->hasValidDataObject()) { |
||
370 | $valid->error('Please enter valid Tabe/Class name ("' . htmlspecialchars($this->DataObject) .'" does not exist)'); |
||
371 | } elseif (! $this->hasValidDataObjectFields()) { |
||
372 | $valid->error('Please select valid fields for both Email & Date'); |
||
373 | } elseif (! $this->hasValidFields()) { |
||
374 | $valid->error('Please fill in all fields. Make sure not to forget the email details (from who, subject, content)'); |
||
375 | } |
||
376 | } |
||
377 | return $valid; |
||
378 | } |
||
379 | |||
380 | public function onBeforeWrite() |
||
381 | { |
||
382 | parent::onBeforeWrite(); |
||
383 | if ($this->RepeatDays < ($this->Days * 3)) { |
||
384 | $this->RepeatDays = ($this->Days * 3); |
||
385 | } |
||
386 | } |
||
387 | |||
388 | public function onAfterWrite() |
||
389 | { |
||
390 | parent::onAfterWrite(); |
||
391 | if ($this->SendTestTo) { |
||
392 | if ($mailOutObject = $this->getMailOutObject()) { |
||
393 | $mailOutObject->setTestOnly(true); |
||
394 | $mailOutObject->setVerbose(true); |
||
395 | $mailOutObject->run(null); |
||
396 | } |
||
397 | } |
||
398 | } |
||
399 | |||
400 | /** |
||
401 | * |
||
402 | * @return null | EmailReminder_ReplacerClassInterface |
||
403 | */ |
||
404 | public function getReplacerObject() |
||
405 | { |
||
406 | if ($mailOutObject = $this->getMailOutObject()) { |
||
407 | return $mailOutObject->getReplacerObject(); |
||
408 | } |
||
409 | } |
||
410 | |||
411 | /** |
||
412 | * |
||
413 | * @return null | ScheduledTask |
||
414 | */ |
||
415 | public function getMailOutObject() |
||
416 | { |
||
417 | $mailOutClass = $this->Config()->get('mail_out_class'); |
||
418 | if (class_exists($mailOutClass)) { |
||
419 | $obj = Injector::inst()->get($mailOutClass); |
||
420 | if ($obj instanceof BuildTask) { |
||
421 | return $obj; |
||
422 | } else { |
||
423 | user_error($mailOutClass.' needs to be an instance of a Scheduled Task'); |
||
424 | } |
||
425 | } |
||
426 | } |
||
427 | |||
428 | /** |
||
429 | * @param int $limit |
||
430 | * @return array |
||
431 | */ |
||
432 | public function SampleFieldDataForRecords($limit = 200) |
||
433 | { |
||
434 | if ($this->hasValidFields()) { |
||
435 | $className = $this->DataObject; |
||
436 | $objects = $className::get()->sort('RAND()') |
||
437 | ->where('"'.$this->DateField.'" IS NOT NULL AND "'.$this->DateField.'" <> \'\' AND "'.$this->DateField.'" <> 0') |
||
438 | ->limit($limit); |
||
439 | if ($objects->count()) { |
||
440 | return array_unique($objects->column($this->DateField)); |
||
441 | } else { |
||
442 | return array(); |
||
443 | } |
||
444 | } |
||
445 | } |
||
446 | |||
447 | |||
448 | /** |
||
449 | * @return DataList | null |
||
450 | */ |
||
451 | public function CurrentRecords() |
||
452 | { |
||
453 | if ($this->hasValidFields()) { |
||
454 | $className = $this->DataObject; |
||
455 | |||
456 | // Use StartsWith to match Date and DateTime fields |
||
457 | $records = $className::get()->where($this->whereStatementForDays()); |
||
458 | //sample record |
||
459 | $firstRecord = $records->first(); |
||
460 | if($firstRecord && $firstRecord->exists()) |
||
461 | //methods |
||
462 | $includeMethod = $this->Config()->get('include_method'); |
||
463 | $excludeMethod = $this->Config()->get('exclude_method'); |
||
464 | |||
465 | //included method? |
||
466 | $hasIncludeMethod = false; |
||
467 | if($record->hasMethod($includeMethod)) { |
||
468 | $includedRecords = [0 => 0]; |
||
469 | $hasIncludeMethod = true; |
||
470 | } |
||
471 | |||
472 | //excluded method? |
||
473 | $hasExcludeMethod = false; |
||
474 | if($record->hasMethod($excludeMethod)) { |
||
475 | $excludedRecords = [0 => 0]; |
||
476 | $hasExcludeMethod = true; |
||
477 | } |
||
478 | |||
479 | //see who is in and out |
||
480 | if($hasIncludeMethod || $hasExcludeMethod) { |
||
481 | foreach($records as $record) { |
||
482 | if($hasIncludeMethod) { |
||
483 | $in = $record->$includeMethod($this, $records); |
||
484 | if($in === true) { |
||
542 |
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.