Completed
Push — master ( 4ee78f...ce1053 )
by Damian
14:52 queued 39s
created

DateField   D

Complexity

Total Complexity 86

Size/Duplication

Total Lines 416
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
dl 0
loc 416
rs 4.5142
c 0
b 0
f 0
wmc 86
lcom 1
cbo 11

16 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 22 6
B FieldHolder() 0 23 5
A SmallFieldHolder() 0 7 1
C Field() 0 62 13
A Type() 0 3 1
C setValue() 0 52 14
A dataValue() 0 7 2
A performReadonlyTransformation() 0 7 1
A castedCopy() 0 13 3
B validateArrayValue() 0 16 10
C validate() 0 67 15
A getLocale() 0 3 1
A setLocale() 0 4 1
C setConfig() 0 23 9
A getConfig() 0 7 3
A getSchemaValidation() 0 5 1

How to fix   Complexity   

Complex Class

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

1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 68 and the first side effect is on line 12.

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.

Loading history...
2
3
namespace SilverStripe\Forms;
4
5
use SilverStripe\Core\Convert;
6
use SilverStripe\Core\Injector\Injector;
7
use SilverStripe\i18n\i18n;
8
use InvalidArgumentException;
9
use Zend_Locale;
10
use Zend_Date;
11
12
require_once 'Zend/Date.php';
13
14
/**
15
 * Form field to display an editable date string,
16
 * either in a single `<input type="text">` field,
17
 * or in three separate fields for day, month and year.
18
 *
19
 * # Configuration
20
 *
21
 * - 'showcalendar' (boolean): Determines if a calendar picker is shown.
22
 *    By default, jQuery UI datepicker is used (see {@link DateField_View_JQuery}).
23
 * - 'jslocale' (string): Overwrites the "Locale" value set in this class.
24
 *    Only useful in combination with {@link DateField_View_JQuery}.
25
 * - 'dmyfields' (boolean): Show three input fields for day, month and year separately.
26
 *    CAUTION: Might not be useable in combination with 'showcalendar', depending on the used javascript library
27
 * - 'dmyseparator' (string): HTML markup to separate day, month and year fields.
28
 *    Only applicable with 'dmyfields'=TRUE. Use 'dateformat' to influence date representation with 'dmyfields'=FALSE.
29
 * - 'dmyplaceholders': Show HTML5 placehoder text to allow identification of the three separate input fields
30
 * - 'dateformat' (string): Date format compatible with Zend_Date.
31
 *    Usually set to default format for {@link locale} through {@link Zend_Locale_Format::getDateFormat()}.
32
 * - 'datavalueformat' (string): Internal ISO format string used by {@link dataValue()} to save the
33
 *    date to a database.
34
 * - 'min' (string): Minimum allowed date value (in ISO format, or strtotime() compatible).
35
 *    Example: '2010-03-31', or '-7 days'
36
 * - 'max' (string): Maximum allowed date value (in ISO format, or strtotime() compatible).
37
 *    Example: '2010-03-31', or '1 year'
38
 *
39
 * Depending which UI helper is used, further namespaced configuration options are available.
40
 * For the default jQuery UI, all options prefixed/namespaced with "jQueryUI." will be respected as well.
41
 * Example: <code>$myDateField->setConfig('jQueryUI.showWeek', true);</code>
42
 * See http://docs.jquery.com/UI/Datepicker for details.
43
 *
44
 * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
45
 * since the required frontend dependencies are included through CMS bundling.
46
 *
47
 * # Localization
48
 *
49
 * The field will get its default locale from {@link i18n::get_locale()}, and set the `dateformat`
50
 * configuration accordingly. Changing the locale through {@link setLocale()} will not update the
51
 * `dateformat` configuration automatically.
52
 *
53
 * See http://doc.silverstripe.org/framework/en/topics/i18n for more information about localizing form fields.
54
 *
55
 * # Usage
56
 *
57
 * ## Example: German dates with separate fields for day, month, year
58
 *
59
 *   $f = new DateField('MyDate');
60
 *   $f->setLocale('de_DE');
61
 *   $f->setConfig('dmyfields', true);
62
 *
63
 * # Validation
64
 *
65
 * Caution: JavaScript validation is only supported for the 'en_NZ' locale at the moment,
66
 * it will be disabled automatically for all other locales.
67
 */
