DateTimeValidator   C
last analyzed

Complexity

Total Complexity 64

Size/Duplication

Total Lines 250
Duplicated Lines 11.2 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
dl 28
loc 250
rs 5.8364
c 0
b 0
f 0
wmc 64
lcom 1
cbo 11

2 Methods

Rating   Name   Duplication   Size   Complexity  
F validate() 28 189 57
C getMinOrMaxValue() 0 37 7

How to fix   Duplicated Code    Complexity   

Duplicated Code

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 Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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

1
<?php
2
namespace Agavi\Validator;
3
4
// +---------------------------------------------------------------------------+
5
// | This file is part of the Agavi package.                                   |
6
// | Copyright (c) 2005-2011 the Agavi Project.                                |
7
// |                                                                           |
8
// | For the full copyright and license information, please view the LICENSE   |
9
// | file that was distributed with this source code. You can also view the    |
10
// | LICENSE file online at http://www.agavi.org/LICENSE.txt                   |
11
// |   vi: set noexpandtab:                                                    |
12
// |   Local Variables:                                                        |
13
// |   indent-tabs-mode: t                                                     |
14
// |   End:                                                                    |
15
// +---------------------------------------------------------------------------+
16
use Agavi\Config\Config;
17
use Agavi\Date\Calendar;
18
use Agavi\Date\DateDefinitions;
19
use Agavi\Date\DateFormat;
20
use Agavi\Exception\AgaviException;
21
use Agavi\Exception\ConfigurationException;
22
use Agavi\Exception\ValidatorException;
23
use Agavi\Translation\DateFormatter;
24
use Agavi\Translation\Locale;
25
26
/**
27
 * DateTimeValidator verifies that a parameter is of a date and or time
28
 * format.
29
 *
30
 * Arguments:
31
 *   This can be:
32
 *    * a single argument which will then be parsed with the formats in the
33
 *      'formats' parameter.
34
 *    * multiple arguments with the calendar constants
35
 *      (AgaviDateDefinitions::MONTH, etc) as key and the argument field as
36
 *      value.
37
 *    * multiple arguments and the 'arguments_format' parameter defined. This
38
 *      will use the string in 'arguments_format' as input string to sprintf and
39
 *      will use the arguments in the given order as argument to sprintf.
40
 *
41
 * Parameters:
42
 *   'check'       check date if the specified day really exists
43
 *   'formats'     an array of arrays with these keys:
44
 *     'type'       The type of the string in 'format'.
45
 *     'format'     The input string dependent on the type. These types are
46
 *                  allowed:
47
 *                    format:   The value is a date format string.
48
 *                    time:     The value is a time specifier (full,...) or null
49
 *                    date:     The value is a date specifier or null
50
 *                    datetime: The value is a date specifier or null
51
 *                    translation_domain: The value will be translated in the
52
 *                              domain given in the 'translation_domain' key.
53
 *                    unix:     Always null/empty
54
 *                    unix_milliseconds: Always null/empty
55
 *
56
 *     'locale'     The optional locale which will be used for this format.
57
 *     'translation_domain' Only applicable when the type is translation_domain
58
 *   'cast_to'     Only useful in combination with the export parameter.
59
 *                 This can either be a string or an array. If its an string it
60
 *                 can be one of 'unix' (converts the date to a unix timestamp),
61
 *                 'string' (converts it to a string using the default format),
62
 *                 'calendar' (will return the AgaviCalendar object),
63
 *                 'datetime' (case sensitive, will return a PHP DateTime
64
 *                 object, requires PHP 5.1.x with DateTime explicitly enabled
65
 *                 or >= PHP 5.2).
66
 *                 If it's an array it can have these keys:
67
 *     'type'        The type of the format (format, time, date, datetime)
68
 *     'format'      see in 'formats' above.
69
 *   'arguments_format' A string which will be used as the format string for
70
 *                 sprintf.
71
 *   'min'         Either an string or an array. When its a string the the
72
 *                 its assumed to be in the format 'yyyy-MM-dd[ HH:mm:ss[.S]]'.
73
 *                 When its an array it will take the minimum value from a
74
 *                 request field. These indizes apply:
75
 *     'format'      A custom format string which should be used when the field
76
 *                   is an string.
77
 *     'field'       The name of the field to use as minimum value (could be a
78
 *                   previous exported calendar object). Do NOT use unvalidated
79
 *                   fields here. Lax parsing will be used.
80
 *                 This value is inclusive.
81
 *   'max'         The same as min except that the max is exclusive.
82
 *
83
 * @package    agavi
84
 * @subpackage validator
85
 *
86
 * @author     Dominik del Bondio <[email protected]>
87
 * @copyright  Authors
88
 * @copyright  The Agavi Project
89
 *
90
 * @since      0.11.0
91
 *
92
 * @version    $Id$
93
 */
