Completed
Push — 3.7 ( 81b2d8...ef0909 )
by
unknown
09:42
created

DatetimeField   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 329
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
dl 0
loc 329
rs 6.96
c 0
b 0
f 0
wmc 53
lcom 1
cbo 8

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
A setForm() 0 9 1
A setName() 0 9 1
A FieldHolder() 0 10 1
A Field() 0 4 1
F setValue() 0 73 20
A Value() 0 7 2
A setDisabled() 0 7 2
A setReadonly() 0 7 2
A getDateField() 0 3 1
A setDateField() 0 14 2
A getTimeField() 0 3 1
A setTimeField() 0 14 2
A getTimezoneField() 0 3 1
A setLocale() 0 5 1
A getLocale() 0 3 1
A setConfig() 0 10 2
A getConfig() 0 7 3
A validate() 0 6 2
A performReadonlyTransformation() 0 20 5
A __clone() 0 4 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
/**
3
 * A composite field for date and time entry,
4
 * based on {@link DateField} and {@link TimeField}.
5
 * Usually saves into a single {@link SS_Datetime} database column.
6
 * If you want to save into {@link Date} or {@link Time} columns,
7
 * please instanciate the fields separately.
8
 *
9
 * # Configuration
10
 *
11
 * The {@link setConfig()} method is only used to configure common properties of this field.
12
 * To configure the {@link DateField} and {@link TimeField} instances contained within, use their own
13
 * {@link setConfig()} methods.
14
 *
15
 * Example:
16
 * <code>
17
 * $field = new DatetimeField('Name', 'Label');
18
 * $field->setConfig('datavalueformat', 'yyyy-MM-dd HH:mm'); // global setting
19
 * $field->getDateField()->setConfig('showcalendar', 1); // field-specific setting
20
 * </code>
21
 *
22
 * - "timezone": Set a different timezone for viewing. {@link dataValue()} will still save
23
 * the time in PHP's default timezone (date_default_timezone_get()), its only a view setting.
24
 * Note that the sub-fields ({@link getDateField()} and {@link getTimeField()})
25
 * are not timezone aware, and will have their values set in local time, rather than server time.
26
 * - "datetimeorder": An sprintf() template to determine in which order the date and time values will
27
 * be combined. This is necessary as those separate formats are set in their invididual fields.
28
 *
29
 * @package framework
30
 * @subpackage forms
31
 */
