Прежде всего хотел бы поблагодарить за более, чем 80 звёзд на GitHub, которые мне дали читатели Хабра по результатам предыдущего поста. И это несмотря на то, что репозиторий был почти пустой, а ссылка была неочевидна. На лицо полезность этого пакета!


Для тех, кто пропустил первый пост, маленькое повторение. Если у Вас в приложении есть что-то вроде:



Или что-то такое (ВК вообще не смог перевести Южный Мельбурн):



То встречайте (стучат барабаны) – библиотека Географ доступна в PHP-версии. В данной статье я покажу на примере собственного сайта плюсы перехода на новый пакет. Собственно, так и пришла мысль создать библиотеку – я заметил, что начинаю частенько повторять один и тот же функционал в разных приложениях, а повторять сегодня в мире разработчиков – ну просто как-то немодно.


Установка


Установить пакет можно одной командой, так как он опубликован в Packagist:


composer require menarasolutions/geographer

Никаких зависимостей нет – это является одним из главных принципов разработки на текущий момент. Не хочется обязывать пользователей пакета устанавливать дополнительное ПО или другие пакеты. Тем не менее, планируется добавить опциональные интеграции – Memcached, MongoDB.


Пример 1: простой список стран


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


Как было в моём приложении:


    public static function getCountryNameByCode($countryCode, $language) 
    {
            return Config::get('texts.countries')[$language][$countryCode];
    }

Тут достаточно всё банально – класс-фасад Config даёт доступ к массивам, указанным в текстовых файлах, а далее мы по ключам языка и кода страны получаем необходимый перевод. Проще некуда, точно также делали, наверняка, многие.


Минусы у такого подхода:
– Было необходимо держать эти переводы внутри своего приложения, а прямого отношения к бизнес-логике они не имеют; – В начале все переводы необходимо добавлять вручную. Я не могу просто взять и начать работать с новым языком;
– Читать код возможно, но он не слишком интуитивный.


При переходе на библиотеку-географ стало:


    public static function getCountryNameByCode($countryCode, $language) 
    {
        return Geographer::findOneByCode($countryCode)
            ->setLanguage($language)
            ->getName();
    }

Обретённые плюсы:
– Теперь переводы находятся вне приложения и время от времени они сами обновляются и улучшаются;
– Доступны многие популярные языки сразу "из коробки";
– Код стал более интуитивным, простым к прочтению;
– Есть возможность бросать подходящий exception на конкретной стадии – не найдена страна, не найден язык.


Пример 2: название пункта в правильной форме


А вот это уже сложнее, и здесь плюсы перехода на отдельную библиотеку намного заметнее. У меня на сайте есть страницы с подобными ссылками:



Или такими SEO-оптимизированными замечаниями:



Самое простое, банальное решение – добавить ещё несколько массивов или справочников в наше приложение, на каждую форму слова. Таким образом, у нас уже сотни или тысячи переводов появятся, и многие из них придётся добавлять или править вручную – большинство каталогов вроде Geonames не предоставляют склонений.


Может получится что-то вроде:


    public static function getCountryNameByCode($countryCode, $language, $form = 'default') 
    {
            return Config::get('texts.countries')[$language][$countryCode][$form];
    }

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


Но и это ещё не всё – большинство из нас используют на сайтах шаблоны и текстовые файлы, и возникнет вопрос, где хранить предлог – в справочнике стран (или городов) или в строке-шаблоне. То есть, иметь шаблон вроде "События в: город" или "События: город". В первом случае возникнут нюансы с названиями, которые требуют отличных предлогов, вроде "во Франции". Во втором, будет огромное количество повторений в словарях, либо дополнительная логика в коде.


В случае использования моей библиотеки:


    public static function getCountryNameByCode($countryCode, $language, $form = 'default') 
    {
        return Geographer::findOneByCode($countryCode)
            ->inflict($form)
            ->setLanguage($language)
            ->getName();
    }

Предлоги можно включать и отключать методами includePrepositions() и excludePrepositions(), что позволяет использовать библиотеку в любых шаблонах. Думать о том, какой предлог правильный не надо. Заботиться о том, как текущий язык склоняет имена стран и меняются ли от этого предлоги – не надо.