94
class DateTimeValidator extends Validator
95
{
96
    /**
97
     * Validates the input.
98
     *
99
     * @return     bool True if the input was a valid date.
100
     *
101
     * @author     Dominik del Bondio <[email protected]>
102
     * @since      0.11.0
103
     */
104
    protected function validate()
105
    {
106
        if (!Config::get('core.use_translation')) {
107
            throw new ConfigurationException('The datetime validator can only be used with use_translation on');
108
        }
109
        $tm = $this->getContext()->getTranslationManager();
110
        $cal = null;
111
112
        $check = $this->getParameter('check', true);
113
        $locale = $this->hasParameter('locale') ? $tm->getLocale($this->getParameter('locale')) : $tm->getCurrentLocale();
114
115
        if ($this->hasMultipleArguments() && !$this->getParameter('arguments_format')) {
116
            $cal = $tm->createCalendar();
117
            $cal->clear();
118
            $cal->setLenient(!$check);
119
            foreach ($this->getArguments() as $calField => $field) {
120
                $param = $this->getData($field);
121
                if (defined($calField)) {
122
                    $calField = constant($calField);
123
                } elseif (!is_numeric($calField)) {
124
                    throw new ValidatorException('Unknown argument name "' . $calField . '" for argument "' . $field . '" supplied. This needs to be one of the constants defined in AgaviDateDefinitions.');
125
                }
126
                if (!is_scalar($param)) {
127
                    // everything which is non scalar is ignored, since it couldn't be handled anyways
128
                    continue;
129
                }
130
131
                if ($calField == DateDefinitions::MONTH) {
132
                    $param -= 1;
133
                }
134
135
                $cal->set($calField, (float) $param);
136
            }
137
138
            try {
139
                $cal->getTime();
140
            } catch (AgaviException $e) {
141
                $this->throwError('check');
142
                return false;
143
            }
144
        } else {
145
            if ($argFormat = $this->getParameter('arguments_format')) {
146
                $values = array();
147
                foreach ($this->getArguments() as $field) {
148
                    $values[] = $this->getData($field);
149
                }
150
                $param = vsprintf($argFormat, $values);
151
            } else {
152
                $param = $this->getData($this->getArgument());
153
                if (!is_scalar($param)) {
154
                    $this->throwError();
155
                    return false;
156
                }
157
            }
158
159
            $matchedFormat = false;
160
            foreach ((array)$this->getParameter('formats', array()) as $key => $item) {
161
                if (!is_array($item)) {
162
                    $item = array((is_int($key) ? 'format' : $key) => $item);
163
                }
164
                
165
                $itemLocale = empty($item['locale']) ? $locale : $tm->getLocale($item['locale']);
166
                $type = empty($item['type']) ? 'format' : $item['type'];
167
168
                if ($type == 'format') {
169
                    $formatString = $item['format'];
170 View Code Duplication
                } elseif ($type == 'time' || $type == 'date' || $type == 'datetime') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
171
                    $format = isset($item['format']) ? $item['format'] : null;
172
                    $formatString = DateFormatter::resolveFormat($format, $itemLocale, $type);
173
                } elseif ($type == 'translation_domain') {
174
                    $td = $item['translation_domain'];
175
                    $formatString = $tm->_($item['format'], $td, $itemLocale);
176
                } elseif ($type == 'unix') {
177
                    $matchedFormat = ($param === (string)(int)$param);
178
                    $cal = $tm->createCalendar($itemLocale);
179
                    $cal->setUnixTimestamp($param);
180 View Code Duplication
                    if ($matchedFormat) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
181
                        try {
182
                            if ($cal->getUnixTimestamp() !== (int)$param) {
183
                                $this->throwError('check');
184
                                return false;
185
                            }
186
                        } catch (AgaviException $e) {
187
                            $matchedFormat = false;
188
                        }
189
                    }
190
                } elseif ($type == 'unix_milliseconds') {
191
                    $matchedFormat = is_numeric($param);
192
                    $cal = $tm->createCalendar($itemLocale);
193
                    $cal->setTime($param);
194 View Code Duplication
                    if ($matchedFormat) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
195
                        try {
196
                            if ($cal->getTime() !== (float)$param) {
197
                                $this->throwError('check');
198
                                return false;
199
                            }
200
                        } catch (AgaviException $e) {
201
                            $matchedFormat = false;
202
                        }
203
                    }
204
                }
205
206
                if (!$cal) {
207
                    try {
208
                        $format = new DateFormat($formatString);
0 ignored issues
show
Bug introduced by
The variable $formatString does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
209
                        $cal = $format->parse($param, $itemLocale, $check);
210
211
                        // no exception got thrown so the parsing was successful
212
                        $matchedFormat = true;
213
                        break;
214
                    } catch (AgaviException $e) {
215
                        // nop
216
                    }
217
                }
218
            }