32
class DatetimeField extends FormField {
33
34
	/**
35
	 * @var DateField
36
	 */
37
	protected $dateField = null;
38
39
	/**
40
	 * @var TimeField
41
	 */
42
	protected $timeField = null;
43
44
	/**
45
	 * @config
46
	 * @var array
47
	 */
48
	private static $default_config = array(
49
		'datavalueformat' => 'yyyy-MM-dd HH:mm:ss',
50
		'usertimezone' => null,
51
		'datetimeorder' => '%s %s',
52
	);
53
54
	/**
55
	 * @var array
56
	 */
57
	protected $config;
58
59
	public function __construct($name, $title = null, $value = ""){
60
		$this->config = $this->config()->default_config;
0 ignored issues
show
Documentation introduced by
The property default_config does not exist on object<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...
61
62
		$this->dateField = DateField::create($name . '[date]', false)
63
			->addExtraClass('fieldgroup-field');
64
		$this->timeField = TimeField::create($name . '[time]', false)
65
			->addExtraClass('fieldgroup-field');
66
		$this->timezoneField = new HiddenField($name . '[timezone]');
0 ignored issues
show
Documentation introduced by
The property timezoneField does not exist on object<DatetimeField>. 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...
67
68
		parent::__construct($name, $title, $value);
69
	}
70
71
	public function setForm($form) {
72
		parent::setForm($form);
73
74
		$this->dateField->setForm($form);
75
		$this->timeField->setForm($form);
76
		$this->timezoneField->setForm($form);
77
78
		return $this;
79
	}
80
81
	public function setName($name) {
82
		parent::setName($name);
83
84
		$this->dateField->setName($name . '[date]');
85
		$this->timeField->setName($name . '[time]');
86
		$this->timezoneField->setName($name . '[timezone]');
87
88
		return $this;
89
	}
90
91
	/**
92
	 * @param array $properties
93
	 * @return HTMLText
94
	 */
95
	public function FieldHolder($properties = array()) {
96
		$config = array(
97
			'datetimeorder' => $this->getConfig('datetimeorder'),
98
		);
99
		$config = array_filter($config);
100
		$this->addExtraClass('fieldgroup');
101
		$this->addExtraClass(Convert::raw2json($config));
102
103
		return parent::FieldHolder($properties);
104
	}
105
106
	/**
107
	 * @param array $properties
108
	 * @return HTMLText
109
	 */
110
	public function Field($properties = array()) {
111
		Requirements::css(FRAMEWORK_DIR . '/css/DatetimeField.css');
112
		return parent::Field($properties);
113
	}
114
115
	/**
116
	 * Sets the internal value to ISO date format, based on either a database value in ISO date format,
117
	 * or a form submssion in the user date format. Uses the individual date and time fields
118
	 * to take care of the actual formatting and value conversion.
119
	 *
120
	 * Value setting happens *before* validation, so we have to set the value even if its not valid.
121
	 *
122
	 * Caution: Only converts user timezones when value is passed as array data (= form submission).
123
	 * Weak indication, but unfortunately the framework doesn't support a distinction between
124
	 * setting a value from the database, application logic, and user input.
125
	 *
126
	 * @param string|array $val String expects an ISO date format. Array notation with 'date' and 'time'
127
	 *  keys can contain localized strings. If the 'dmyfields' option is used for {@link DateField},
128
	 *  the 'date' value may contain array notation was well (see {@link DateField->setValue()}).
129
	 */
130
	public function setValue($val) {
131
		$locale = new Zend_Locale($this->locale);
132
133
		// If timezones are enabled, assume user data needs to be reverted to server timezone
134
		if($this->getConfig('usertimezone')) {
135
			// Accept user input on timezone, but only when timezone support is enabled
136
			$userTz = (is_array($val) && array_key_exists('timezone', $val)) ? $val['timezone'] : null;
137
			if(!$userTz) $userTz = $this->getConfig('usertimezone'); // fall back to defined timezone
138
		} else {
139
			$userTz = null;
140
		}
141
142
		if(empty($val)) {
143
			$this->value = null;
144
			$this->dateField->setValue(null);
145
			$this->timeField->setValue(null);
146
		} else {
147
			// Case 1: String setting from database, in ISO date format
148
			if(is_string($val) && Zend_Date::isDate($val, $this->getConfig('datavalueformat'), $locale)) {
149
				$this->value = $val;
150
			}
151
			// Case 2: Array form submission with user date format
152
			elseif(is_array($val) && array_key_exists('date', $val) && array_key_exists('time', $val)) {
153
154
				$dataTz = date_default_timezone_get();
155
				// If timezones are enabled, assume user data needs to be converted to server timezone
156
				if($userTz) date_default_timezone_set($userTz);
157
158
				// Uses sub-fields to temporarily write values and delegate dealing with their normalization,
159
				// actual sub-field value setting happens later
160
				$this->dateField->setValue($val['date']);
161
				$this->timeField->setValue($val['time']);
162
				if($this->dateField->dataValue() && $this->timeField->dataValue()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->dateField->dataValue() 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...
Bug Best Practice introduced by
The expression $this->timeField->dataValue() 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...
163
					$userValueObj = new Zend_Date(null, null, $locale);
164
					$userValueObj->setDate($this->dateField->dataValue(),
165
						$this->dateField->getConfig('datavalueformat'));
166
					$userValueObj->setTime($this->timeField->dataValue(),
167
						$this->timeField->getConfig('datavalueformat'));
168
					if($userTz) $userValueObj->setTimezone($dataTz);
169
					$this->value = $userValueObj->get($this->getConfig('datavalueformat'), $locale);
170
					unset($userValueObj);
171
				} else {
172
					// Validation happens later, so set the raw string in case Zend_Date doesn't accept it
173
					$this->value = trim(sprintf($this->getConfig('datetimeorder'), $val['date'], $val['time']));
174
				}
175
176
				if($userTz) date_default_timezone_set($dataTz);
177
			}
178
			// Case 3: Value is invalid, but set it anyway to allow validation by the fields later on
179
			else {
180
				$this->dateField->setValue($val);
181
				if(is_string($val) )$this->timeField->setValue($val);
182
				$this->value = $val;
183
			}
184
185
			// view settings (dates might differ from $this->value based on user timezone settings)
186
			if (Zend_Date::isDate($this->value, $this->getConfig('datavalueformat'), $locale)) {
187
				$valueObj = new Zend_Date($this->value, $this->getConfig('datavalueformat'), $locale);
188
				if($userTz) $valueObj->setTimezone($userTz);
189
190
				// Set view values in sub-fields
191
				if($this->dateField->getConfig('dmyfields')) {
192
					$this->dateField->setValue($valueObj->toArray());
193
				} else {
194
					$this->dateField->setValue(
195
						$valueObj->get($this->dateField->getConfig('dateformat'), $locale));
196
				}
197
				$this->timeField->setValue($valueObj->get($this->timeField->getConfig('timeformat'), $locale));
198
			}
199
		}
200
201
		return $this;
202
	}
203
204
	public function Value() {
205
		$valDate = $this->dateField->Value();
206
		$valTime = $this->timeField->Value();
207
		if(!$valTime) $valTime = '00:00:00';
208
209
		return sprintf($this->getConfig('datetimeorder'), $valDate, $valTime);
210
	}
211
212
	public function setDisabled($bool) {
213
		parent::setDisabled($bool);
214
		$this->dateField->setDisabled($bool);
215
		$this->timeField->setDisabled($bool);
216
		if($this->timezoneField) $this->timezoneField->setDisabled($bool);
217
		return $this;
218
	}
219
220
	public function setReadonly($bool) {
221
		parent::setReadonly($bool);
222
		$this->dateField->setReadonly($bool);
223
		$this->timeField->setReadonly($bool);
224
		if($this->timezoneField) $this->timezoneField->setReadonly($bool);
225
		return $this;
226
	}
227
228
	/**
229
	 * @return DateField
230
	 */
231
	public function getDateField() {
232
		return $this->dateField;
233
	}
234
235
	/**
236
	 * @param FormField
237
	 */
238
	public function setDateField($field) {
239
		$expected = $this->getName() . '[date]';
240
		if($field->getName() != $expected) {
241
			throw new InvalidArgumentException(sprintf(
242
				'Wrong name format for date field: "%s" (expected "%s")',
243
				$field->getName(),
244
				$expected
245
			));
246
		}
247
248
		$field->setForm($this->getForm());
249
		$this->dateField = $field;
250
		$this->setValue($this->value); // update value
251
	}
252
253
	/**
254
	 * @return TimeField
255
	 */
256
	public function getTimeField() {
257
		return $this->timeField;
258
	}
259
260
	/**
261
	 * @param FormField
262
	 */
263
	public function setTimeField($field) {
264
		$expected = $this->getName() . '[time]';
265
		if($field->getName() != $expected) {
266
			throw new InvalidArgumentException(sprintf(
267
				'Wrong name format for time field: "%s" (expected "%s")',
268
				$field->getName(),
269
				$expected
270
			));
271
		}
272
273
		$field->setForm($this->getForm());
274
		$this->timeField = $field;
275
		$this->setValue($this->value); // update value
276
	}
277
278
	/**
279
	 * @return FormField
280
	 */
281
	public function getTimezoneField() {
282
		return $this->timezoneField;
283
	}
284
285
	public function setLocale($locale) {
286
		$this->dateField->setLocale($locale);
287
		$this->timeField->setLocale($locale);
288
		return $this;
289
	}
290
291
	public function getLocale() {
292
		return $this->dateField->getLocale();
293
	}
294
295
	/**
296
	 * Note: Use {@link getDateField()} and {@link getTimeField()}
297
	 * to set field-specific config options.
298
	 *
299
	 * @param string $name
300
	 * @param mixed $val
301
	 */
302
	public function setConfig($name, $val) {
303
		$this->config[$name] = $val;
304
305
		if($name == 'usertimezone') {
306
			$this->timezoneField->setValue($val);
307
			$this->setValue($this->dataValue());
308
		}
309
310
		return $this;
311
	}
312
313
	/**
314
	 * Note: Use {@link getDateField()} and {@link getTimeField()}
315
	 * to get field-specific config options.
316
	 *
317
	 * @param String $name Optional, returns the whole configuration array if empty
318
	 * @return mixed
319
	 */
320
	public function getConfig($name = null) {
321
		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...
322
			return isset($this->config[$name]) ? $this->config[$name] : null;
323
		} else {
324
			return $this->config;
325
		}
326
	}
