Patched to Drupal 8.4.8 level. See https://www.drupal.org/sa-core-2018-004 and patch...
[yaffs-website] / web / core / lib / Drupal / Core / Datetime / DateFormatter.php
1 <?php
2
3 namespace Drupal\Core\Datetime;
4
5 use Drupal\Core\Config\ConfigFactoryInterface;
6 use Drupal\Core\Entity\EntityManagerInterface;
7 use Drupal\Core\Language\Language;
8 use Drupal\Core\Language\LanguageManagerInterface;
9 use Drupal\Core\StringTranslation\TranslationInterface;
10 use Drupal\Core\StringTranslation\StringTranslationTrait;
11 use Symfony\Component\HttpFoundation\RequestStack;
12
13 /**
14  * Provides a service to handle various date related functionality.
15  *
16  * @ingroup i18n
17  */
18 class DateFormatter implements DateFormatterInterface {
19   use StringTranslationTrait;
20
21   /**
22    * The list of loaded timezones.
23    *
24    * @var array
25    */
26   protected $timezones;
27
28   /**
29    * The date format storage.
30    *
31    * @var \Drupal\Core\Entity\EntityStorageInterface
32    */
33   protected $dateFormatStorage;
34
35   /**
36    * Language manager for retrieving the default langcode when none is specified.
37    *
38    * @var \Drupal\Core\Language\LanguageManagerInterface
39    */
40   protected $languageManager;
41
42   /**
43    * The configuration factory.
44    *
45    * @var \Drupal\Core\Config\ConfigFactoryInterface
46    */
47   protected $configFactory;
48
49   /**
50    * The request stack.
51    *
52    * @var \Symfony\Component\HttpFoundation\RequestStack
53    */
54   protected $requestStack;
55
56   protected $country = NULL;
57   protected $dateFormats = [];
58
59   /**
60    * Contains the different date interval units.
61    *
62    * This array is keyed by strings representing the unit (e.g.
63    * '1 year|@count years') and with the amount of values of the unit in
64    * seconds.
65    *
66    * @var array
67    */
68   protected $units = [
69     '1 year|@count years' => 31536000,
70     '1 month|@count months' => 2592000,
71     '1 week|@count weeks' => 604800,
72     '1 day|@count days' => 86400,
73     '1 hour|@count hours' => 3600,
74     '1 min|@count min' => 60,
75     '1 sec|@count sec' => 1,
76   ];
77
78   /**
79    * Constructs a Date object.
80    *
81    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
82    *   The entity manager.
83    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
84    *   The language manager.
85    * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
86    *   The string translation.
87    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
88    *   The configuration factory.
89    * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
90    *   The request stack.
91    */
92   public function __construct(EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager, TranslationInterface $translation, ConfigFactoryInterface $config_factory, RequestStack $request_stack) {
93     $this->dateFormatStorage = $entity_manager->getStorage('date_format');
94     $this->languageManager = $language_manager;
95     $this->stringTranslation = $translation;
96     $this->configFactory = $config_factory;
97     $this->requestStack = $request_stack;
98   }
99
100   /**
101    * {@inheritdoc}
102    */
103   public function format($timestamp, $type = 'medium', $format = '', $timezone = NULL, $langcode = NULL) {
104     if (!isset($timezone)) {
105       $timezone = date_default_timezone_get();
106     }
107     // Store DateTimeZone objects in an array rather than repeatedly
108     // constructing identical objects over the life of a request.
109     if (!isset($this->timezones[$timezone])) {
110       $this->timezones[$timezone] = timezone_open($timezone);
111     }
112
113     if (empty($langcode)) {
114       $langcode = $this->languageManager->getCurrentLanguage()->getId();
115     }
116
117     // Create a DrupalDateTime object from the timestamp and timezone.
118     $create_settings = [
119       'langcode' => $langcode,
120       'country' => $this->country(),
121     ];
122     $date = DrupalDateTime::createFromTimestamp($timestamp, $this->timezones[$timezone], $create_settings);
123
124     // If we have a non-custom date format use the provided date format pattern.
125     if ($type !== 'custom') {
126       if ($date_format = $this->dateFormat($type, $langcode)) {
127         $format = $date_format->getPattern();
128       }
129     }
130
131     // Fall back to the 'medium' date format type if the format string is
132     // empty, either from not finding a requested date format or being given an
133     // empty custom format string.
134     if (empty($format)) {
135       $format = $this->dateFormat('fallback', $langcode)->getPattern();
136     }
137
138     // Call $date->format().
139     $settings = [
140       'langcode' => $langcode,
141     ];
142     return $date->format($format, $settings);
143   }
144
145   /**
146    * {@inheritdoc}
147    */
148   public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
149     $output = '';
150     foreach ($this->units as $key => $value) {
151       $key = explode('|', $key);
152       if ($interval >= $value) {
153         $output .= ($output ? ' ' : '') . $this->formatPlural(floor($interval / $value), $key[0], $key[1], [], ['langcode' => $langcode]);
154         $interval %= $value;
155         $granularity--;
156       }
157       elseif ($output) {
158         // Break if there was previous output but not any output at this level,
159         // to avoid skipping levels and getting output like "1 year 1 second".
160         break;
161       }
162
163       if ($granularity == 0) {
164         break;
165       }
166     }
167     return $output ? $output : $this->t('0 sec', [], ['langcode' => $langcode]);
168   }
169
170   /**
171    * {@inheritdoc}
172    */
173   public function getSampleDateFormats($langcode = NULL, $timestamp = NULL, $timezone = NULL) {
174     $timestamp = $timestamp ?: time();
175     // All date format characters for the PHP date() function.
176     $date_chars = str_split('dDjlNSwzWFmMntLoYyaABgGhHisueIOPTZcrU');
177     $date_elements = array_combine($date_chars, $date_chars);
178     return array_map(function ($character) use ($timestamp, $timezone, $langcode) {
179       return $this->format($timestamp, 'custom', $character, $timezone, $langcode);
180     }, $date_elements);
181   }
182
183   /**
184    * {@inheritdoc}
185    */
186   public function formatTimeDiffUntil($timestamp, $options = []) {
187     $request_time = $this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME');
188     return $this->formatDiff($request_time, $timestamp, $options);
189   }
190
191   /**
192    * {@inheritdoc}
193    */
194   public function formatTimeDiffSince($timestamp, $options = []) {
195     $request_time = $this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME');
196     return $this->formatDiff($timestamp, $request_time, $options);
197   }
198
199   /**
200    * {@inheritdoc}
201    */
202   public function formatDiff($from, $to, $options = []) {
203
204     $options += [
205       'granularity' => 2,
206       'langcode' => NULL,
207       'strict' => TRUE,
208       'return_as_object' => FALSE,
209     ];
210
211     if ($options['strict'] && $from > $to) {
212       $string = $this->t('0 seconds');
213       if ($options['return_as_object']) {
214         return new FormattedDateDiff($string, 0);
215       }
216       return $string;
217     }
218
219     $date_time_from = new \DateTime();
220     $date_time_from->setTimestamp($from);
221
222     $date_time_to = new \DateTime();
223     $date_time_to->setTimestamp($to);
224
225     $interval = $date_time_to->diff($date_time_from);
226
227     $granularity = $options['granularity'];
228     $output = '';
229
230     // We loop over the keys provided by \DateInterval explicitly. Since we
231     // don't take the "invert" property into account, the resulting output value
232     // will always be positive.
233     $max_age = 1e99;
234     foreach (['y', 'm', 'd', 'h', 'i', 's'] as $value) {
235       if ($interval->$value > 0) {
236         // Switch over the keys to call formatPlural() explicitly with literal
237         // strings for all different possibilities.
238         switch ($value) {
239           case 'y':
240             $interval_output = $this->formatPlural($interval->y, '1 year', '@count years', [], ['langcode' => $options['langcode']]);
241             $max_age = min($max_age, 365 * 86400);
242             break;
243
244           case 'm':
245             $interval_output = $this->formatPlural($interval->m, '1 month', '@count months', [], ['langcode' => $options['langcode']]);
246             $max_age = min($max_age, 30 * 86400);
247             break;
248
249           case 'd':
250             // \DateInterval doesn't support weeks, so we need to calculate them
251             // ourselves.
252             $interval_output = '';
253             $days = $interval->d;
254             $weeks = floor($days / 7);
255             if ($weeks) {
256               $interval_output .= $this->formatPlural($weeks, '1 week', '@count weeks', [], ['langcode' => $options['langcode']]);
257               $days -= $weeks * 7;
258               $granularity--;
259               $max_age = min($max_age, 7 * 86400);
260             }
261
262             if ((!$output || $weeks > 0) && $granularity > 0 && $days > 0) {
263               $interval_output .= ($interval_output ? ' ' : '') . $this->formatPlural($days, '1 day', '@count days', [], ['langcode' => $options['langcode']]);
264               $max_age = min($max_age, 86400);
265             }
266             else {
267               // If we did not output days, set the granularity to 0 so that we
268               // will not output hours and get things like "1 week 1 hour".
269               $granularity = 0;
270             }
271             break;
272
273           case 'h':
274             $interval_output = $this->formatPlural($interval->h, '1 hour', '@count hours', [], ['langcode' => $options['langcode']]);
275             $max_age = min($max_age, 3600);
276             break;
277
278           case 'i':
279             $interval_output = $this->formatPlural($interval->i, '1 minute', '@count minutes', [], ['langcode' => $options['langcode']]);
280             $max_age = min($max_age, 60);
281             break;
282
283           case 's':
284             $interval_output = $this->formatPlural($interval->s, '1 second', '@count seconds', [], ['langcode' => $options['langcode']]);
285             $max_age = min($max_age, 1);
286             break;
287
288         }
289         $output .= ($output && $interval_output ? ' ' : '') . $interval_output;
290         $granularity--;
291       }
292       elseif ($output) {
293         // Break if there was previous output but not any output at this level,
294         // to avoid skipping levels and getting output like "1 year 1 second".
295         break;
296       }
297
298       if ($granularity <= 0) {
299         break;
300       }
301     }
302
303     if (empty($output)) {
304       $output = $this->t('0 seconds');
305       $max_age = 0;
306     }
307
308     if ($options['return_as_object']) {
309       return new FormattedDateDiff($output, $max_age);
310     }
311
312     return $output;
313   }
314
315   /**
316    * Loads the given format pattern for the given langcode.
317    *
318    * @param string $format
319    *   The machine name of the date format.
320    * @param string $langcode
321    *   The langcode of the language to use.
322    *
323    * @return \Drupal\Core\Datetime\DateFormatInterface|null
324    *   The configuration entity for the date format in the given language for
325    *   non-custom formats, NULL otherwise.
326    */
327   protected function dateFormat($format, $langcode) {
328     if (!isset($this->dateFormats[$format][$langcode])) {
329       $original_language = $this->languageManager->getConfigOverrideLanguage();
330       $this->languageManager->setConfigOverrideLanguage(new Language(['id' => $langcode]));
331       $this->dateFormats[$format][$langcode] = $this->dateFormatStorage->load($format);
332       $this->languageManager->setConfigOverrideLanguage($original_language);
333     }
334     return $this->dateFormats[$format][$langcode];
335   }
336
337   /**
338    * Returns the default country from config.
339    *
340    * @return string
341    *   The config setting for country.default.
342    */
343   protected function country() {
344     if ($this->country === NULL) {
345       $this->country = \Drupal::config('system.date')->get('country.default');
346     }
347     return $this->country;
348   }
349
350 }