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


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

-->


К сожалению, не доступен сервер mySQL