219
220
            if (!$matchedFormat) {
221
                $this->throwError('format');
222
                return false;
223
            }
224
        }
225
226
        $cal->setLenient(true);
227
        $value = $cal;
228
229
        if ($cast = $this->getParameter('cast_to')) {
230
            // an array means the user wants it custom formatted
231
            if (is_array($cast)) {
232
                $type = empty($cast['type']) ? 'format' : $cast['type'];
233
                if ($type == 'format') {
234
                    $formatString = $cast['format'];
235 View Code Duplication
                } elseif ($type == 'time' || $type == 'date' || $type == 'datetime') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
236
                    $format = isset($cast['format']) ? $cast['format'] : null;
237
                    $formatString = DateFormatter::resolveFormat($format, $locale, $type);
238
                }
239
240
                $format = new DateFormat($formatString);
241
                $value = $format->format($cal, $cal->getType(), $locale);
242
            } else {
243
                $cast = strtolower($cast);
244
                if ($cast == 'unix') {
245
                    $value = $cal->getUnixTimestamp();
246
                } elseif ($cast == 'string') {
247
                    $value = $tm->_d($cal);
248
                } elseif ($cast == 'datetime') {
249
                    $value = $cal->getNativeDateTime();
250
                } else {
251
                    $value = $cal;
252
                }
253
            }
254
        }
255
256
        $defaultParseFormat = new DateFormat('yyyy-MM-dd HH:mm:ss.S');
257
258
        if ($this->hasParameter('min')) {
259
            $min = $this->getMinOrMaxValue('min', $defaultParseFormat, $locale);
260
261
            $isAfterEqual = $cal->after($min) || $cal->equals($min);
262
            if (!$isAfterEqual) {
263
                $this->throwError('min');
264
                return false;
265
            }
266
        }
267
268
        if ($this->hasParameter('max')) {
269
            $max = $this->getMinOrMaxValue('max', $defaultParseFormat, $locale);
270
271
            $isBefore = $cal->before($max);
272
            if (!$isBefore) {
273
                $this->throwError('max');
274
                return false;
275
            }
276
        }
277
278
        if ($this->hasParameter('export')) {
279
            $export = $this->getParameter('export');
280
            if (is_string($export)) {
281
                $this->export($value);
282
            } elseif (is_array($export)) {
283
                foreach ($export as $calField => $field) {
284
                    if (defined($calField)) {
285
                        $this->export($cal->get(constant($calField)), $field);
286
                    }
287
                }
288
            }
289
        }
290
291
        return true;
292
    }
293
294
    /**
295
     * Returns the calendar object for a max or min definition.
296
     *
297
     * @param      string $minMax 'min' or 'max'
298
     * @param      DateFormat $defaultParseFormat The default format when parsing strings.
299
     * @param      Locale $locale The locale to use.
300
     *
301
     * @return     Calendar The calendar object storing the date.
302
     *
303
     * @author     Dominik del Bondio <[email protected]>
304
     * @since      0.11.0
305
     */
306
    protected function getMinOrMaxValue($minMax, $defaultParseFormat, $locale)
307
    {
308
        $format = $defaultParseFormat;
309
310
        $minMax = $this->getParameter($minMax);
311
        if (is_array($minMax)) {
312
            $minMaxValue = $this->validationParameters->getParameter($minMax['field']);
313
            if (!$minMaxValue instanceof Calendar) {
314
                if (isset($minMax['format'])) {
315
                    $format = new DateFormat($minMax['format']);
316
                }
317
                $result = $format->parse($minMaxValue, $locale, false);
318
            } else {
319
                $result = $minMaxValue;
320
            }
321
        } elseif (strpos($minMax, '.') === false) {
322
            // a strtotime compatible string does not contain a dot, so all strings with dots are assumed to be
323
            // strings matching the calendar format and all others are handled with strtotime
324
            $tz = $locale->getLocaleTimeZone();
325
            if ($tz) {
326
                // set the timezone for the strtotime call to be the same as for creating the calendar
327
                $oldDefaultTimezone = date_default_timezone_get();
328
                date_default_timezone_set($tz);
329
            }
330
            // create the calendar in the requested locale/timezone
331
            $result = $this->getContext()->getTranslationManager()->createCalendar($locale);
332
            $result->setUnixTimestamp(strtotime($minMax));
333
            if ($tz) {
334
                // reset the php timezone
335
                date_default_timezone_set($oldDefaultTimezone);
0 ignored issues
show
Bug introduced by
The variable $oldDefaultTimezone does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
336
            }
337
        } else {
338
            $result = $format->parse($minMax, $locale, false);
339
        }
340
341
        return $result;
342
    }
343
}
344