Пакет-географ – первая рабочая версия

    Прежде всего хотел бы поблагодарить за более, чем 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!

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 42
    • +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?
      • 0
        Ох, я сходу уже не помню – там что-то требовало 5.6. У меня изначально стояло 5.5, пока не наткнулся :) Я проверю в общем!

        Про Laravel/Lumen — да, вероятно вы правы. Кроме того, там настолько простая интеграция — её чуть ли не проще в документации описать :)
        • 0
          Я смотрел, правда очень бегло, но не нашел массового использования основных «фишек» 5.6 — плавающего кол-ва параметров аргументов фу-ии или использования «default», отлова «несуществующих» каллбэков тоже вроде не заметил через try-catch над object, deprecated function вам вовсе рано использовать, так что вам думаю видней в чем там загвоздка была.
          • 0
            Вспомнил — у меня тесты все в Travis запускаются. И изначально там 5.5 стояло в том числе — оно фейлилось как раз. Сейчас верну в Travis 5.5 и увижу :)
            • 0
              Нашел — это PhpUnit 5-ый не поддерживает 5.5. Сейчас попробую в 'require' сделать 5.5, а в 'require-dev' 5.6.

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

                Теперь проходят тесты на 5.5
                • 0
                  Да, видел, вы это, из теглайна и релизов удалите старый с «support php 5.5» и создайте его снова, чтобы в 'stable' нормально стягивало.
        • 0
          Вот бы ещё интеграцию с КЛАДР/ФИАС! Но боюсь, что проще будет запилить отдельную библиотеку…
          • 0
            Подозреваю, что это настолько массивная задача, что должна быть Git под-модулем. Вообще, всё, что не глобальное — должно быть опциональным, а не входить в основной пакет
            • +3
              Достаточно в вашем пакете выделить логику чтения и преобразования исходных данных о странах/регионах/городах в интерфейсы, и пользователи сами припишут нужные адаптеры для чтения данных из КЛАДР, а не из массивов.
              • 0
                Или, как вариант, из базы, доступные редактированию пользователям.
                • 0
                  Базы и так доступны к редактированию :) Выбран формат JSON специально, чтобы было удобно редактировать прямо через web-интерфейс GitHub:
                  https://github.com/MenaraSolutions/geographer/blob/master/resources/translations/country/ru.json

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

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

                  про интерфейсы Вы правы, спасибо за замечание
                  • 0
                    В дальнейшей перспективе неплохо было бы прикрутить (улицы и дома).
            • 0
              Честно говоря хотелось бы немного покритиковать API.

              В примерах меня сильно удивило следующее:

              return Geographer::findOneByCode($countryCode)
              ->setLanguage($language)
              ->getName();

              Если Geographier::findOneByCode() возвращает объект страны то метод setLanguage вызывает попаболь.
              Любой человек подумает что вы меняете язык у страны, а не локализацию строк. Но даже setLocale/getLocale вызвало бы одно недоумение.

              Имхо более разумный подход с локалями:

              Geographer::setDefaultLocale = 'ru'

              Geographer::Country::findByCode($countryCode)
              ->getName($locale);

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

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

                Я еще подумаю насчет language vs locale, спасибо за мысль!
                • 0
                  C точки зрения API Язык в стране можно поменять — это всего лишь данные.

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

                  ИМХО все таки локалализация строк в результатах методов не должна быть свойством объекта.
                  • 0
                    Хм, Вы правы — конфуз есть ;-) getLanguage() есть только у страны, но тем не менее.

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

                      Russia (Россия)
                      Japan (日本)
                      • 0
                        В большинстве случаев — да

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

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

                          Предлагаю взглянуть на аналоги:
                          https://github.com/hexorx/countries
                          • 0
                            Да, логично. Возможно, на этом и остановлюсь! :)

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

                            А сейчас все похоже на то, что только имена и переводятся. Вполне логично туда локаль (язык) и вставить
                            • 0
                              applyLanguage и/или useLanguage
                              • 0
                                я пока таки склоняюсь к setLocale() и к возможности дать параметром в getName()
                                • +1
                                  можно еще withLocale($locale)
              • 0
                Вы конечно молодцы, и выпустили удобный продукт… Но давайте класс Earth отправим на глубокий рефактор.
                Есть список макрорегионов (https://ru.wikipedia.org/wiki/Макрорегионы_мира_(ООН)), на основе которого и надо именовать методы.
                Регионов дефакто немного больше, они немного по другому разбиты. И Micro — это микронезия, не надо ее обижать.
                PS: Связь стран и макрорегионов можно взять из экспорта http://data.esosedi.org/
                • 0
                  Сейчас есть только континенты в Earth, и они определены международным обществом :)

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

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

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

                • 0
                  1) подозреваю, что коды стран все-таки по ISO 3166, а не по ISO 3611
                  2) хотелось бы поддержки кодов стран по ISO 3166 numeric, а не только ISO 3166-1 alpha-2 и ISO 3166-1 alpha-3
                  • 0
                    Точно — не помню откуда copy/paste сделал такой. Сам усомнился — а теперь точно вижу что там была ошибка.

                    Цифровые добавим сегодня-завтра, спасибо за совет!
                    • 0
                      Поправил первый пункт и добавил цифровые коды, спасибо еще раз
                    • 0
                      Интересное решение, спасибо!

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

                        Про второе неуверен — gettext, насколко я знаю, популярностью ныне не пользуется, входной порог будет выше
                        • 0
                          Ясно, спасибо! Я видел упоминание, но потом посмотрел на репозиторий, и получил непонимание :)

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

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

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

                              Координаты у меня уже есть, они все в базе. Алгоритм простой для геоиндексации тоже придумал
                              • 0
                                Например, можно выкачать данные здесь: http://planet.openstreetmap.org/
                                А с помощью osmosis можно извлекать только те данные, которые нужны http://wiki.openstreetmap.org/wiki/RU:Osmosis
                                • 0
                                  Догадываюсь, что у них нет склонений — им они не нужны?

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

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

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

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

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