DateFormatter.php 11 KB

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