Dater — определяет часовой пояс, локализует и форматирует время в PHP



    Когда при разработке веб-проекта встаёт задача учёта часовых поясов пользователей, то каждый раз решение как будто приходится изобретать заново. Из множества известных мне проектов решивших эту задачу я не припомню ни одного решения, которое можно было бы назвать простым, прозрачным для всей архитектуры и универсальным.

    А что если хочется просто несколько строчек кода, и чтобы весь остальной код никогда не узнал, что данные клиенту нужно отдавать в другом часовом поясе, и что в данных пришедших от клиента время может быть указано с часовым поясом отличным от серверного? Для решения этой и многих других задач, сегодня я расскажу вам про одну лаконичную и функциональную библиотеку для PHP.

    И так, встречайте — Dater, и его основные возможности:

    • Биндинг форматов
    • Локализация текстов и форматов
    • Расширение списка опций форматирования
    • Автоопределение часового пояса
    • Конвертация времени с учётом часового пояса
    • Автоматическая конвертация времени в $_GET, $_POST, $_REQUEST с учётом часового пояса
    • Автоматическая конвертация часового пояса в шаблоне отправляемых данных

    Обещанные строчки кода по автоматическому определению и обработке пользовательского часового пояса оставим на десерт, а пока пробежимся по основным возможностям Dater, в кратце, на примерах.

    Биндинг форматов


    Позволяет стандартизовать для проекта набор используемых форматов и работать с ними по имени:

    $dater = new Dater\Dater(new Dater\Locale\En());
    $dater->format(time(), 'd/m/Y'); // 2013/03/14
    $dater->addFormat('slashedDate', 'd/m/Y');
    $dater->format(time(), 'slashedDate'); // 2013/03/14
    $dater->slashedDate(time()); // 2013/03/14
    

    Расширение опций форматирования


    Доступны все опции форматирования из date(), которые также могут быть переопределены и расширены:

    $dater->addFormatOption('ago', function (DateTime $datetime) {
        return floor((time() - $datetime->getTimestamp()) / 86400) . ' days ago';
    });
    $dater->format(time() - 60*60*24*7, 'd F Y, ago'); // 14 March 2013, 7 days ago
    

    Поддержка локалей


    $dater->setLocale(new Dater\Locale\En());
    echo $dater->date(); // 03/21/2013
    echo $dater->now('j F Y'); // 21 March 2013
    
    $dater->setLocale(Dater\Dater::getLocaleByCode('ru'));
    echo $dater->date(); // 21.03.2013
    echo $dater->now('j F Y'); // 21 марта 2013
    

    Стандартные методы для серверных и пользовательских форматов с учётом локали


    echo $dater->date(); // 03/21/2013 (client timezone, depends on locale)
    echo $dater->time(); // 5:41 AM (client timezone, depends on locale)
    echo $dater->datetime(); // 03/21/2013 5:41 (client timezone, depends on locale)
    echo $dater->isoDate(); // 2013-03-21 (client timezone)
    echo $dater->isoTime(); // 05:41:28 (client timezone)
    echo $dater->isoDatetime(); // 2013-03-21 05:41:28 (client timezone)
    echo $dater->serverDate(); // 2013-03-21 (server timezone)
    echo $dater->serverTime(); // 09:41:28 (server timezone)
    echo $dater->serverDatetime(); // 2013-03-21 09:41:28 (server timezone)
    

    Конвертация даты-времени с учётом часового пояса


    $dater->setServerTimezone('Europe/Moscow');
    $dater->setClientTimezone('Europe/London');
    echo $dater->serverDatetime(); // 2013-03-21 08:18:06
    echo $dater->isoDatetime(); // 2013-03-21 04:18:06
    echo $dater->time(); // 04:18
    

    Стоит упомянуть, что при вызове $dater->setServerTimezone('Europe/Moscow'); функция date() и класс DateTime будут возвращать время в новом установленном часовом поясе. Чтобы это отключить передайте методу false вторым параметром.

    И наконец обещанное


    Код, который позволит вам автоматически определить часовой пояс клиента и выводить для него актуальную дату-время:

    В заголовке глобального скрипта инициализации

    $dater = new Dater\Dater(new Dater\Locale\Ru(), 'Europe/Moscow');
    $timezoneDetector = new Dater\TimezoneDetector();
    $dater->setClientTimezone($timezoneDetector->getClientTimezone());
    
    $dataHandler = new Dater\DataHandler($dater);
    $dataHandler->enableOutputTimezoneHandler();
    $dataHandler->convertRequestDataToServerTimezone();
    

    В основном шаблоне

    <html>
    	<head>
    		<?= $timezoneDetector->getHtmlJsCode() ?>
    	</head>
    </html>
    

    Теперь все строки YYYY-MM-DD HH:MM:SS в отправляемых данных будут заменены на YYYY-MM-DD HH:MM:SS в автоматически определённом часовом поясе клиента. Если же вам нужно выводить дату-время в определённом формате, то достаточно добавить YYYY-MM-DD HH:MM:SS[Н m d] или YYYY-MM-DD HH:MM:SS[date] где date — забинденный в Dater формат. Также можно выводить и форматировать timestamp формат: 1363853607[d.m.Y].

    Например, следующие данные

    <html>
    <body>
    	Timestamp format: 1363238564 (не изменится)
    	Timestamp format: 1363238564[Y/m/d]
    	Timestamp format: 1363238564[datetime]
    	Server datetime format: 2013-03-14 09:22:44[Y/m/d]
    	Server datetime format: 2013-03-14 09:22:44[time]
    	Server datetime format: 2013-03-14 09:22:44
    </body>
    </html>
    

    Будут автоматически конвертированы в

    <html>
    <body>
    	Timestamp format: 1363238564 (не изменится)
    	Timestamp format: 2013/03/14
    	Timestamp format: 14.03.2013 07:22
    	Server datetime format: 2013/03/14
    	Server datetime format: 07:22
    	Server datetime format: 2013-03-14 07:22:44
    </body>
    </html>
    


    В то же время $dataHandler->convertRequestDataToServerTimezone(); сделает так, что все YYYY-MM-DD HH:MM:SS данные поступающие от клиента будут конвертированы в YYYY-MM-DD HH:MM:SS часового пояса сервера. Таким образом сервер никогда не узнает о том, что клиент получает и отправляет дату-время в другом часовом поясе.

    Стоит признать, что это немножко экстремальный вариант обработки часовых поясов. Более универсальным и традиционным решением было бы отказаться от использования $dataHandler->enableOutputTimezoneHandler(); и просто обрамлять вставку каждой даты-времени вызовом соответствующего метода форматирования. Например <?= $dater->date($datetimeOrTimestamp) ?>.

    О проекте


    Честно признаюсь, что являюсь автором этой библиотеки, и буду очень благодарен за любую критику и помощь в доработке. Исходники выложены на GitHub под свободной BSD лицензией, пользуйтесь и распространяйте как пожелаете.

    Надеюсь кому-нибудь таки пригодится :)

    UPD По многочисленным просьбам библиотека была зарефакторена с использованием namespace, в соответствии с PSR-0. Предыдущая версия сохранена в отдельный бранч, и доступна из Composer как "dater/dater":"1.*@dev".
    Поделиться публикацией

    Комментарии 92

      +2
      буквально вчера искал что-то подобное, а тут ваша библиотека. Спасибо :)
        0
        А как вы храните даты в базе данных?
        Конвертируете или всё в unixtimestamp'e храните?
          0
          Я сторонник идеи сохранять и получать данные из базы в одом часовом поясе, так что timestamp для меня никаких особых преимуществ не даёт. Поэтому храню в datetime. timestamp использую только в MySQL(т.к. MySQL на выходе всё равно timestamp в datetime конвертирует) для полей, в которых нужно чтобы он автоматом проставлялся при ON CREATE & ON UPDATE.
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              Да, я поэтому и стараюсь всегда хранить в UTC+0. чтобы не зависеть от настроек сервера или пхп.
              Я вопрос задавал потому что, наличие serverDateTime() показалось немного странным
                0
                А что разве удобней вызывать date('Y-m-d H:i:s') каждый раз прописывая формат, чем просто serverDateTime()? Этот метод введён из соображений практичности, а не функциональности)
                • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    Всё равно не понял. Вот код преобразования из Europe/Minsk в UTC и обратно. Ничего нигде не теряется.

                    $dater = new Dater(new Dater_Locale_Russian(), 'Europe/Moscow');
                    $utcDateTime = $dater->format('2012-08-12 12:00:00', 'server_datetime', 'UTC', 'Europe/Minsk');
                    echo $dater->format($utcDateTime, 'server_datetime', 'Europe/Minsk', 'UTC');

                    Можете привести пример кода в котором что-то куда-то потеряется?
                0
                Формат данных и часовой пояс — разные вещи) Тут спросили про формат данных, datetime или timestamp. То, что часовой пояс UTC наиболее универсален для сервера трудно поспорить :)

                И хранить даты нужно с уканаием пояса. Иначе вы не сможете вернуться в нужный часовой пояс и реальное время, к примеру кода у нас прыгали пояса, или между летним и зимнем временем.

                Не совсем понял. Почему мы не сможем вернуться в нужный часовой пояс? Есть datetime клиента по Europe/Minsk скажем в летний период, приводим её к UTC, потом обратно в Europe/Minsk… и что вы хотите сказать, что в разное время года оно будет отличаться от исходного?
                • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    Так это всё благодаря идиотам-политикам(типа Медведева), которые внезапно летнее время то отменяют, то не отменяют. В реальности же такие изменения в базах часовых поясов крайне редко происходят. И что ради этих крайне маловероятных событий теперь париться datetime в строковом формате хранить или заводить дополнительное поле для timezone? Лично мне это кажется немножко избыточными жертвами.

                    Плюс ко всему изменения утверждённые в гос реестре ещё неизвестно когда будут синхронизированы с базами серверов во всём Мире. Т.е. с уверенность могу сказать, что для большинства PHP серверов Россия переход на лентнее время не отменяла :) Это случится только после того как они в принудительном порядке timezonedb у себя внезапно зачемто обновят.
                      0
                      Для актуального ванильного PHP «Note: This list is based upon the timezone database version 2013.2.» и в минорных релизах обновляется, если изменения были (а они не так уже редки — государств порядка двух сотен и в каждом свои политики — каждое государство раз в сто лет сменит время — раз в полгода обновления базы — не так уж маловероятно, а есть ещё регионы и т. п.). В некоторых дистрах идет привязка к системному timezonedb, обновления которому прилетают из репов дистра. В нормальных дистрах обновление прилетает заблаговременно, через несколько дней после принятия закона или постановления. Два года никто не обновлялся?

                      В общем хранить датувремя нужно или с указанием пояса, или приводить его к какому-то фиксированному для приложения (не для сервера!), обычно UTC, причем не захардкодив где-то в глубине кода строку «UTC», а вынести как константу или элемент конфига со зловещим комментарием «Не менять, если не хотите трудноуловимых глюков и/или конвертировать всю базу!!!», а если разрешить смену в админке, то конвертировать все даты в новый пояс.
                        0
                        И всё равно я не пойму: если мы приводим дату-время к UTC в момент получения этой даты, то даже если в timezonedb обратная конвертация изменится, то мы всё равно получим оригинальное время. Например:

                        до 2008
                        Europe/Moscow: +4 летом

                        после 2008
                        Europe/Moscow: +3 летом

                        И вот к примеру летом 2007-го мы получаем datetime по UTC +4, к примеру 14:00, при водим к UTC получаем 10:00.
                        Потом в 2009 году обновляем timezonedb и приводим обратно эту дату-время к Europe/Moscow. И получаем уже не 14:00, а 13:00. И вы считаете это ошибкой?

                        На мой взгляд это очень даже правильно. Т.к. хоть для одной страны мы и получили разницу в смещении(хотя не факт что не оправданную, раз они в timezonedb изменения внесли), но для всех других стран эта метка времени осталась актуальной. А если придерживаться вашей идеии хранить название оригинальногого timezone, то потом приводя эту дату к другим часовым вы будете получать искажённое значение. К примеру будете 14:00 приводить к UTC после 2008-го и получите не 10:00, а 11:00, и так для всех других часовых поясов.

                        В итоге дилемма такая: или вы ломаете дату для одного пояса, или ломаете её для всех остальных поясов. Логика подсказывает, что большую погрешность в актуальности даты даёт как раз ваш, последний случай.

                        Кстати, в пользу хранения даты-времени у универсальном часовом поясе даже специальный тип данных ввели — timestamp называется.
                          0
                          timezonedb хранит не только текущие значения для разных часовых поясов, но и всю историю. 1 августа 2007 14:00 MSK мы приводим 1 августа 2007 10:00 UTC по любой базе после 2007, а обратное преобразование по другой любой базе после 2007, хоть за 2025, даст нам то же время. При этом, на любой базе после 2009 1 августа 2009 10:00 UTC приведется к 1 августа 2009 13:00 MSK. Внешняя нелогичность может быть, если мы к дате 1 августа 2007 14:00 MSK прибавим 2 года и получим 1 августа 2007 13:00 MSK. Но по UTC/unixtime это будет именно плюс 60*60*24*(365*2 + 1) секунд (с учетом, что 2008-й — високосный). Дилеммы нет. Ошибок нет. Правда при условии, что мы считаем время физическими единицами, а не юридическими — со всеми этими свистоплясками на timezonedb полагаться нельзя если меняется дата при преобразованиях. Грубо говоря, если нас интересует датавремя 1 августа 2007 00:00 MSK + 2 года, то по российским законам 2 года истекут 2 августа 2009 00:00 MSK в общем случае, а если положимся на timezonedb, то 1 августа 2009 23:00 MSK. Это следует иметь в виду при проектировании приложений в которых сроки имеют юридическую значимость (или при даче показаний или ответов на запросы, типа пришел запрос из прокуратуры или СК «предоставьте логи заходов с такого-то IP за последние три года»). Юридические сроки нужно считать не в физических единицах через timezonedb, а в юридических согласно законодательству конкретной страны, зачастую учитывающих не только законы, но и график работы конкретных учреждений. Особенно если любишь оттягивать дедлайн до последнего :) Или наезжать, если дедлайн хоть на секунду задержали :(
                            0
                            > timezonedb хранит не только текущие значения для разных часовых поясов, но и всю историю

                            Я вот тоже первым делом на это понадеился, но перепроверив понял, что это не так:

                            echo $dater->format('2001-07-07 12:00:00', 'time', 'UTC', 'Europe/Moscow') . PHP_EOL;
                            echo $dater->format('2012-07-07 12:00:00', 'time', 'UTC', 'Europe/Moscow') . PHP_EOL;
                            


                            Всегда выводит 11:00. При том, что timezone_version_get() возвращает 2011.8, т.е. вполне актуальная.
                            Можете привести пример подтверждающий ваши слова?
                              0
                              $date = new DateTime('2000-01-01', new DateTimeZone('Europe/Moscow'));
                              echo $date->format('Y-m-d H:i:sP') . PHP_EOL; // 2000-01-01 00:00:00+03:00
                              $date->setTimeZone(new DateTimeZone('UTC'));
                              echo $date->format('Y-m-d H:i:sP') . PHP_EOL; // 1999-12-31 21:00:00+00:00
                              $date->modify('+ 13 year'); 
                              echo $date->format('Y-m-d H:i:sP') . PHP_EOL; // 2012-12-31 21:00:00+00:00
                              $date->setTimeZone(new DateTimeZone('Europe/Moscow'));
                              echo $date->format('Y-m-d H:i:sP') . PHP_EOL; // 2013-01-01 01:00:00+04:00
                              

                              Как видите, «ниоткуда» час появился, а PHP «знает», что зимой 2000-го у нас было +3, а это зимой — +4.
                                0
                                Отлично, timezonedb хранит историю изменений — это здорово. И это в очередной раз доказывает, что нет необходимости хранить даты в оригинальном timezone с timezone меткой — это ничего не даёт.

                                $date = new DateTime('2000-01-01 12:00:00', new DateTimeZone('Europe/Moscow'));
                                echo $date->format('Y-m-d H:i:s P') . PHP_EOL; // 2000-01-01 12:00:00 +03:00
                                $date->setTimeZone(new DateTimeZone('UTC'));
                                echo $date->format('Y-m-d H:i:s P') . PHP_EOL; // 2000-01-01 09:00:00 +00:00
                                $date->setTimeZone(new DateTimeZone('Europe/Moscow'));
                                echo $date->format('Y-m-d H:i:s P') . PHP_EOL; // 2000-01-01 12:00:00 +03:00
                                


                                Как видите при конвертации Msk-UTC-Msk ничего не теряется. У вас время теряется только после того как вы 13 лет прибавляете. Не понятно только зачем вы это делаете и почему думаете, что время в 2000-ом должно быть эквивалентно времени в 2013, в то время как правила его исчисления изменились?
                                  0
                                  Делаю это чтобы исчислять сроки. Скажем, для задач типа GTD — поставили начальную датувремя проекта как now(), поставили «срок — неделя». В зависимости от формата хранения могут быть три варианта: аларм выскочит ровно через неделю (60*60*24*7 секунд), может на час раньше, а может на час позже. Последние два варианта по местному времени, если переход летнее/зимнее произошел. Получаем, что результат работы программы зависит от формата хранения данных и нужно определиться какой формат хранения нам нужен. И это мы только с UTC и MSK разбирались. А если нам нужно между EET (Europa/Kiev) и, скажем, бывшим PETT (Asia/Kamchatka, MSK+9), ныне объединенным с MAGT (Asia/Magadan, MSK+8), разбираться, то вообще не уверен, что timezone функции справятся. А для меня, по идее, это область практических интересов, и там, и там родные живут.
                                    0
                                    А не проще ли было для одного такого случая модификации даты-времени с сохранением попраки на оригинальный часовой пояс просто реализовать метод модификации, который бы автоматически корректировал время на разницу в timezonedb нужного часового пояса?

                                    Там ведь делов то на несколько строчек :) И не пришлось бы с заморачиваться с хранением timezone каждой даты.
                                      +1
                                      На моем основном проекте (MMO игра) сейчас люди из разных стран (минимум три — Россия, причем не только московское время, Украина и Германия), им постоянно нужно показывать время событий (в том числе и будущих) друг друга в их местном времени. Сейчас там костыль на костыле, саппорт советует для Москвы выставлять часовой пояс, кажется, Стамбула, чтобы более-менее нормально работало. Я вот хочу успеть к последнему воскресению марта сделать безкостыльную систему :)
                                        0
                                        Смещения даты-времени без учёта часового пояса — это уже костыль :) А вот реализация смещения без учёта поправки на часовой пояс — это вполне себе простенькое и прозрачное решение, которое вы по коду, по идее, лишь в нескольких местах будете использовать.
                                0
                                Заинтересовал ваш пример, поигрался, и вообще вот что получил:
                                $date = new DateTime('2000-01-01', new DateTimeZone('Europe/Moscow'));
                                echo $date->format('Y-m-d H:i:sP') . PHP_EOL; // 2000-01-01 00:00:00+03:00
                                $date->setTimeZone(new DateTimeZone('UTC'));
                                echo $date->format('Y-m-d H:i:sP') . PHP_EOL; // 1999-12-31 21:00:00+00:00
                                $date->setTimeZone(new DateTimeZone('Europe/Moscow'));
                                $date->modify('+ 13 year');
                                echo $date->format('Y-m-d H:i:sP') . PHP_EOL; // 2013-01-01 00:00:00+04:00
                                $date->setTimeZone(new DateTimeZone('UTC'));
                                echo $date->format('Y-m-d H:i:sP') . PHP_EOL; // 2012-12-31 20:00:00+00:00
                                


                                То есть модификаторы типа '+ 13 year' и, видимо, DateInterval с DateTime работают с учетом текущего часового пояса. Приводим к UTC — работаем с физическим временем 9аналогично timestamp), приводим к «политическому» поясу — работаем с работаем с местным временем. Как-то я эту фишку не просек, когда экспериментировал. Потому вывод: хранить исключительно в формате с часовым поясом, причем не в формате «Y-m-d H:i:s P», а в формате «Y-m-d H:i:s T» или «Y-m-d H:i:s e». С мускулом, правда, нужны отдельные эксперименты, насчет того, как он воспринимает таймзоны для полей типа DATETIME. Беглое чтение доков показало, что адекватного восприятия им строк нужно регулярно конвертитровать timezonedb в таблицу в базе mysql. Но хранит ли он таймзону в самом поле или конвертирует её к какой-то нужно разбираться.
                                  0
                                  Это всё имеет смысл только при том, что у вас частенько возникают конвертации типа +13 years и вы хотите в таких конвертациях сохранять неизменность времени. И мало того, что это спорный вопрос насколько правильно сохранять время при конвертации в другие timezenedb правила, так ещё и сам кейс крайне крайне редкий.

                                  Лично я не готов был бы пойти на заморочки с отдельным хранение timezone для каждой даты-времени лишь ради такого сомнительного выигрыша как сохранении времени в таких крайне редких случаях как +13 years.
                                    0
                                    Простой кейс — поиск записей за последний год/месяц/неделю/день на ресурсе типа форума или блога с хотя бы российской аудиторией. Имхо, просто непрофессионально сдавать заказчику (пускай и внутреннему типа начальника отдела или менеджера проекта), если не можешь описать её поведение (а оно зависит от формата хранения) в экстремальных условиях типа Путина/Медведева. Когда я вижу в ТЗ записи типа «за последнюю неделю» я сразу задаю уточняющие вопросы типа «что считать неделей, если у пользователя часовой пояс изменился, причем не просто +3 или +4 к MSK, а с MSK+4 к MSK на UTC-7:30 DST».
                                      0
                                      См. habrahabr.ru/post/173693/#comment_6036635 Можно просто реализовать метод, который будет возвращать дату-время с игнорированием timezonedb изменений.
                                        0
                                        Что значит «с игнорированием измененияй»? Что принимаем за основу, после которой игнорируем? И почему имеено эта основа?
                                          0
                                          Ваша проблема с потерей времени при конвертации msk-utc-msk в этом примере habrahabr.ru/post/173693/#comment_6036409 заключается в том, что вы делаете modify +13 years над utc, а не над msk. Т.е. в вашем случае правильней было бы реализовать modify, который работал бы перед вызовом modify приводил время к нужному поясу. Например:

                                          $date = new DateTime('2000-01-01', new DateTimeZone('Europe/Moscow'));
                                          echo $date->format('Y-m-d H:i:sP') . PHP_EOL; // 2000-01-01 00:00:00+03:00
                                          $date->setTimeZone(new DateTimeZone('UTC'));
                                          echo $date->format('Y-m-d H:i:sP') . PHP_EOL; // 1999-12-31 21:00:00+00:00
                                          $date->setTimeZone(new DateTimeZone('Europe/Moscow'));
                                          $date->modify('+ 13 year');
                                          echo $date->format('Y-m-d H:i:sP') . PHP_EOL; // 2013-01-01 00:00:00+04:00
                                          


                                          И используйте его на здоровье там, где вам нужно модифицировать дату с сохранением времени. Всё ведь просто, и не нужно timezone для каждой даты хранить.
                                            0
                                            Да собственно это не проблема. Просто нужно знать нюансы.
                +3
                Из пожеланий — composer, ну и уже namespace, пора уже…
                  +1
                  Всё будет!
                    0
                    ИМХО — Сейчас публиковать библиотеки для PHP без пакета композера и PSR-0 уже плохой тон.

                    Но, в любом случае, спасибо за библиотеку, как минимум попробую поюзать :)
                      0
                      Composer есть ведь. Устанавливайте «dater/dater» => «dev-master» А psr-o будет как только на неймспейсы зарефакторю.
                    0
                    Готово. Тестируем первый release candidate.
                    +1
                    Храню время в unix timestamp, в начале скрипта выставляю часовой пояс по умолчанию (каждый пользователь может иметь свой).
                    Форматы дат хранятся в языковых файлах, для каждой локали свой набор форматов краткой, полной даты и т.д…
                    Просто, прозрачно, применяется ко всем операциям с датами в любом месте скрипта.
                    Для себя решил, что такой подход наиболее удобен.
                    Работа с несколькими локалями и часовыми поясами — для очень узкой ниши специализированных проектов.
                      0
                      Использую такой же подход. Никаких проблем не наблюдается.
                      0
                      А вы не рассматривали возможности определять время клиента по заголовкам HTTP без этого шаманства с JS?
                        0
                        А что существуют такие заголовки?) Расскажите? Очень интересно)
                          0
                          =) Если бы я знал, я бы так и написал. А я как раз и спрашивал, исследовали ли вы этот вопрос, может, есть что-то.
                            0
                            Нет такого :)
                              0
                              Я ж без наездов, правда, интересно было, а искать — лень =) Сейчас вот тоже поискал, таки нет ничего похожего.
                            0
                              +2
                              Таки в одной России 9 часовых поясов :D С вероятностью 1 к 9 часовой пояс определять?))
                                0
                                Ну да. Тут я промазал.Однако
                                  0
                                  Ну да. Тут я промазал.
                                  Однако можно определить географическое положение пользователя по его IP-адресу. В сумме, все эти «догадки» (geoip, accept-language, etc) могут дать вполне достоверную информацию о таймзоне. Не ручаюсь за степень достоверности.
                          +1
                          Спасибо, интересно.

                          А зачем классы Dater_Locale_*? Не лучше ли стандартными строками типа ru_UA или en_US (и в идеале ещё и кодировку корректно обрабатывать типа ru_RU.UTF-8) инициализировать конструктор Dater_Locale сделав его не абстрактным, а локали хранить или в статическом массиве этого класса, или в «конфигах», или заполнять динамически с помощью setlocale/strftime, ну или уж если так нравится, то сделать фабричный статический метод в Dater_Locale, который будет внутри нужный конструктор вызывать?

                          Главная фишка в чем со строками — для локалей задаваемых пользователем или детектируемых не нужно будет лепить большой switch или костыли типа new 'Dater_Locale_'.$locale;
                            0
                            Добавил статический метод Dater::getLocaleByCode('ru');
                              0
                              Лучше Dater_Locale::getByCode('ru'); по-моему. И маленький пулл-реквест ещё отправил. Было интересно функциональность гитхаба редактирования онлайн проверить :)
                                0
                                > Лучше Dater_Locale::getByCode('ru');

                                Не могу согласиться. Всё-таки Dater_Locale это класс сущности локали, а не агрегатор какой-то функциональности. А то так будет странно выглядеть Dater_Localer_English::getByCode('ru'). Ну и возможность переопределить список локалей в под-классе сущности выглядит странно. Pull реквест получил, спасибо :) но вынужден отклонить.
                                  0
                                  Ну, Dater_Locale_English::getByCode('ru') это будет нецелевое использование классов-наследников Dater_Locale :) Для исключения переопределения можно (и нужно) final использовать. В принципе можно и исключение бросать если getByCode вызывается из Dater_Locale_English, сравнивая self и static (LSB). А в идеале отдельный класс Dater_Locale_Fabric сделать, пускай и с одним (пока) методом. А так вы нарушаете принцип единственной ответственности и усиливаете связанность — класс Dater у вас теперь отвечает за то, какой из подклассов Dater_Locale создавать и статически с ними связан, причем так, что поиск по имени класса эту связь не покажет, IDE вряд ли её обнаружит при попытке переименования классов Dater_Locale_* и т. п. Не лучше ли такую связь вынести в более очевидное место?

                                  Может реализация у меня костыльная, но над предложением чтобы фабричный метод принимал не только «ru», но и «ru_RU», и «ru_UA» и переадресовывал на «ru» раз точных описаний найти не может, советую подумать — это общепринятая техника работы с локалями, даже MS её перетащила из никсов в Висте :). А заодно переименовать классы типа Dater_Localer_English в Dater_Localer_En.
                                    0
                                    Я ведь не про переопределение говорил, а про вызов Dater_Locale_English::getByCode('ru'). Лично мне не нравится идея выносить в класс сущности фабричный метод её инициализации. Заводить отдельный класс-фабрику для мапинга кодов локалей на классы я специально не стал, хочется оставить библиотеку в лаконичном виде. Dater::$localeCodes поэтому публичным оставил, кому надо — добавят своих классов локалей и впишут их в Dater::$localeCodes.

                                    По поводу предложения ru_RU я комментом к пул реквесту ответил: форматов может быть сотни, преобразуются они элементарно, не вижу смысла заграмождать Dater этимим преобразованиями, кому надо сами преобразуют перед вызовом getLocaleByCode.

                                    Тем не менее спасибо, что проявили участие! :) Для меня это очень важно.
                                      0
                                      Пожалуйста :)
                                      кому надо сами преобразуют перед вызовом getLocaleByCode.

                                      У многих строка локали уже есть в формате BTC 47, хотя бы потому что это общепринятый стандарт, не нужно думать о том как закодировать американский и британский английские. Зачем ограничивать совместимость со стандартом, тем более что даже строчки не добавляется? Или не хотите, чтобы сейчас кто-то ввел «russian» или «english», оно у него работало, а потом отвалилось из-за того что перешли на формат \.._..\ с \...*\?
                                        0
                                        Вот вам нравится этот стандарт, кому-то другому нравится трёхбуквенный стандарт RUS, ENG. Таки я не хочу вступать в полемику какой стандарт лучше. Каждый из прочих других стандартов элементарным образом приводится к двух-буквенному.
                                          0
                                          Мне этот стандарт не нравится, но он общепринят на уровне самых распространенных ОС, хоть на десктопах (Windows, MacOS, Linux), хоть на серверах (Linux, *BSD, Windows). И он описывает именно локали, а не языки. Он может описать различия между русским российским («на Украину») и украинским («в Украину), английским американским (»I will") и британским («I shall»).
                                            0
                                            А вот этого я не учёл. Спасибо вам. Скоро зарефакторю на новый стандарт.
                                              0
                                              На самом деле под разными ОС локаль прописывается в разном формате: ru_RU, RUS_RUS, Russian_Russia — 3 разных варианта. В общем я зарефакторил всё на работу в двухсимпольном формате, без привязки к регистру, и без подчёркивания. Теперь работает как-то так $dater->getLocaleByCode('en', 'uk'); второй аргумент опционален.
                              0
                              __autoload.php

                              Composer умеет сам работать с автозагрузкой: getcomposer.org/doc/04-schema.md#autoload
                                –1
                                Это я знаю, но все классы перечислять не хочу, а под psr-0 пока не зарефакторил. Так или иначе __autoload.php нужен т.к. из общего числа пользователей Composer процентов 3-5 используют.
                                  0
                                  Там можно classMap использовать или files
                                  +1
                                  А так? :)
                                      "autoload": {
                                          "psr-0" : {
                                              "Dater_" : "Dater/"
                                          }
                                      }
                                  


                                  PS Лучше «Dater/» => «src/Dater/».
                                    0
                                    У меня вот жёсткая дилемма по этому поводу: если переходить на неймспейсы, то придётся переименовывать Dater в \Dater\Dater. Вы считаете нормальным такие именования?
                                      0
                                      Liaren\Dater — вполне обычная практика :)
                                        0
                                        Это то понятно)) Но неймспейс у библиотеки должен быть не Liaren, а Dater. Отсюда возникает вопрос: как назвать основной класс? А то ведь new \Dater\Dater правда странно смотреться будет. А вот переименовать его в какой-нить Formatter и т.п. рука не поднимается
                                          0
                                          Dater\Dater вполне подходит. Не нужно писать new \Dater\Dater;
                                          Вместо этого используйте use. К тому же ide сама добавляет нехватающие use в начало файла (напримерв PhpStorm напишите new Dater; нажмите Alt + Enter)
                                            0
                                            Имел в виду нэймспэйс Liaren\Dater, полное имя класса \Liaren\Dater\Dater Опять же обычная практика.
                                              0
                                              Честно говоря ниразу не видел код, в которым бы в качестве базового namespace имя автора использовалось. Искал на гитхабе примеры организации именований неймспейсов в более ли менее солидных библиотеках, так вот нашёл единственный случай именования по аналогии с Dater/Dater — github.com/ircmaxell/PHP-PasswordLib/blob/master/lib/PasswordLib/PasswordLib.php больше такого нигде не видел.
                                                0
                                                Первая (значащая) строка PSR-0
                                                \<Vendor Name>\(<Namespace>\)*<Class Name>
                                                
                                                . Вы же вендор :)
                                                  0
                                                  Под Vendor name в данном случае понимается название библиотеки, а не класса :) fabpot ведь не пихал свой фреймворк Silex в namespace /Fabpot/Silex github.com/fabpot/Silex, да и вообще так никто не делает. Всегда используют название библиотеки как корневой namespace.
                                                    0
                                                    Не всегда. Zend так точно делает :) Фабиен тоже месяц назад писал SensioLabs\Connect\Security\Authentication\Provider;
                                                      0
                                                      У Зенда название продукта совпадает с названием бренда, тут без вариантов)) А SensioLabs просто нормального названия продукту не придумали, вот и решили вставить себя в корень, заодно и попиариться. Вы посмотрите сотни прочих более ли менее современных библиотек, так больше никто особо не делает.

                                                      А что если библиотеку форкать будут, всем потом название корневого неймспейса вручную менять?) А так есть базовая версия Dater, она же на packagist.org под dater/dater опубликована. Кто будет форкать — пусть публикует под myname/dater, но имхо конечные пользователи не должны париться каждый раз namespace у себя перепрописывать только из-за того, что переключились на использование Dater, который был форкнут и доработан другим автором.
                                                          0
                                                          Это что по вашему Yaml — это чей-то ник?)
                                                            0
                                                            Это отдельный компонент Symfony, который не имеет зависимостей от самого фреймоворка, поэтому может выступать как отдельный пакет для чего угодно.

                                                            Symfony — это имя вендора. Можете не использовать свой ник. Можете взять любое название, под которым будете выпускать свои пакеты.
                                                              0
                                                              Вы путаете название компонента и название вендора. Vendor — поставщик, т.е. название автора. В случае с Symfony название вендора должно было быть SensioLabs т.к. по сути это их продукт, однако где вы видели в исходниках Symfony namespace SensioLabs? В нормальной практике никто так не делает, это крайне неудобно.
                                                                0
                                                                В нормальной практике никто так не делает, это крайне неудобно.

                                                                Смотря как посмотреть. Уникальный Vendor_name сильно уменьшает вероятность конфликта имен. И позаботиться о глобальной уникальности только один раз нужно, а дальше только о внутренней.
                                                                  0
                                                                  Мне кажется лучше пусть разработчик компонента 1 раз позаботиться об уникальности названия своего детища(гугл в помощь), чем пользователи этого компонента потом будут каждый раз в неймспесах его ник прописывать, а потом ещё париться с автозаменой неймспейса, если решат переключиться на альтернативный форк от другого разработчика.
                                                                  0
                                                                  Да, вы правы… Но суть в том, что у вашего корневого класса должно быть как минимум два неймспейса: ). Можно и так Dater\Dater\Dater :^ )
                                                                    0
                                                                    А чем обусловлено это ваше «должно»?)
                                                                      0
                                                                      Про PSR-0 Вам уже выше сказали. Только это.
                                                                        0
                                                                        В таком случае в жопу PSR-0 :)
                                                                          0
                                                                          А чем Вы руководствуетесь?
                                                                            0
                                                                            Тем, что в этом нет абсолютно никакой практической составляющей. Кстати, вы что-то напутали с PSR-0, там не обязательно namespace Dater\Dater использовать, можно просто Dater. См. habrahabr.ru/post/173693/?reply_to=6047733#comment_6044785 там (<Namespace>\)* как бы намекатает, что под-неймспейсов должно быть от 0 и более, а не от 1 и более.

                                                                            Так что всё ок с PSR-0, это вы меня напутали)
                                                                              0
                                                                              Действительно, напутал… Сам вот недавно пакет делал и там vendor не vendor, и корневой namespace один.
                                                              +1
                                                              Это что по вашему Yaml — чей-то ник?)
                                                0
                                                Liaren\Dater\Dater :^ )
                                              0
                                              Готово. Тестируем первый release candidate.
                                          0
                                          Очень интересная библиотека. У самого есть что-то похожее, но значительно беднее. Теперь вот думаю: стоит ли свое развивать, раз уже готовое есть? )

                                          Есть предложение: ввести еще функцию, которая будет возвращать шаблон даты на основе текущей локали. Например так:

                                          $dater->setLocale(new Dater\Locale\En()); echo $dater->getDatePattern(); // 'mm/dd/yyyy' $dater->setLocale(Dater\Dater::getLocaleByCode('ru')); echo $dater->getDatePattern(); // 'dd.mm.yyyy'

                                          Очень удобно для инициализации яваскриптовых библиотек, чтобы уж везде на страницах сайта дата единообразно фомировалась
                                            0
                                            Добавил, см. github.com/barbushin/dater/commit/8546fb9ffc2776a11cac0fa8af8465e47f0ff989

                                            Вообще, было бы у меня больше свободного времени, я бы реализовал метод, который бы возвращал js код с функциями форматирования даты на клиенте.
                                              0
                                              Отлично!

                                              А метод с js-кодом, наверное, вряд ли так уж необходим. Нормальные библиотеки для манипуляции с календарями, например, позволяют задавать формат вывода даты при инициализации, и добавленных функций достаточно. А на все случаи жизни все равно методов не напасешься.
                                                0
                                                А как быть с этим js, ведь сначало исполняется php, а потом уже клиент, соответственно когда человек заходит на сайт первый раз, то php ещё не знает о часовом поясе. Как это можно обойти?
                                                  0
                                                  Не совсем так. JS при первой загрузке, после установки куки с правильной timezone сразу перезагружает страницу, см. github.com/barbushin/dater/blob/master/src/Dater/TimezoneDetector.php#L50
                                                    0
                                                    А, понял, сразу не заметил. Просто у меня периодически выскакивает ошибка в логах.

                                                    date_default_timezone_set(): Timezone ID '' is invalid in /public_html/library/Controllers/ControllerBase.php on line 19


                                                    Не знаю как это исправить

                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                            Самое читаемое