Краткий обзор API


Методы на коллекциях


Массивы подразделений (стран, областей или городов) реализованы через популярные сегодня коллекции – умные массивы, поддерживающие Fluent API:


$states->sortBy('name'); // Отсортировать области по имени
$states->setLanguage('ru')->sortBy('name'); // По русским именам
$states->find(['code' => 472039]); // Найти все совпадения по параметрам
$states->findOne(['code' => 472039]); // Вернуть только первое совпадение
$states->findOneByCode(472039); // Волшебный метод для удобства

Общие методы


Все классы подразделений являются потомками одного класса и имеют общие методы:


$object->toArray(); // Вернуть в виде обычного массива
$object->parent(); // Вернуть родителя (город вернёт область, штат вернёт страну)
$object->getCode(); // Уникальный ID 
$object->getShortName(); // Стандартное для языка название
$object->getLongName(); // Официальное, государственное название

Все данные о подразделении можно получать разными способами:


$object->getName(); // Через метод (при необходимости будет склонено)
$object->name; // Тоже самое
$object['name']; // Можно и как массив
$object->toArray()['name']; // Можно вытащить из примитивного массива

Класс-планета


$earth->getAfrica(); // Страны Ффрики
$earth->getEurope(); // Европейские страны
$earth->getNorthAmerica(); // Северная Америка и так далее
$earth->getSouthAmerica(); 
$earth->getAsia();
$earth->getOceania();

$earth->getCountries(); // Все страны мира
$earth->withoutMicro(); // Только страны с населением от 100,000

Связь между библиотекой и приложением


Если мы вынесем все данные о географических единицах в отдельную библиотеку, то мы сможем смело почистить свои массивы (или базу данных, или что-то ещё), но нам всё-таки надо как-то фиксировать связь между конкретным городом (или страной или областью) записи в нашей БД с записью в библиотеке.


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


На текущий момент страны имеют коды ISO 3611-2, ISO 3611-3 и Geonames. Области имеют коды ISO 3166, FIPS и Geonames. Города имеют только коды Geonames – это самое негибкое место.


Таким образом, чтобы вывести на сайте, скажем, город пользователя, мы можем хранить geonames_id в таблице пользователей, а по нему восстанавливать объект:


$city = City::build($geonames_id);

Большинство современных фреймворков смогут делать такое преобразование даже автоматически. Я специально выбрал различные международные системы идентификации – разработчик и его приложения не должны быть привязаны к библиотеке Географ. От неё отказаться должно быть также просто, как и начать ей пользоваться.


Покрытие на сегодня


В базе имеются все города мира с населением выше 50 тысяч человек, все области и страны.


Каждая страна имеет данные:


  • идентификаторы ISO 3611-2 и 3611-3, Geonames;
  • размер территории;
  • национальная валюта;
  • телефонный код;
  • население;
  • континент;
  • официальный язык;
  • различные формы названия страны.

Города и области имеют названия и уникальные идентификаторы.


Названия переведены на языки: русский, английский, испанский, итальянский, французский, китайский (путунхуа).
Для стран это 100% покрытие, для областей и городов – меньше, но постоянно дополняется. Для непереводимых городов предлагается добавить возможность простой транслитерации.


Все страны правильно склоняются – проверено через онлайн-словари орфографии.


Планы на будущее


  1. Планируется добавить примитивный гео-индекс, чтобы по координатам можно было получить ближайший населенный пункт.


  2. Разные языки, скорее всего, будут разнесены в отдельные репозитории, чтобы разработчику не было необходимости скачивать ненужные JSON-справочники. Более того, JSON-справочники станут независимы от библиотек-клиентов – на них можно будет завязать будущие клиенты Python и Ruby.

Миссия простая – стать стандартной гео-библиотекой веб-разработчиков. При достижении достаточной популярности, можно ожидать от пользователей разных стран внесения поправок в переводы через pull-запросы – справочники будут сами постоянно улучшаться, подобно wiki.


Буду очень рад услышать замечания и пожелания к API!

Поделиться с друзьями
-->