327
328
	public function validate($validator) {
329
		$dateValid = $this->dateField->validate($validator);
330
		$timeValid = $this->timeField->validate($validator);
331
332
		return ($dateValid && $timeValid);
333
	}
334
335
	public function performReadonlyTransformation() {
336
		$field = $this->castedCopy('DatetimeField_Readonly');
337
		$field->setValue($this->dataValue());
338
339
		$dateFieldConfig = $this->getDateField()->getConfig();
340
		if($dateFieldConfig) {
341
			foreach($dateFieldConfig as $k => $v) {
342
				$field->getDateField()->setConfig($k, $v);
343
			}
344
		}
345
346
		$timeFieldConfig = $this->getTimeField()->getConfig();
347
		if($timeFieldConfig) {
348
			foreach($timeFieldConfig as $k => $v) {
349
				$field->getTimeField()->setConfig($k, $v);
350
			}
351
		}
352
353
		return $field;
354
	}
355
356
	public function __clone() {
357
		$this->dateField = clone $this->dateField;
358
		$this->timeField = clone $this->timeField;
359
	}
360
}
361
362
/**
363
 * The readonly class for our {@link DatetimeField}.
364
 *
365
 * @package forms
366
 * @subpackage fields-datetime
367
 */
368
class DatetimeField_Readonly extends DatetimeField {
369
370
	protected $readonly = true;
371
372
	public function Field($properties = array()) {
373
		$valDate = $this->dateField->dataValue();
374
		$valTime = $this->timeField->dataValue();
375
376
		if($valDate && $valTime) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $valDate 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...
Bug Best Practice introduced by
The expression $valTime 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...
377
			$format = sprintf(
378
				$this->getConfig('datetimeorder'),
379
				$this->dateField->getConfig('dateformat'),
380
				$this->timeField->getConfig('timeformat')
381
			);
382
			$valueObj = new Zend_Date(
383
				sprintf($this->getConfig('datetimeorder'), $valDate, $valTime),
384
				$this->getConfig('datavalueformat'),
385
				$this->dateField->getLocale()
386
			);
387
			$val = $valueObj->toString($format);
388
389
		} else {
390
			$val = sprintf('<em>%s</em>', _t('DatetimeField.NOTSET', 'Not set'));
391
		}
392
393
		return "<span class=\"readonly\" id=\"" . $this->id() . "\">$val</span>";
394
	}
395
396
}
397