68
class DateField extends TextField {
69
70
	protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_DATE;
71
72
	/**
73
	 * @config
74
	 * @var array
75
	 */
76
	private static $default_config = array(
77
		'showcalendar' => false,
78
		'jslocale' => null,
79
		'dmyfields' => false,
80
		'dmyseparator' => '&nbsp;<span class="separator">/</span>&nbsp;',
81
		'dmyplaceholders' => true,
82
		'dateformat' => null,
83
		'datavalueformat' => 'yyyy-MM-dd',
84
		'min' => null,
85
		'max' => null,
86
	);
87
88
	/**
89
	 * @var array
90
	 */
91
	protected $config;
92
93
	/**
94
	 * @var String
95
	 */
96
	protected $locale = null;
97
98
	/**
99
	 * @var Zend_Date Just set if the date is valid.
100
	 * {@link $value} will always be set to aid validation,
101
	 * and might contain invalid values.
102
	 */
103
	protected $valueObj = null;
104
105
	public function __construct($name, $title = null, $value = null) {
106
		if(!$this->locale) {
107
			$this->locale = i18n::get_locale();
108
		}
109
110
		$this->config = $this->config()->default_config;
0 ignored issues
show
Documentation introduced by
The property default_config does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
111
		if(!$this->getConfig('dateformat')) {
112
			$this->setConfig('dateformat', i18n::config()->get('date_format'));
113
		}
114
115
		foreach ($this->config()->default_config AS $defaultK => $defaultV) {
116
			if ($defaultV) {
117
				if ($defaultK=='locale') {
118
					$this->locale = $defaultV;
119
				} else {
120
					$this->setConfig($defaultK, $defaultV);
121
			}
122
		}
123
		}
124
125
		parent::__construct($name, $title, $value);
126
	}
127
128
	public function FieldHolder($properties = array()) {
129
		if ($this->getConfig('showcalendar')) {
130
			// TODO Replace with properly extensible view helper system
131
			$d = DateField_View_JQuery::create($this);
132
			if(!$d->regionalSettingsExist()) {
133
				$dateformat = $this->getConfig('dateformat');
134
135
				// if no localefile is present, the jQuery DatePicker
136
				// month- and daynames will default to English, so the date
137
				// will not pass Zend validatiobn. We provide a fallback
138
				if (preg_match('/(MMM+)|(EEE+)/', $dateformat)) {
139
					$this->setConfig('dateformat', $this->getConfig('datavalueformat'));
140
				}
141
			}
142
			$d->onBeforeRender();
143
		}
144
		$html = parent::FieldHolder();
145
146
		if(!empty($d)) {
147
			$html = $d->onAfterRender($html);
148
		}
149
		return $html;
150
	}
151
152
	function SmallFieldHolder($properties = array()){
153
		$d = DateField_View_JQuery::create($this);
154
		$d->onBeforeRender();
155
		$html = parent::SmallFieldHolder($properties);
156
		$html = $d->onAfterRender($html);
157
		return $html;
158
	}
159
160
	public function Field($properties = array()) {
161
		$config = array(
162
			'showcalendar' => $this->getConfig('showcalendar'),
163
			'isoDateformat' => $this->getConfig('dateformat'),
164
			'jquerydateformat' => DateField_View_JQuery::convert_iso_to_jquery_format($this->getConfig('dateformat')),
165
			'min' => $this->getConfig('min'),
166
			'max' => $this->getConfig('max')
167
		);
168
169
		// Add other jQuery UI specific, namespaced options (only serializable, no callbacks etc.)
170
		// TODO Move to DateField_View_jQuery once we have a properly extensible HTML5 attribute system for FormField
171
		$jqueryUIConfig = array();
172
		foreach($this->getConfig() as $k => $v) {
173
			if(preg_match('/^jQueryUI\.(.*)/', $k, $matches)) $jqueryUIConfig[$matches[1]] = $v;
174
		}
175
		if ($jqueryUIConfig)
0 ignored issues
show
Bug Best Practice introduced by
The expression $jqueryUIConfig 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...
176
			$config['jqueryuiconfig'] =  Convert::array2json(array_filter($jqueryUIConfig));
177
		$config = array_filter($config);
178
		foreach($config as $k => $v) $this->setAttribute('data-' . $k, $v);
179
180
		// Three separate fields for day, month and year
181
		if($this->getConfig('dmyfields')) {
182
			// values
183
			$valArr = ($this->valueObj) ? $this->valueObj->toArray() : null;
184
185
			// fields
186
			$fieldNames = Zend_Locale::getTranslationList('Field', $this->locale);
187
			$fieldDay = NumericField::create($this->name . '[day]', false, ($valArr) ? $valArr['day'] : null)
188
				->addExtraClass('day')
189
				->setAttribute('placeholder', $this->getConfig('dmyplaceholders') ? $fieldNames['day'] : null)
190
				->setMaxLength(2);
191
192
			$fieldMonth = NumericField::create($this->name . '[month]', false, ($valArr) ? $valArr['month'] : null)
193
				->addExtraClass('month')
194
				->setAttribute('placeholder', $this->getConfig('dmyplaceholders') ? $fieldNames['month'] : null)
195
				->setMaxLength(2);
196
197
			$fieldYear = NumericField::create($this->name . '[year]', false, ($valArr) ? $valArr['year'] : null)
198
				->addExtraClass('year')
199
				->setAttribute('placeholder', $this->getConfig('dmyplaceholders') ? $fieldNames['year'] : null)
200
				->setMaxLength(4);
201
202
			// order fields depending on format
203
			$sep = $this->getConfig('dmyseparator');
204
			$format = $this->getConfig('dateformat');
205
			$fields = array();
206
			$fields[stripos($format, 'd')] = $fieldDay->Field();
207
			$fields[stripos($format, 'm')] = $fieldMonth->Field();
208
			$fields[stripos($format, 'y')] = $fieldYear->Field();
209
			ksort($fields);
210
			$html = implode($sep, $fields);
211
212
			// dmyfields doesn't work with showcalendar
213
			$this->setConfig('showcalendar',false);
214
		}
215
		// Default text input field
216
		else {
217
			$html = parent::Field();
218
		}
219
220
		return $html;
221
	}
222
223
	public function Type() {
224
		return 'date text';
225
	}
226
227
	/**
228
	 * Sets the internal value to ISO date format.
229
	 *
230
	 * @param mixed $val
231
	 * @return $this
232
	 */
233
	public function setValue($val) {
234
		$locale = new Zend_Locale($this->locale);
235
236
		if(empty($val)) {
237
			$this->value = null;
238
			$this->valueObj = null;
239
		} else {
240
			if($this->getConfig('dmyfields')) {
241
				// Setting in correct locale
242
				if(is_array($val) && $this->validateArrayValue($val)) {
243
					// set() gets confused with custom date formats when using array notation
244
					if(!(empty($val['day']) || empty($val['month']) || empty($val['year']))) {
245
						$this->valueObj = new Zend_Date($val, null, $locale);
246
						$this->value = $this->valueObj->toArray();
247
					} else {
248
						$this->value = $val;
249
						$this->valueObj = null;
250
					}
251
				}
252
				// load ISO date from database (usually through Form->loadDataForm())
253
				else if(!empty($val) && Zend_Date::isDate($val, $this->getConfig('datavalueformat'), $locale)) {
254
					$this->valueObj = new Zend_Date($val, $this->getConfig('datavalueformat'), $locale);
255
					$this->value = $this->valueObj->toArray();
256
				}
257
				else {
258
					$this->value = $val;
259
					$this->valueObj = null;
260
				}
261
			} else {
262
				// Setting in correct locale.
263
				// Caution: Its important to have this check *before* the ISO date fallback,
264
				// as some dates are falsely detected as ISO by isDate(), e.g. '03/04/03'
265
				// (en_NZ for 3rd of April, definetly not yyyy-MM-dd)
266
				if(!empty($val) && Zend_Date::isDate($val, $this->getConfig('dateformat'), $locale)) {
267
					$this->valueObj = new Zend_Date($val, $this->getConfig('dateformat'), $locale);
268
					$this->value = $this->valueObj->get($this->getConfig('dateformat'), $locale);
269
270
				}
271
				// load ISO date from database (usually through Form->loadDataForm())
272
				else if(!empty($val) && Zend_Date::isDate($val, $this->getConfig('datavalueformat'))) {
273
					$this->valueObj = new Zend_Date($val, $this->getConfig('datavalueformat'));
274
					$this->value = $this->valueObj->get($this->getConfig('dateformat'), $locale);
275
				}
276
				else {
277
					$this->value = $val;
278
					$this->valueObj = null;
279
				}
280
			}
281
		}
282
283
		return $this;
284
	}
285
286
	/**
287
	 * @return String ISO 8601 date, suitable for insertion into database
288
	 */
289
	public function dataValue() {
290
		if($this->valueObj) {
291
			return $this->valueObj->toString($this->getConfig('datavalueformat'));
292
		} else {
293
			return null;
294
		}
295
	}
296
297
	public function performReadonlyTransformation() {
298
		$field = $this->castedCopy('SilverStripe\\Forms\\DateField_Disabled');
299
		$field->setValue($this->dataValue());
300
		$field->readonly = true;
301
302
		return $field;
303
	}
304
305
	/**
306
	 * @param mixed $class
307
	 * @return FormField
308
	 */
309
	public function castedCopy($class) {
310
		/** @var FormField $copy */
311
		$copy = Injector::inst()->create($class, $this->name);
312
		if($copy->hasMethod('setConfig')) {
313
			/** @var DateField $copy */
314
			$config = $this->getConfig();
315
			foreach($config as $k => $v) {
316
				$copy->setConfig($k, $v);
317
			}
318
		}
319
320
		return parent::castedCopy($copy);
321
	}
322
323
	/**
324
	 * Validate an array with expected keys 'day', 'month' and 'year.
325
	 * Used because Zend_Date::isDate() doesn't provide this.
326
	 *
327
	 * @param array $val
328
	 * @return bool
329
	 */
330
	public function validateArrayValue($val) {
331
		if(!is_array($val)) {
332
			return false;
333
		}
334
335
		// Validate against Zend_Date,
336
		// but check for empty array keys (they're included in standard form submissions)
337
		return (
338
			array_key_exists('year', $val)
339
			&& (!$val['year'] || Zend_Date::isDate($val['year'], 'yyyy', $this->locale))
340
			&& array_key_exists('month', $val)
341
			&& (!$val['month'] || Zend_Date::isDate($val['month'], 'MM', $this->locale))
342
			&& array_key_exists('day', $val)
343
			&& (!$val['day'] || Zend_Date::isDate($val['day'], 'dd', $this->locale))
344
		);
345
	}
346
347
	/**
348
	 * @param Validator $validator
349
	 * @return bool
350
	 */
351
	public function validate($validator) {
352
		// Don't validate empty fields
353
		if(empty($this->value)) {
354
			return true;
355
		}
356
357
		// date format
358
		if($this->getConfig('dmyfields')) {
359
			$valid = (!$this->value || $this->validateArrayValue($this->value));
360
		} else {
361
			$valid = (Zend_Date::isDate($this->value, $this->getConfig('dateformat'), $this->locale));
362
		}
363
		if(!$valid) {
364
			$validator->validationError(
365
				$this->name,
366
				_t(
367
					'DateField.VALIDDATEFORMAT2', "Please enter a valid date format ({format})",
368
					array('format' => $this->getConfig('dateformat'))
369
				),
370
				"validation"
371
			);
372
			return false;
373
		}
374
375
		// min/max - Assumes that the date value was valid in the first place
376
		if($min = $this->getConfig('min')) {
377
			// ISO or strtotime()
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
378
			if(Zend_Date::isDate($min, $this->getConfig('datavalueformat'))) {
379
				$minDate = new Zend_Date($min, $this->getConfig('datavalueformat'));
380
			} else {
381
				$minDate = new Zend_Date(strftime('%Y-%m-%d', strtotime($min)), $this->getConfig('datavalueformat'));
382
			}
383
			if(!$this->valueObj || (!$this->valueObj->isLater($minDate) && !$this->valueObj->equals($minDate))) {
384
				$validator->validationError(
385
					$this->name,
386
					_t(
387
						'DateField.VALIDDATEMINDATE',
388
						"Your date has to be newer or matching the minimum allowed date ({date})",
389
						array('date' => $minDate->toString($this->getConfig('dateformat')))
390
					),
391
					"validation"
392
				);
393
				return false;
394
			}
395
		}
396
		if($max = $this->getConfig('max')) {
397
			// ISO or strtotime()
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
398
			if(Zend_Date::isDate($min, $this->getConfig('datavalueformat'))) {
399
				$maxDate = new Zend_Date($max, $this->getConfig('datavalueformat'));
400
			} else {
401
				$maxDate = new Zend_Date(strftime('%Y-%m-%d', strtotime($max)), $this->getConfig('datavalueformat'));
402
			}
403
			if(!$this->valueObj || (!$this->valueObj->isEarlier($maxDate) && !$this->valueObj->equals($maxDate))) {
404
				$validator->validationError(
405
					$this->name,
406
					_t('DateField.VALIDDATEMAXDATE',
407
						"Your date has to be older or matching the maximum allowed date ({date})",
408
						array('date' => $maxDate->toString($this->getConfig('dateformat')))
409
					),
410
					"validation"
411
				);
412
				return false;
413
			}
414
		}
415
416
		return true;
417
	}
418
419
	/**
420
	 * @return string
421
	 */
422
	public function getLocale() {
423
		return $this->locale;
424
	}
425
426
	/**
427
	 * Caution: Will not update the 'dateformat' config value.
428
	 *
429
	 * @param string $locale
430
	 * @return $this
431
	 */
432
	public function setLocale($locale) {
433
		$this->locale = $locale;
434
		return $this;
435
	}
436
437
	/**
438
	 * @param string $name
439
	 * @param mixed $val
440
	 * @return $this
441
	 */
442
	public function setConfig($name, $val) {
443
		switch($name) {
444
			case 'min':
445
				$format = $this->getConfig('datavalueformat');
446
				if($val && !Zend_Date::isDate($val, $format) && !strtotime($val)) {
447
					throw new InvalidArgumentException(
448
						sprintf('Date "%s" is not a valid minimum date format (%s) or strtotime() argument',
449
						$val, $format));
450
				}
451
				break;
452
			case 'max':
453
				$format = $this->getConfig('datavalueformat');
454
				if($val && !Zend_Date::isDate($val, $format) && !strtotime($val)) {
455
					throw new InvalidArgumentException(
456
						sprintf('Date "%s" is not a valid maximum date format (%s) or strtotime() argument',
457
						$val, $format));
458
				}
459
				break;
460
		}
461
462
		$this->config[$name] = $val;
463
		return $this;
464
	}
465
466
	/**
467
	 * @param String $name Optional, returns the whole configuration array if empty
468
	 * @return mixed|array
469
	 */
470
	public function getConfig($name = null) {
471
		if($name) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $name of type string|null is loosely compared to true; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
472
			return isset($this->config[$name]) ? $this->config[$name] : null;
473
		} else {
474
			return $this->config;
475
		}
476
	}
477
478
	public function getSchemaValidation() {
479
		$rules = parent::getSchemaValidation();
480
		$rules['date'] = true;
481
		return $rules;
482
	}
483
}
484