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

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


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


82dd8adb797c427bb1cc8d3b1cb0414a.jpg

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


5238f3021dc649a38d6b104808b831ac.jpg

То встречайте (стучат барабаны) — библиотека Географ доступна в 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: название пункта в правильной форме


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


efacbc711e6f4dccb4b051f738a3ca3a.jpg

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


923b0761fdf64c41bd42fd0a8fcfafa4.jpg

Самое простое, банальное решение — добавить ещё несколько массивов или справочников в наше приложение, на каждую форму слова. Таким образом, у нас уже сотни или тысячи переводов появятся, и многие из них придётся добавлять или править вручную — большинство каталогов вроде 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!

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

  • 2 июля 2016 в 12:05

    0

    В принципе, вполне годная для использования библиотека, код вполне качественный, да и использовать удобно. Но возникло, как говориться одно «но»:
    "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?
    • 2 июля 2016 в 12:18

      0

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

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

      • 2 июля 2016 в 12:31

        0

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

          0

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

          0

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

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

© Habrahabr.ru