Issues (9)

src/DateExtension.php (5 issues)

1
<?php
2
3
namespace Jasny\Twig;
4
5
use Twig\Error\RuntimeError;
6
use Twig\Extension\AbstractExtension;
7
use Twig\TwigFilter;
8
9
/**
10
 * Format a date based on the current locale in Twig
11
 */
12
class DateExtension extends AbstractExtension
13
{
14
    /**
15
     * Class constructor
16
     */
17 40
    public function __construct()
18
    {
19 40
        if (!extension_loaded('intl')) {
20
            throw new \Exception("The Date Twig extension requires the 'intl' PHP extension."); // @codeCoverageIgnore
21
        }
22 40
    }
23
24
25
    /**
26
     * Return extension name
27
     *
28
     * @return string
29
     */
30
    public function getName()
31
    {
32
        return 'jasny/date';
33
    }
34
35
    /**
36
     * Callback for Twig to get all the filters.
37
     *
38
     * @return \Twig\TwigFilter[]
39
     */
40 30
    public function getFilters()
41
    {
42
        return [
43 30
            new TwigFilter('localdate', [$this, 'localDate']),
44 30
            new TwigFilter('localtime', [$this, 'localTime']),
45 30
            new TwigFilter('localdatetime', [$this, 'localDateTime']),
46 30
            new TwigFilter('duration', [$this, 'duration']),
47 30
            new TwigFilter('age', [$this, 'age']),
48
        ];
49
    }
50
51
    /**
52
     * Turn a value into a DateTime object
53
     *
54
     * @param string|int|\DateTime $date
55
     * @return \DateTime
56
     */
57 22
    protected function valueToDateTime($date)
58
    {
59 22
        if (!$date instanceof \DateTime) {
60 22
            $date = is_int($date) ? \DateTime::createFromFormat('U', $date) : new \DateTime((string)$date);
61
        }
62
63 22
        if ($date === false) {
64
            throw new RuntimeError("Invalid date '$date'");
65
        }
66
67 22
        return $date;
68
    }
69
70
    /**
71
     * Get configured intl date formatter.
72
     *
73
     * @param string|null $dateFormat
74
     * @param string|null $timeFormat
75
     * @param string      $calendar
76
     * @return \IntlDateFormatter
77
     */
78 20
    protected function getDateFormatter($dateFormat, $timeFormat, $calendar)
79
    {
80 20
        $datetype = isset($dateFormat) ? $this->getFormat($dateFormat) : null;
81 20
        $timetype = isset($timeFormat) ? $this->getFormat($timeFormat) : null;
82
83 20
        $calendarConst = $calendar === 'traditional' ? \IntlDateFormatter::TRADITIONAL : \IntlDateFormatter::GREGORIAN;
84
85 20
        $pattern = $this->getDateTimePattern(
86 20
            isset($datetype) ? $datetype : $dateFormat,
87 20
            isset($timetype) ? $timetype : $timeFormat,
88
            $calendarConst
89
        );
90
91 20
        return new \IntlDateFormatter(\Locale::getDefault(), $datetype, $timetype, null, $calendarConst, $pattern);
92
    }
93
94
    /**
95
     * Format the date/time value as a string based on the current locale
96
     *
97
     * @param string|false $format  'short', 'medium', 'long', 'full', 'none' or false
98
     * @return int|null
99
     */
100 20
    protected function getFormat($format)
101
    {
102 20
        if ($format === false) {
103 18
            $format = 'none';
104
        }
105
106
        $types = [
107 20
            'none' => \IntlDateFormatter::NONE,
108
            'short' => \IntlDateFormatter::SHORT,
109
            'medium' => \IntlDateFormatter::MEDIUM,
110
            'long' => \IntlDateFormatter::LONG,
111
            'full' => \IntlDateFormatter::FULL
112
        ];
113
114 20
        return isset($types[$format]) ? $types[$format] : null;
115
    }
116
117
    /**
118
     * Get the date/time pattern.
119
     *
120
     * @param int|string $datetype
121
     * @param int|string $timetype
122
     * @param int        $calendar
123
     * @return string
124
     */
125 20
    protected function getDateTimePattern($datetype, $timetype, $calendar = \IntlDateFormatter::GREGORIAN)
126
    {
127 20
        if (is_int($datetype) && is_int($timetype)) {
128 10
            return null;
129
        }
130
131 10
        return $this->getDatePattern(
132 10
            isset($datetype) ? $datetype : \IntlDateFormatter::SHORT,
133 10
            isset($timetype) ? $timetype : \IntlDateFormatter::SHORT,
134
            $calendar
135
        );
136
    }
137
138
    /**
139
     * Get the formatter to create a date and/or time pattern
140
     *
141
     * @param int|string $datetype
142
     * @param int|string $timetype
143
     * @param int        $calendar
144
     * @return \IntlDateFormatter
145
     */
146 4
    protected function getDatePatternFormatter($datetype, $timetype, $calendar = \IntlDateFormatter::GREGORIAN)
147
    {
148 4
        return \IntlDateFormatter::create(
149 4
            \Locale::getDefault(),
150 4
            is_int($datetype) ? $datetype : \IntlDateFormatter::NONE,
151 4
            is_int($timetype) ? $timetype : \IntlDateFormatter::NONE,
152 4
            \IntlTimeZone::getGMT(),
0 ignored issues
show
IntlTimeZone::getGMT() of type IntlTimeZone is incompatible with the type string expected by parameter $timezone of IntlDateFormatter::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

152
            /** @scrutinizer ignore-type */ \IntlTimeZone::getGMT(),
Loading history...
153 4
            $calendar
154
        );
155
    }
156
157
    /**
158
     * Get the date and/or time pattern
159
     * Default date pattern is short date pattern with 4 digit year.
160
     *
161
     * @param int|string $datetype
162
     * @param int|string $timetype
163
     * @param int        $calendar
164
     * @return string
165
     */
166 10
    protected function getDatePattern($datetype, $timetype, $calendar = \IntlDateFormatter::GREGORIAN)
167
    {
168
        $createPattern =
0 ignored issues
show
Consider adding parentheses for clarity. Current Interpretation: $createPattern = (is_int...ntlDateFormatter::NONE), Probably Intended Meaning: $createPattern = is_int(...ntlDateFormatter::NONE)
Loading history...
169 10
            (is_int($datetype) && $datetype !== \IntlDateFormatter::NONE) ||
170 10
            (is_int($timetype) && $timetype !== \IntlDateFormatter::NONE);
171
172 10
        $pattern = $createPattern ? $this->getDatePatternFormatter($datetype, $timetype, $calendar)->getPattern() : '';
173
174 10
        return trim(
175 10
            (is_string($datetype) ? $datetype . ' ' : '') .
176 10
            preg_replace('/\byy?\b/', 'yyyy', $pattern) .
177 10
            (is_string($timetype) ? ' ' . $timetype : '')
178
        );
179
    }
180
181
    /**
182
     * Format the date and/or time value as a string based on the current locale
183
     *
184
     * @param \DateTime|int|string $value
185
     * @param string               $dateFormat  null, 'short', 'medium', 'long', 'full' or pattern
186
     * @param string               $timeFormat  null, 'short', 'medium', 'long', 'full' or pattern
187
     * @param string               $calendar    'gregorian' or 'traditional'
188
     * @return string
189
     */
190 23
    protected function formatLocal($value, $dateFormat, $timeFormat, $calendar = 'gregorian')
191
    {
192 23
        if (!isset($value)) {
193 3
            return null;
194
        }
195
196 20
        $date = $this->valueToDateTime($value);
197 20
        $formatter = $this->getDateFormatter($dateFormat, $timeFormat, $calendar);
198
199 20
        return $formatter->format($date->getTimestamp());
200
    }
201
202
    /**
203
     * Format the date value as a string based on the current locale
204
     *
205
     * @param \DateTime|int|string $date
206
     * @param string               $format    null, 'short', 'medium', 'long', 'full' or pattern
207
     * @param string               $calendar  'gregorian' or 'traditional'
208
     * @return string
209
     */
210 11
    public function localDate($date, $format = null, $calendar = 'gregorian')
211
    {
212 11
        return $this->formatLocal($date, $format, false, $calendar);
0 ignored issues
show
false of type false is incompatible with the type string expected by parameter $timeFormat of Jasny\Twig\DateExtension::formatLocal(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

212
        return $this->formatLocal($date, $format, /** @scrutinizer ignore-type */ false, $calendar);
Loading history...
213
    }
214
215
    /**
216
     * Format the time value as a string based on the current locale
217
     *
218
     * @param \DateTime|int|string $date
219
     * @param string               $format    'short', 'medium', 'long', 'full' or pattern
220
     * @param string               $calendar  'gregorian' or 'traditional'
221
     * @return string
222
     */
223 7
    public function localTime($date, $format = 'short', $calendar = 'gregorian')
224
    {
225 7
        return $this->formatLocal($date, false, $format, $calendar);
0 ignored issues
show
false of type false is incompatible with the type string expected by parameter $dateFormat of Jasny\Twig\DateExtension::formatLocal(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

225
        return $this->formatLocal($date, /** @scrutinizer ignore-type */ false, $format, $calendar);
Loading history...
226
    }
227
228
    /**
229
     * Format the date/time value as a string based on the current locale
230
     *
231
     * @param \DateTime|int|string $date
232
     * @param string               $format    date format, pattern or ['date'=>format, 'time'=>format)
233
     * @param string               $calendar  'gregorian' or 'traditional'
234
     * @return string
235
     */
236 5
    public function localDateTime($date, $format = null, $calendar = 'gregorian')
237
    {
238 5
        if (is_array($format) || $format instanceof \stdClass || !isset($format)) {
239 2
            $formatDate = isset($format['date']) ? $format['date'] : null;
240 2
            $formatTime = isset($format['time']) ? $format['time'] : 'short';
241
        } else {
242 3
            $formatDate = $format;
243 3
            $formatTime = false;
244
        }
245
246 5
        return $this->formatLocal($date, $formatDate, $formatTime, $calendar);
247
    }
248
249
250
    /**
251
     * Split duration into seconds, minutes, hours, days, weeks and years.
252
     *
253
     * @param int $seconds
254
     * @return array
255
     */
256 13
    protected function splitDuration($seconds, $max)
257
    {
258 13
        if ($max < 1 || $seconds < 60) {
259 1
            return [$seconds];
260
        }
261
262 12
        $minutes = floor($seconds / 60);
263 12
        $seconds = $seconds % 60;
264 12
        if ($max < 2 || $minutes < 60) {
265 2
            return [$seconds, $minutes];
266
        }
267
268 10
        $hours = floor($minutes / 60);
269 10
        $minutes = $minutes % 60;
270 10
        if ($max < 3 || $hours < 24) {
271 4
            return [$seconds, $minutes, $hours];
272
        }
273
274 6
        $days = floor($hours / 24);
275 6
        $hours = $hours % 24;
276 6
        if ($max < 4 || $days < 7) {
277 2
            return [$seconds, $minutes, $hours, $days];
278
        }
279
280 4
        $weeks = floor($days / 7);
281 4
        $days = $days % 7;
282 4
        if ($max < 5 || $weeks < 52) {
283 2
            return [$seconds, $minutes, $hours, $days, $weeks];
284
        }
285
286 2
        $years = floor($weeks / 52);
287 2
        $weeks = $weeks % 52;
288 2
        return [$seconds, $minutes, $hours, $days, $weeks, $years];
289
    }
290
291
    /**
292
     * Calculate duration from seconds.
293
     * One year is seen as exactly 52 weeks.
294
     *
295
     * Use null to skip a unit.
296
     *
297
     * @param int    $value     Time in seconds
298
     * @param array  $units     Time units (seconds, minutes, hours, days, weeks, years)
299
     * @param string $separator
300
     * @return string
301
     */
302 14
    public function duration($value, $units = ['s', 'm', 'h', 'd', 'w', 'y'], $separator = ' ')
303
    {
304 14
        if (!isset($value)) {
305 1
            return null;
306
        }
307
308 13
        $parts = $this->splitDuration($value, count($units) - 1) + array_fill(0, 6, null);
309
310 13
        $duration = '';
311
312 13
        for ($i = 5; $i >= 0; $i--) {
313 13
            if (isset($parts[$i]) && isset($units[$i])) {
314 13
                $duration .= $separator . $parts[$i] . $units[$i];
315
            }
316
        }
317
318 13
        return trim($duration, $separator);
319
    }
320
321
    /**
322
     * Get the age (in years) based on a date.
323
     *
324
     * @param \DateTime|string $value
325
     * @return int
326
     */
327 3
    public function age($value)
328
    {
329 3
        if (!isset($value)) {
330 1
            return null;
331
        }
332
333 2
        $date = $this->valueToDateTime($value);
334
335 2
        return $date->diff(new \DateTime())->format('%y');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $date->diff(new DateTime())->format('%y') returns the type string which is incompatible with the documented return type integer.
Loading history...
336
    }
337
}
338