-Поиск по дневнику

Поиск сообщений в rss_rss_hh_new

 -Подписка по e-mail

 

 -Статистика

Статистика LiveInternet.ru: показано количество хитов и посетителей
Создан: 17.03.2011
Записей:
Комментариев:
Написано: 51


Sonata Import Bundle

Пятница, 29 Сентября 2017 г. 15:28 + в цитатник
DOC_tr сегодня в 15:28 Разработка

Sonata Import Bundle

    До сих пор самой лучшей админ панелью для Symfony является SonataAdminBundle, и не зря. Простая установка, конфигурация, множество фич “из коробки” и большое сообщество.

    Единственное, что в ней отсутствует, так это импорт файлов. Согласитесь, немаловажная функция.

    В сети лежит множество реализаций импорта для Sonata, но везде есть небольшие недочеты — возможность импорта только текстовых полей, а не сущностей, не работает с коллекциями, проблематично грузить огромные базы, которые могут обрабатываться не один час…
    Сегодня я хочу вам представить свою реализацию, которой успешно пользуюсь уже достаточно долго, но только сейчас руки дошли все это причесать и оформить в отдельный bundle.

    image

    Я не буду здесь описывать весь процесс установки и настройки. Темболее что в отличии от многих реализаций он крайне прост. Все это вы можете прочитать в README.md и вики на github
    github.com/Doctrs/SonataImportBundle
    github.com/Doctrs/SonataImportBundle/wiki

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

    Большие объемы данных


    Впервые идея реализации этого бандла мне пришла в то время, когда требовалось перенести достаточно большую таблицу регионов, городов, поселков, площадей, и всего-всего-всего в базу заказчика (~3 млн строк). Осложняло все то, что доступов к его серверу у нас не было.
    Я попробовал несколько готовых решений, но понял, что они рассчитаны на небольшие объемы, которые можно загрузить за время ожидания ответа от сервера.

    Решение


    Необходимо было это реализовать не через веб-сервер, а через php-cli. Благо в Symfony есть очень хорошие инструменты для работы с консольными командами.
    Для вызова есть отличный класс Application:

    $application = new Application(); 
    // ... register commands 
    $application->run();


    Но данный способ также не подходит, потому что он работает через веб-сервер. Остается только одно: Symfony\Component\Process\Process, так как он напрямую работает с консолью. Создаем простую команду:

    $command = '/usr/bin/php '; 
    $command .= $this->get('kernel')->getRootDir() . '/console '; 
    $command .= 'promoatlas:sonata:import '; 
    $command .= $fileEntity->getId() . ' '; 
    $command .= '"' . $this->admin->getCode() . '" '; 
    $command .= '"' . ($fileEntity->getEncode() ? $fileEntity->getEncode() : 'utf8') . '" '; 
    $command .= ' > /dev/null 2>&1 &';


    Последняя строка для асинхронной работы. И запускаем все это в фоне.

    Отчетность


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

    Именно поэтому нам и нужен какой-то лог консольной команды. Обычно для логов я использую текстовые файлы, но в этот раз, из-за объема информации, я решил использовать базу данных.
    За каждую строку отвечает сущность: Doctrs\SonataImportBundle\Entity\ImportLog.
    Каждая запись соответствует строке из файла и в ней есть все, что нужно:

    • ts — таймштамп,
    • status — что произошло со строкой,
    • message — сообщения об ошибке,
    • line — номер строки из файла,
    • foreignId — ID сущности

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

    Так как для разбора файла используется итератор, то процент выполнения вывести не получится. Выводим просто общее обработанное число записей.

    Ошибки


    К сожалению, отлавливать FatalError я так и не научился. Поэтому в случае, например,

    function setOwner(Owner $owner);
    $owner = $em->findOwner(); // не найдено, вернет null
    $entity->setOwner($owner);


    команда упадет с FatalError.

    Еще одно исключение, с которым я столкнулся — ORMException.
    Что в нем такого интересного? Обычное исключение при попытке обработать запрос с неправильными данными.

    Собственно именно для этого он и предназначается, правда после выбрасывания такого исключения EntityManager закрывает соединение, и на любые попытки запросов к БД отвечает:
    EntityManager is closed

    В моем бандле такое исключение бросается в 2х случаях. Первый — если неверно настроена валидация сущности (перед добавлением в базу сущности в обязательном порядке валидируются)

    $validator = $this->getContainer()->get('validator');
    $errors = $validator->validate($entity);


    А второй связан с работой бандла с полями типа choice и entity. В случае, если у нас в сущности есть дочерняя сущность (например, у книги есть автор. Выбор автора происходит из базы), то при импорте книги мы можем указывать автора либо с помощью ID, либо с помощью названия. Если поле не числовое, то система пытается найти сущность по полю name. Если у сущности нет такого поля (например, имя автора хранится не в name, а в login или в username), то мы получаем ORMException.

    В принципе, они было достаточно частыми, поэтому мне пришлось сделать небольшой хак по перезагрузке EntityManager, для того, что бы после выбрасывания исключения, система могла выставить файлу STATUS_ERROR, и успешно отобразить все это в интерфейсе:

    if (!$this->em->isOpen()) {
        $this->em = $this->em->create(
            $this->em->getConnection(),
            $this->em->getConfiguration()
        );
    }


    Настройка Импорта/Экспорта


    По умолчанию Sonata экспортирует только простые поля (текст, дата, числа). Для того, чтобы она экспортировала вложенные сущности, их нужно явно задать в методе getExportFields. Помимо этого, у вложенных сущностей необходимо настроить метод __toString(); Представление сущности в виде строки как раз и будет экспортироваться.

    ImportBundle также использует этот метод, чтобы только что импортированный файл можно было без изменений загрузить в базу. В случае, если вы заново создаете файл, то таблица с соответствием столбец-поле есть на странице импорта.

    Расширяемость


    Мне никогда не нравилось то, что ради изменения пары строчек в бандле, необходимо делать (не то, чтобы сложную, но и не слишком удобную) надстройку с помощью easy-extends.
    Поэтому все, что можно, я вынес в конфиги. Даже класс, с помощью которого происходит разбор файла. Так что, в случае чего, вы всегда сможете реализовать загрузку и XML и JSON и XLS.

    doctrs_sonata_import:
        mappings:
            - { name: center_point, class: promaotlas.form_format.point}
            - { name: city_autocomplete, class: promoatlas.form_format.city_pa}
        upload_dir: %kernel.root_dir%/../web/uploads
        class_loader: Doctrs\SonataImportBundle\Loaders\CsvFileLoader
        encode:
            default: utf8
            list:
                - cp1251
                - utf8
                - koir8

    Подробнее обо всех параметрах конфигурации можно прочитать в вики

    Нестандартные типы полей


    В случае, если у вас есть нестандартные поля в базе данных (например, в моем случае, center_point — это координаты в базе данных), то необходимо объявить класс, который будет обрабатывать данные из файла, и приводить их к виду, в котором они будут заливаться в mysql.

    Например: тип center_point это координата (MySql тип — point). При добавлении в базу и получении из базы, она является объектом класса Point. У объекта Point есть метод __toString.

    public function __toString(){
        retrun $this->x . ', ' . $this->y;
    }

    С помощью него и происходит импорт, и в файле импорта мы получаем красивые координаты. Если мы попытаемся залить в базу те же x,y, то нас ждет ORMException. Именно для этого и предназначается массив mappings. В данном случае он просто берет сервис с id doctrs.form_format.point, который реализует интерфейс Doctrs\SonataImportBundle\Service\ImportAbstract, и на основе полученного значения возвращает нужный тип, который мы сможем залить в базу.

    Вот код самого сервиса
    class Point implements ImportAbstract {
    
        public function getFormatValue($value){
            $value = explode(',', $value);
            $point = new \PHPOpenGIS\MainBundle\Geometry\Point($value[0], $value[1] ?? 0);
            return $point;
        }
    
    }

    Код сервиса doctrs.form_format.city_pa

    class CityPa implements ImportAbstract, ContainerAwareInterface {
    
        private $container;
        public function setContainer(ContainerInterface $container = null) {
            $this->container = $container;
        }
    
        public function getFormatValue($value){
            /** @var ContainerInterface $container */
            $container = $this->container;
            $city = $container->get('promoatlas.city_autocomplete')->byName($value);
            return $city;
        }
    
    }

    Как видите, в параметре mappings мы указываем не названия классов, а id сервисов, что дает нам свободу действий. Например для преобразования типа city_autocomplete мне понадобился container.

    Заключение


    Данным бандлом я пользовался в течении полугода (в то время он еще не был оформлен и я просто подтягивал его с bitbucket). Само собой были некоторые некритичные ошибки, но после регистрации на packagist.org я стараюсь все исправлять, чтобы не осталось вопросов и невнятных сообщений об ошибках.

    Есть небольшие планы по улучшению этого бандла, но посмотрим, дойдут ли до них руки.

    Любым комментариям и замечаниям буду рад.
    Original source: habrahabr.ru (comments, light).

    https://habrahabr.ru/post/338986/

    Метки:  

     

    Добавить комментарий:
    Текст комментария: смайлики

    Проверка орфографии: (найти ошибки)

    Прикрепить картинку:

     Переводить URL в ссылку
     Подписаться на комментарии
     Подписать картинку