
date() formatuje znacznik czasu, DateTimeImmutable przydaje się przy każdym obliczeniu, a IntlDateFormatter daje francuskie nazwy dni i miesięcy. Ustaw strefę wprost: bez date.timezone PHP używa UTC. strftime() jest wycofane od PHP 8.1 i nie zastępuje go date(), które niczego nie tłumaczy.
Wyświetlenie daty w PHP jest proste. Wyświetlenie właściwej daty, we właściwej strefie, po francusku, i policzenie odstępu bez pomyłki przy zmianie czasu, już znacznie mniej. Ten artykuł omawia podstawowe funkcje, a potem klasy DateTimeImmutable i IntlDateFormatter, które rozwiązują przypadki, z jakimi date() sobie nie radzi. Wszystkie wyniki powstały na PHP 8.5.10.
Najpierw strefa czasowa
To źródło błędów numer jeden i działa po cichu. Jeśli date.timezone nie jest ustawione w php.ini, PHP używa UTC. Francuska strona pokazuje wtedy godzinę przesuniętą o jedną albo dwie, zależnie od pory roku, i nigdzie nie pojawia się ostrzeżenie.
echo ini_get('date.timezone'), "\n"; // (puste) na wielu instalacjach
echo date_default_timezone_get(), "\n"; // UTCUstawia się to w php.ini albo na starcie aplikacji:
date_default_timezone_set('Europe/Paris');Zasada jest jedna: przechowuj w UTC, wyświetlaj w strefie użytkownika. Baza danych pełna czasów lokalnych staje się bezużyteczna, gdy tylko użytkownik zmieni strefę albo nadejdzie czas letni.
date() i time()
time() zwraca liczbę sekund, które upłynęły od 1. stycznia 1970 roku, od północy czasu UTC. date() formatuje taki znacznik czasu, albo bieżącą chwilę, jeśli nic mu nie podasz.
date_default_timezone_set('Europe/Paris');
echo date('d/m/Y'), "\n"; // 02/09/2026
echo date('d/m/Y H:i:s'), "\n"; // 02/09/2026 21:47:03
echo time(), "\n"; // 1788378423
echo date('d/m/Y', 1645303037), "\n"; // 19/02/2022Najczęściej używane znaki formatu:
| Data | Godzina | ||
|---|---|---|---|
d | dzień na 2 cyfrach (01 do 31) | H | godzina w cyklu 24-godzinnym (00 do 23) |
j | dzień bez zera (1 do 31) | h | godzina w cyklu 12-godzinnym (01 do 12) |
m | miesiąc na 2 cyfrach (01 do 12) | i | minuty (00 do 59) |
n | miesiąc bez zera (1 do 12) | s | sekundy (00 do 59) |
Y | rok na 4 cyfrach | A | AM albo PM |
N | dzień tygodnia (1 = poniedziałek) | P | przesunięcie, na przykład +02:00 |
t | liczba dni w miesiącu | U | znacznik czasu Unix |
L | 1, jeśli rok jest przestępny | c | pełny format ISO 8601 |
Żeby wstawić znak, który jest zarazem kodem formatu, trzeba go poprzedzić lewym ukośnikiem:
echo date('\L\e d/m/Y'), "\n"; // Le 02/09/2026
// bez lewego ukośnika „L” dałoby 0 albo 1, a „e” nazwę strefydate() nie mówi po francusku
To druga klasyczna pułapka. Nazwy dni i miesięcy zwracane przez date() są zawsze angielskie, a setlocale() niczego tu nie zmienia.
$ts = mktime(0, 0, 0, 4, 1, 2022);
echo date('l', $ts), "\n"; // Friday
setlocale(LC_TIME, 'fr_FR.UTF-8', 'fr_FR');
echo date('l', $ts), "\n"; // Friday — bez zmianRozwiązaniem jest IntlDateFormatter z rozszerzenia intl:
$formateur = new IntlDateFormatter(
'fr_FR',
IntlDateFormatter::FULL,
IntlDateFormatter::NONE,
'Europe/Paris',
IntlDateFormatter::GREGORIAN,
);
echo $formateur->format($ts), "\n"; // vendredi 1 avril 2022Do własnego formatu klasa przyjmuje wzorzec ICU, którego kody różnią się od tych z date():
$formateur->setPattern("EEEE d MMMM y '\u{e0}' HH'h'mm");
echo $formateur->format(new DateTimeImmutable('now', new DateTimeZone('Europe/Paris')));
// mercredi 2 septembre 2026 à 21h47Jeśli intl jest niedostępne, tablica odpowiedników ratuje sytuację po francusku, ale nie da się jej rozciągnąć na wiele języków.
DateTimeImmutable, właściwy wybór
Do wszystkiego, co wykracza poza samo wyświetlanie, klasy dat sprawdzają się lepiej niż funkcje. DateTimeImmutable jest lepsze od DateTime: jego metody zwracają nowy obiekt zamiast modyfikować bieżący, co eliminuje zmiany na odległość.
$paris = new DateTimeZone('Europe/Paris');
$date = new DateTimeImmutable('2026-09-02 14:30:00', $paris);
echo $date->format('d/m/Y H:i P'), "\n"; // 02/09/2026 14:30 +02:00
echo $date->modify('+3 days')->format('d/m/Y'), "\n"; // 05/09/2026
echo $date->format('d/m/Y'), "\n"; // 02/09/2026 — bez zmian
// Z DateTime (mutowalnym) trzeci wiersz pokazałby 05/09/2026// Dodawanie i odejmowanie okresów
$dans30Mois = $date->add(new DateInterval('P30M'));
$ilYa2Sem = $date->sub(new DateInterval('P2W'));
// Różnica między dwiema datami
$ecart = $date->diff(new DateTimeImmutable('2026-12-25', $paris));
echo $ecart->days, " jours\n";
echo $ecart->format('%m mois et %d jours'), "\n";
// Seria dat
$periode = new DatePeriod(
$date,
new DateInterval('P1D'),
new DateTimeImmutable('2026-09-07', $paris),
);
foreach ($periode as $jour) {
echo $jour->format('D d/m'), "\n";
}Pułapka ostatniego dnia miesiąca
Dodanie miesiąca do końca miesiąca nie daje tego, czego się spodziewasz, i działa tak samo z DateInterval, jak z arytmetyką na mktime().
$fin = new DateTimeImmutable('2026-01-31');
echo $fin->add(new DateInterval('P1M'))->format('d/m/Y'), "\n"; // 03/03/202631 lutego nie istnieje: PHP przelewa go na marzec. Żeby dostać ostatni dzień następnego miesiąca, trzeba o niego poprosić wprost:
echo $fin->modify('last day of next month')->format('d/m/Y'), "\n"; // 28/02/2026Doba nie zawsze ma 24 godziny
Przy przejściu na czas letni doba ma ich 23, przy przejściu na zimowy, 25. Dodanie 86400 sekund do znacznika czasu daje wtedy złą godzinę.
$paris = new DateTimeZone('Europe/Paris');
$avant = new DateTimeImmutable('2026-03-28 12:00:00', $paris);
$apres = $avant->add(new DateInterval('P1D'));
echo $apres->format('d/m/Y H:i P'), "\n"; // 29/03/2026 12:00 +02:00
echo ($apres->getTimestamp() - $avant->getTimestamp()) / 3600, " heures\n"; // 23DateInterval liczy w dniach kalendarzowych i poprawnie zwraca południe następnego dnia, mimo że minęły tylko 23 godziny. To zachowanie właściwe dla przypomnienia „jutro w południe”, a błędne dla rozliczanego czasu. To dwa różne pojęcia: P1D to dzień kalendarzowy, PT24H to dwadzieścia cztery godziny.
Odczyt daty wpisanej przez użytkownika
strtotime() rozumie bardzo dużo, ale formaty niejednoznaczne interpretuje po amerykańsku: 03/04/2026 to 4 marca, a nie 3 kwietnia. Przy znanym formacie createFromFormat() nie zostawia miejsca na domysły.
$date = DateTimeImmutable::createFromFormat(
'!d/m/Y', // „!” resetuje godzinę do 00:00:00
'03/04/2026',
new DateTimeZone('Europe/Paris'),
);
echo $date->format('d F Y'), "\n"; // 03 April 2026Bez początkowego ! pola, których nie podasz, przyjmują wartości z bieżącej chwili: dwa uruchomienia tego samego kodu dają dwie różne godziny.
Najgroźniejsze jest to, że niepoprawna data nie kończy się błędem, tylko po cichu się przelewa.
$saisie = '31/02/2026';
$date = DateTimeImmutable::createFromFormat('!d/m/Y', $saisie, $paris);
echo $date->format('d/m/Y'), "\n"; // 03/03/2026
print_r(DateTimeImmutable::getLastErrors()); // ['warnings' => [10 => 'The parsed date was invalid'], …]
var_dump(checkdate(2, 31, 2026)); // bool(false)Po każdym parsowaniu trzeba więc sprawdzić getLastErrors() albo porównać datę sformatowaną z powrotem z tym, co przyszło z formularza:
function lireDate(string $saisie, DateTimeZone $fuseau): ?DateTimeImmutable
{
$date = DateTimeImmutable::createFromFormat('!d/m/Y', $saisie, $fuseau);
if ($date === false) {
return null;
}
$erreurs = DateTimeImmutable::getLastErrors();
if ($erreurs !== false && ($erreurs['warning_count'] || $erreurs['error_count'])) {
return null;
}
return $date->format('d/m/Y') === $saisie ? $date : null;
}Od PHP 8.2 getLastErrors() zwraca false, kiedy nie ma nic do zgłoszenia, zamiast tablicy z wyzerowanymi licznikami. Test musi to uwzględniać.
Co nowego
PHP 8.4 dorzucił createFromTimestamp(), które przyjmuje ułamkowe znaczniki czasu:
$d = DateTimeImmutable::createFromTimestamp(1645303037.5);
echo $d->format('d/m/Y H:i:s.u P'), "\n"; // 19/02/2022 20:37:17.500000 +00:00Wcześniej trzeba było iść przez '@' . $ts albo setTimestamp(), a mikrosekundy ginęły po drodze.
Przestarzałe funkcje, których nie należy już używać
Kilka wciąż obecnych funkcji daty wypisuje ostrzeżenie o wycofaniu. Na PHP 8.5.10 działają, ale ich usunięcie jest już zaplanowane.
| Funkcja | Wycofana od | Zamiennik |
|---|---|---|
strftime() | PHP 8.1 | IntlDateFormatter::format() |
gmstrftime() | PHP 8.1 | IntlDateFormatter::format() |
strptime() | PHP 8.2 | date_parse_from_format() albo IntlDateFormatter::parse() |
date_sunrise() / date_sunset() | PHP 8.1 | date_sun_info() |
utf8_encode() | PHP 8.2 | mb_convert_encoding() |
Dokładne komunikaty, zebrane na PHP 8.5.10 i na PHP 8.4.25:
Deprecated: Function strftime() is deprecated since 8.1,
use IntlDateFormatter::format() instead
Deprecated: Function strptime() is deprecated since 8.2,
use date_parse_from_format() (for locale-independent parsing),
or IntlDateFormatter::parse() (for locale-dependent parsing) instead
Deprecated: Constant SUNFUNCS_RET_STRING is deprecated since 8.4,
as date_sunrise() and date_sunset() were deprecated in 8.1strftime() było zwykłym sposobem na datę po francusku. Jego zamiennikiem nie jest date(), które niczego nie tłumaczy, tylko właśnie IntlDateFormatter.
Odwrotnie niż one, mktime(), checkdate(), getdate() i idate() nie są wycofane i działają normalnie. W prostych przypadkach wciąż się przydają, nawet jeśli klasy pokrywają wszystko, co robią.
Do zapamiętania
- Ustaw strefę wprost, przechowuj w UTC, wyświetlaj lokalnie.
date()do sformatowania chwili,DateTimeImmutabledo każdego obliczenia.IntlDateFormatterdo francuskich nazw dni i miesięcy.- Po
createFromFormat()sprawdźgetLastErrors(): niepoprawna data przelewa się po cichu. - Dzień kalendarzowy to nie 24 godziny.
P1DiPT24Hnie są wymienne.
Do parsowania milionów dat w pliku importu zobacz czytanie dużych plików w PHP, a żeby zapisać je w UTC, połączenie z bazą danych.
Zobacz też walidację daty wyrażeniem regularnym, obsługę dat z Carbonem w Laravelu i hub Programowanie webowe.
Częste błędy
date.timezone w php.ini PHP wraca do UTC i pokazuje przesuniętą godzinę bez najmniejszego ostrzeżenia.setlocale() nie ma żadnego wpływu na date(). Zamiennikiem strftime() jest IntlDateFormatter, a nie date().31/02/2026 staje się 03/03/2026 ze zwykłym ostrzeżeniem w getLastErrors(). Trzeba je sprawdzać po każdym parsowaniu.03/04/2026 to 4 marca, a nie 3 kwietnia.31/01/2026 + P1M daje 03/03/2026. Po ostatni dzień miesiąca sięgnij po modify('last day of next month').P1D poprawnie przesuwa o dzień kalendarzowy, choć minęły tylko 23 godziny. P1D i PT24H nie są wymienne.false, kiedy nie ma nic do zgłoszenia, zamiast tablicy z wyzerowanymi licznikami.