Комментарии (42)


  1. zenn
    02.07.2016 12:05
    +1

    В принципе, вполне годная для использования библиотека, код вполне качественный, да и использовать удобно. Но возникло, как говориться одно «но»:

    "php": ">=5.6.0"
    

    да, php 5.5 сейчас уже вышел из «active support» и перешел в «security updates only», но некоторые твердолобые до сих пор его используют и объяснить им что срочно необходимо обновляться до 5.6/7.0 проблематично. Есть ли возможность ввести поддержку 5.5?
    Еще 1 вопрос о поддержке laravel/lumen — может стоит оформить отдельным пакетом, а базовый выделить как «standalone» и его уже использовать в require для пакета laravel/lumen?


    1. dusterio
      02.07.2016 12:18

      Ох, я сходу уже не помню – там что-то требовало 5.6. У меня изначально стояло 5.5, пока не наткнулся :) Я проверю в общем!

      Про Laravel/Lumen — да, вероятно вы правы. Кроме того, там настолько простая интеграция — её чуть ли не проще в документации описать :)


      1. zenn
        02.07.2016 12:31

        Я смотрел, правда очень бегло, но не нашел массового использования основных «фишек» 5.6 — плавающего кол-ва параметров аргументов фу-ии или использования «default», отлова «несуществующих» каллбэков тоже вроде не заметил через try-catch над object, deprecated function вам вовсе рано использовать, так что вам думаю видней в чем там загвоздка была.


        1. dusterio
          02.07.2016 12:33

          Вспомнил — у меня тесты все в Travis запускаются. И изначально там 5.5 стояло в том числе — оно фейлилось как раз. Сейчас верну в Travis 5.5 и увижу :)


        1. dusterio
          02.07.2016 12:40

          Нашел — это PhpUnit 5-ый не поддерживает 5.5. Сейчас попробую в 'require' сделать 5.5, а в 'require-dev' 5.6.

          В общем, я уже закомиттил и пометил


        1. dusterio
          02.07.2016 16:35

          Нашлась-таки одна мелочь, не поддерживаемая в 5.5 – в static properties нельзя было использовать оператор конкатенации (.).

          Теперь проходят тесты на 5.5


          1. zenn
            02.07.2016 16:53

            Да, видел, вы это, из теглайна и релизов удалите старый с «support php 5.5» и создайте его снова, чтобы в 'stable' нормально стягивало.


            1. dusterio
              02.07.2016 16:58

              Ага, готово


  1. ITMatika
    02.07.2016 16:35

    Вот бы ещё интеграцию с КЛАДР/ФИАС! Но боюсь, что проще будет запилить отдельную библиотеку…


    1. dusterio
      02.07.2016 16:36

      Подозреваю, что это настолько массивная задача, что должна быть Git под-модулем. Вообще, всё, что не глобальное — должно быть опциональным, а не входить в основной пакет


      1. Delphinum
        02.07.2016 17:51
        +3

        Достаточно в вашем пакете выделить логику чтения и преобразования исходных данных о странах/регионах/городах в интерфейсы, и пользователи сами припишут нужные адаптеры для чтения данных из КЛАДР, а не из массивов.


        1. VolCh
          02.07.2016 23:31

          Или, как вариант, из базы, доступные редактированию пользователям.


          1. dusterio
            03.07.2016 03:40

            Базы и так доступны к редактированию :) Выбран формат JSON специально, чтобы было удобно редактировать прямо через web-интерфейс GitHub:
            https://github.com/MenaraSolutions/geographer/blob/master/resources/translations/country/ru.json

            Долгосрочный план — что пользователи из разных стран сами будут постепенно улучшать базу

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


        1. dusterio
          03.07.2016 03:42

          там же в КЛАДР, насколько я понимаю, глубже иерархия — улицы и дома.

          про интерфейсы Вы правы, спасибо за замечание


          1. webmoder
            04.07.2016 17:39

            В дальнейшей перспективе неплохо было бы прикрутить (улицы и дома).


  1. bull1251
    11.11.2016 00:35

    Я бы не стал утверждать что прошлое одно. Это еще не доказано.
    Вариантов прошлого может быть столь же много, сколь и будущего. С повседневной точки зрения нам конечно кажется, что у нас одно прошлое. Но с этой точки зрения будущее также должно иметь только один вариант. Ведь возможность выбора остается только возможностью, пока выбор не сделан, а мы не можем одновременно выбрать два возможных варианта, взаимоисключающих друг друга.


    1. dusterio
      03.07.2016 03:39
      +1

      Это дело вкуса, я думаю. Очевидно же, что язык, на котором говорят люди в стране, нельзя поменять методом в ООП :)

      Насчет второго замечания — это и так возможно: Geographer::setLanguage('ru')
      Так как это singleton во фреймворках (Laravel и др), то изменение настроек отразится на всех последующих вызовах

      Я еще подумаю насчет language vs locale, спасибо за мысль!


      1. t3hk0d3
        03.07.2016 15:51

        C точки зрения API Язык в стране можно поменять — это всего лишь данные.

        Но если есть setLanguage, то я так понимаю getLanguage присутствует тоже, верно? А вот тут уже начинает один сплошной конфуз.

        ИМХО все таки локалализация строк в результатах методов не должна быть свойством объекта.


        1. dusterio
          03.07.2016 15:55

          Хм, Вы правы — конфуз есть ;-) getLanguage() есть только у страны, но тем не менее.

          На объектах городов-областей-стран этот метод для удобства, он в нем не реализован. Идея в том, чтобы можно было на любой стадии «исправить» положение. Это достаточно распространенная практика


          1. t3hk0d3
            03.07.2016 16:04

            Если смотреть по use-кейсам то получается что в большинстве случаев локаль достаточно установить один раз за запрос. Исключение — получение нативного имени, однако тут лучше сделать отдельный метод getNativeName().

            Russia (Россия)
            Japan (??)


            1. dusterio
              03.07.2016 16:07

              В большинстве случаев — да

              Что если надо вывести названия одной и той же страны на разных языках? :) (не только на текущем и нативном)

              Я, кстати, не спорю. Я с этой целью и публиковал статью, чтобы ценные рекомендации получать :)


              1. t3hk0d3
                03.07.2016 16:20

                Если надо вывести название страны в произвольной локали — то есть опциональный параметр метода, например getName($locale = NULL), что я и пропагандирую :)

                Предлагаю взглянуть на аналоги:
                https://github.com/hexorx/countries


                1. dusterio
                  03.07.2016 16:24

                  Да, логично. Возможно, на этом и остановлюсь! :)

                  Когда я начинал делать библиотеку, я даже не знал — будут ли еще какие-то куски информации переводимыми. Поэтому я последовал старому правилу «не оптимизируй заранее»

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


                  1. iqiaqqivik
                    04.07.2016 10:55

                    applyLanguage и/или useLanguage


                    1. dusterio
                      04.07.2016 10:56

                      я пока таки склоняюсь к setLocale() и к возможности дать параметром в getName()


                      1. oxidmod
                        04.07.2016 11:21
                        +1

                        можно еще withLocale($locale)


  1. kashey
    03.07.2016 16:31

    Вы конечно молодцы, и выпустили удобный продукт… Но давайте класс Earth отправим на глубокий рефактор.
    Есть список макрорегионов (https://ru.wikipedia.org/wiki/Макрорегионы_мира_(ООН)), на основе которого и надо именовать методы.
    Регионов дефакто немного больше, они немного по другому разбиты. И Micro — это микронезия, не надо ее обижать.
    PS: Связь стран и макрорегионов можно взять из экспорта http://data.esosedi.org/


    1. dusterio
      03.07.2016 16:37

      Сейчас есть только континенты в Earth, и они определены международным обществом :)

      Не уверен, что кому-то пригодится вывести список стран макрорегиона? Если честно, даже и про континент не уверен – просто это было в исходной базе Geonames, не стал убирать.

      Вы можете привести пример, когда будет полезно вывести список стран макрорегиона?

      По поводу семантики в случае с 'micro' — мне самому вариант не понравился, но ничего лучше не нашел. Мысль такая, что большинству разработчиков в большинстве случаев не нужны крохотные страны с крохотным населением. Искал термин для таких стран (а ля Гибралтар) — не нашел. Но префикс «микро» иногда используется: https://en.wikipedia.org/wiki/European_microstates


  1. jabocrack
    04.07.2016 10:56

    1) подозреваю, что коды стран все-таки по ISO 3166, а не по ISO 3611
    2) хотелось бы поддержки кодов стран по ISO 3166 numeric, а не только ISO 3166-1 alpha-2 и ISO 3166-1 alpha-3


    1. dusterio
      04.07.2016 11:00

      Точно — не помню откуда copy/paste сделал такой. Сам усомнился — а теперь точно вижу что там была ошибка.

      Цифровые добавим сегодня-завтра, спасибо за совет!


    1. dusterio
      04.07.2016 11:56

      Поправил первый пункт и добавил цифровые коды, спасибо еще раз


  1. nikolay_karelin
    04.07.2016 11:55

    Интересное решение, спасибо!

    Есть два предложения:
    1) Отделить библиотеку от ресурсов (т.е. другой репозиторий для ресурсов), чтобы можно было делать другие реализации, например, на другом языке программирования, и
    2) (Попробовать) использовать для перевода формат gettext — в нем сразу содержится и оригинал и перевод, что на мой взгляд гораздо удобнее для поддержки переводов, чем возня с цифровыми кодами.


    1. dusterio
      04.07.2016 11:56

      Первое уже запланировано, и в статье где-то упоминается вскользь :)

      Про второе неуверен — gettext, насколко я знаю, популярностью ныне не пользуется, входной порог будет выше


      1. nikolay_karelin
        04.07.2016 12:06

        Ясно, спасибо! Я видел упоминание, но потом посмотрел на репозиторий, и получил непонимание :)

        И надо будет поискать что-то дружественное, но современное для переводов.


  1. sango
    05.07.2016 01:26

    А где вы берете данные?


    1. dusterio
      05.07.2016 01:27

      Geonames, Wikipedia и бесплатные онлайн-словари


      1. morozovsk
        06.07.2016 11:34

        При достижении достаточной популярности, можно ожидать от пользователей разных стран внесения поправок в переводы через pull-запросы – справочники будут сами постоянно улучшаться, подобно wiki.

        Не пробовали osm? Пользователи разных стран уже вносят туда правки ежедневно. Все данные доступны для экспорта. Есть переводы городов на множество языков. Есть координаты городов, что даёт возможность создать геоиндекс, о котором вы писали в статье.

        Я изначально тоже собирал из Geonames, Wikipedia и других источников, но как всё это потом обновлять и поддерживать в актуальном состоянии? В тоже время сообщество osm достаточно активное, т.о. можно не дублировать работу по актуализации данных.


        1. dusterio
          07.07.2016 11:23

          Немного порылся и пока не нашел источников у OSM :(

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


          1. morozovsk
            07.07.2016 12:33

            Например, можно выкачать данные здесь: http://planet.openstreetmap.org/
            А с помощью osmosis можно извлекать только те данные, которые нужны http://wiki.openstreetmap.org/wiki/RU:Osmosis


            1. dusterio
              07.07.2016 12:54

              Догадываюсь, что у них нет склонений — им они не нужны?

              Как обстоят дела с областями — есть ли все области всех стран мира? У Geonames с этим туго, сейчас приходится с Wikipedia скрейпить


              1. morozovsk
                07.07.2016 14:31

                Догадываюсь, что у них нет склонений — им они не нужны?

                Да и не во всех языках они есть. Но у меня нет под руками данных, чтобы посмотреть, есть ли они в базе.
                Как обстоят дела с областями

                С этим у них гораздо лучше, чем у других гео-проектов. У них есть не только области, у них есть все точки их границ. Есть границы городов и их районов. В городах есть остановки и даже просто лавочки.
                Это большое сообщество как википедия и оно вносит каждый день огромное число данных.


                1. dusterio
                  07.07.2016 15:08

                  Ну, границы — это уже наверное лишнее для меня :) Все-таки я не пакет для картографии предлагаю, а просто базу данных административных делений

                  А вот области обязательно посмотрю. Если у них имеются коды вроде ISO/FIPS/Geonames — смогу автоматически импортнуть