SonataAdminBundle + AJAX загрузка файлов.

Всем приятного времени суток. В данной статье, я хочу рассмотреть 2 способа не совсем обычной загрузки файлов, которые мне по долгу службы пришлось реализовать на одном проекте. Задача стояла такая: необходимо реализовать Drag & Drop закачку файлов в админ части сайта, который был сделан на framefork’e Symfony 2.3 + SonataAdminBundle. По ряду причин я опускаю ту часть, в которой Соната ставилась (если появится необходимость то можно и восполнить этот пробел). Итак, я полагаю что у вас уже установлена Соната и создана хотябы одна сущность в папке Entity. Если же нет, давайте сделаем это.

// MyFolder/MyBundle/Entity/Name

<?php
namespace MyFolder\MyBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Table
 *
 * @ORM\Table(name="table")
 * @ORM\Entity
 */
class Table
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=255, nullable=true)
     */
    private $filePath;
}

Далее сгенерим геттеры и сеттеры. Заходим в терминале/консоли:

$ app/console doctrine:generate:entities MyFolder/MyBundle/Entity/Name

После того, как сгенерировались геттеры и сеттеры, мы приступаем к Сонате. Итак, код, нашего сонатовского файла будет таким:

// MyFolder/MyBundle/Admin/Name
<?php
namespace MyFolder\MyBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;

class NameAdmin extends Admin
{
    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper
            ->add('name');
    }

    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        $datagridMapper
            ->add('name')
    }

    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper
            ->addIdentifier('name')
    }
}
 

Больше нам тут ничего не надо делать. На минуту остановимся, и пойдем по этой ссылке - https://github.com/weixiyen/jquery-filedrop. Тут нас интересует библиотека, там только один js файл, так что не промахнетесь :).

Итак. Начинается самое интересное, ибо нам необходимо реализовать Drag & Drop, давайте его и реализуем. Для этого мы сделаем следующее, в папке MyBundle/Resources/view/Admin (Если такой нет, создайте, путаницы потом меньше будет) создаем файлик шаблона twig - sonata_admin_base_layout.html.twig с таким содержимым:

// MyBundle/Resources/view/Admin/sonata_admin_base_layout.html.twig
{% extends 'SonataAdminBundle::standard_layout.html.twig' %}

{% block stylesheets %}
    {{ parent() }}
    <link href="{{ asset('bundles/mybundle/css/admin/style.css') }}" rel="stylesheet" type="text/css" />
{% endblock %}

{% block javascripts %}
    {{ parent() }}
    <script src="{{ asset('bundles/mybundle/js/admin/jquery.filedrop.js') }}"></script>
    <script src="{{ asset('bundles/mybundle/js/admin/js.fileDropBlock.js') }}"></script>
    <script src="{{ asset('bundles/mybundle/js/admin/js.fileLoadByDefault.js') }}"></script>
    <script src="{{ asset('bundles/mybundle/js/admin/init.js') }}"></script>
{% endblock %}

После идем в config.yml и переопределим основной шаблон сонаты

// app/config/config.yml
sonata_admin:
    title: My Admin Panel
    templates:
        ## default global templates
        layout:  MyFolderMyBundle:Admin:sonata_admin_base_layout.html.twig

Итак, что мы сделали, мы переопределили главный шаблон сонаты, чтобы иметь возможность внедрять свои файлы в него. Вы естественно могли заменить вот эти 4 строчки:

<script src="{{ asset('bundles/mybundle/js/admin/jquery.filedrop.js') }}"></script>
<script src="{{ asset('bundles/mybundle/js/admin/js.fileDropBlock.js') }}"></script>
<script src="{{ asset('bundles/mybundle/js/admin/js.fileLoadByDefault.js') }}"></script>
<script src="{{ asset('bundles/mybundle/js/admin/init.js') }}"></script>

Эти файлы собственно живут у нас по таком пути:

MyBundle/Resources/public/js/admin/jquery.filedrop.js

MyBundle/Resources/public/js/admin/js.fileDropBlock.js

MyBundle/Resources/public/js/admin/js.fileLoadByDefault.js

MyBundle/Resources/public/js/admin/js.init.js.

На заметку, что бы все было по правильному, настоятельно рекомендую вам создать в папке public папку admin в ней создать файлы:

  1. js.fileDropBlock.js
  2. js.fileLoadByDefault.js
  3. init.js.

Как вы могли понять первый файл из 4 это наш ранее загруженный с гита, так что не стесняемся и кидаем его в папку admin, а после делаем assets. В консоле набираем

$ app/console assets:install www --symlink если все правильно, то у вас в папке /bundle/mybundle/ должна появиться копия вашей папки public, появилась? Погнали дальше. Теперь о каждом файле по порядку. Итак, файл №1(js.fileDropBlock.js) и его код:

// MyBundle/Resources/public/js/admin/js.fileDropBlock.js
function fileDropBlock(block, type) {
    var allowType = {
        'img': ['image/jpeg', 'image/png', 'image/gif']
    };

    block.filedrop({
        url: '/upload-file', # url к которой будет происходить обращение при активации загрузки
        paramname: 'file', # параметр. По сути это атрибут name вашего input поля
        fallbackid: 'upload_button', 
        maxfiles: 1, # кол-во файлов
        maxfilesize: 2, # размер файла в mb

# Реакция на ошибки. Тут может быть что угодно
        error: function (err, file) {
            switch (err) {
                case 'BrowserNotSupported':
                    console.log('Old browser');
                    break;
                case 'FileTooLarge':
                    console.log('File Too Large');
                    break;
                case 'TooManyFiles':
                    console.log('Only 1 file can be downloader');
                    break;
                case 'FileTypeNotAllowed':
                    console.log('Wrong file type');
                    break;
                default:
                    console.log('Some error');
            }

        },
        allowedfiletypes: allowType[type], # разрешенные типы файлов для загрузки
        dragOver: function () {
            block.addClass('active-drag-block');
        },
        dragLeave: function () {
            block._removeClass('active-drag-block');
        },


        uploadFinished: function (i, file, response) {
            block.find('input[type="text"]').val(response.filePath); # в инпут поместим путь к файлу
        }
    })
}

файл №2(js.fileDropBнВyDefault.js) и его код:

// MyBundle/Resources/public/js/admin/js.LoadByDefault.js
var arrayType = {
    'img': [
        'image/png',
        'image/jpg',
        'image/jpeg'
    ],
    'pdf': [
        'application/pdf',
        'application/x-pdf'
    ]
};


function fileLoadByDefault(selector, type, block) {

    var input = document.getElementById(selector),
        formdata = false;
    input.click();
}

Не много не правда ли? :) Он нам понадобиться чуть позже. Итак, и наконец файл под номером 3 (init.js) и его код:

// MyBundle/Resources/public/js/admin/init.js
(function ($) {
    $(document).ready(function () {
        $.fn.uploadFile = function (type) {
            var blockText = {
                'img': {'text': ['Drag Image File Here'], 'name': ['img'], 'id': ['imguploadform']}
            };

            this.append('<p>' + blockText[type].text + '</p>');
            this.append('<input type="file" class="upload-file" name="' + type + 'file" id="' + type + 'uploadform" data-type="'+ blockText[type].name +'">');
            this.addClass('drag_n_drop--' + type + 'Path');
            $('input', this).hide();

            fileDropBlock(this, type);
        };

        var imgBlock = $('div', 'div[id$="_coverPath"]');
        imgBlock.uploadFile('img');

        $('input[type="file"]').on("change", function () {
            var $_this = $(this),
                type = $_this.data('type'),
                reader,
                file;
            file = this.files[0];

            if (window.FormData) {
                formdata = new FormData();
            }

            if (window.FileReader) {
                reader = new FileReader();
                reader.readAsDataURL(file);
            }

            if (formdata) {
                formdata.append("file", file);
            }

            if (!$.inArray(file.type, arrayType[type])) {
                $.ajax({
                    url: "/upload-file",
                    type: "POST",
                    data: formdata,
                    processData: false,
                    contentType: false,
                    success: function (res) {
                        var userData = jQuery.parseJSON(res);
                        $_this.parent().find('input[type="text"]').val(userData.filePath);
                    }
                });
            } else {
                alert('Wrong type')
            }
        });

        imgBlock.click(function () {
            fileLoadByDefault('imguploadform', 'img', this);
        });
    });
})(jQuery);

Давайте проясним что происходит и разберем код по частям. Ранее мы имели только один инпут на странице, а нам нужна область для Drag & Drop’a.

$.fn.uploadFile = function (type) {
    var blockText = {
        'img': {
            'text': ['Drag Image File Here'], 
            'name': ['img'], 
            'id': ['imguploadform']
        }
    };

    this.append('<p>' + blockText[type].text + '</p>');
    this.append('<input type="file" class="upload-file" name="' + type + 'file" id="' + type + 'uploadform" data-type="'+ blockText[type].name +'">');
    this.addClass('drag_n_drop--' + type + 'Path');
    $('input', this).hide();

    fileDropBlock(this, type);
};

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

Функция приняла его и вернула нам, по сути уже новый html код. Который включает в себя абрац с текстом

this.append('<p>' + blockText[type].text + '</p>');

кнопку загрузки, она потом еще сыграет важную роль

this.append('<input type="file" class="upload-file" name="' + type + 'file" id="' + type + 'uploadform" data-type="'+ blockText[type].name +'">');

Добавляем к элементу класс, что бы понимать какой он

this.addClass('drag_n_drop--' + type + 'Path');

и скрываем все инпуты

$('input', this).hide();

Добавим красок css

// MyBundle/Resources/public/css/style.css
.drag_n_drop--imgPath{
    width: 150px;
    height: 100px;
    cursor: pointer;
    border: 2px solid #e0e0e0;
    background: #f9f9f9;
}

В конечном итоге, после всех таких манипуляций, у вас должно получиться что то похожее на это

Если так и есть, то все хорошо. :)

Далее по файлу init.js

Такой код

var imgBlock = $('div', 'div[id$="_name"]'); # выбираем целый див в котором наш инпут
imgBlock.uploadFile('img'); # к выбранному применяем функцию uploadFile

Далее вот такой небольшой кусок кода:

$('input[type="file"]').on("change", function () { # реагируем на то, что в инпуте изменилось
    var $_this = $(this),
        type = $_this.data('type'),
        reader,
        file;
    file = this.files[0]; # ловим файл из инпута типа file

    if (window.FormData) {
        formdata = new FormData(); # берем данные (это новые плюшки в версии js)
    }

    if (window.FileReader) {
        reader = new FileReader(); # читаем файл
        reader.readAsDataURL(file);
    }

    if (formdata) {
        formdata.append("file", file); # присоедениям файл в объект
    }

    if (!$.inArray(file.type, arrayType[type])) { # проверяем что тип файла какой нам надо
        $.ajax({
            url: "/upload-file", # идем по пути
            type: "POST", 
            data: formdata, # отправляем туда наши файлы
            processData: false,
            contentType: false,
            success: function (res) {
                var userData = jQuery.parseJSON(res); # парсим результат
                $_this.parent().find('input[type="text"]').val(userData.filePath); # находим наш скрытый инпут и ставим в него путь к уже закаченному файлу
            }
        });
    } else {
        alert('Wrong type'); # а иначе alert с ошибкой
    }
});

Далее, код

imgBlock.click(function () {
    fileLoadByDefault('imguploadform', 'img', this);
});

Тут просто, когда кликаем по нашему диву, который предназначен для бросания в него картинки, срабатывает функция fileLoadByDefault в ней 3 аргумента. 1 - id input’a с типом file. 2 - тип файла который мы хотим загрузить. 3 - собственно сам эллемент родитель, по которому произошел клик.

 

Собственно вот тут, внимательный читатель мог заметить, что по сути наш код, реализует 2 способа загрузки. Первый - Drag & Drop(то к чему и стримились), и второй - это клик по диву контейнера, для вызова стандартной формы upload’a файла, который предназначен то для Drup&Drop. По сути 2 - это побочный эффект, такой приятный :) побочный эффект.

 

Не хотелось бы вас огорчать, но мы проделали только половину работы... Дальше веселее, давайте теперь покодим на php?! :)

Итак, мы помним, что ссылаемся на ссылку [/upload-file] при любом событии, будь то Drop файла или прямая загрузка.

Надо нам определить роут для этого дела:

#MyFolder/MyBundle/Resourses/config/rounting.yml
my_file_upload:
    pattern:  /upload-file
    defaults: { _controller: MyFolderMyBundle:Default:uploadFile }

Взглянем на код метода [uploadFile]

// MyFolder/MuBundle/Contraller/Default.php
<?php

namespace MyFolder\MyBundle\Controller;

use Symfony\Component\HttpFoundation\File\File;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\File\UploadedFile;

class DefaultController extends Controller
{
    public function uploadFileAction()
    {

        $filename = $_FILES['file']; # принимает наш файл
        $uploadPath = $this->upload($filename); # запускаем функцию загрузки

        /**
          * Тут думаю ясно. Обычный ответ на запрос
          */
        return null === $uploadPath
            ? new Response(json_encode(array(
                        'status' => 0,
                        'message' => 'Wrong file type'
                    )
                )
            )

            : new Response(json_encode(array(
                        'status' => 1,
                        'message' => $filename, # имя файла
                        'filePath' => $uploadPath # полный путь к нему
                    )
                )
            );
    }


    private function getFoldersForUploadFile($type)
    {
        $fileType = $this->returnExistFileType($type); #метод возвращающюй тип файлов которые можно грузить

        if ($fileType !== null) {
            return array(
                'root_dir' => $this->container->getParameter('upload_' . $fileType . '_root_directory'), # полный путь к папке с картинкой
                'dir' => $this->container->getParameter('upload_' . $fileType . '_directory'), # отосительный путь к папке
            );
        } else {
            return null;
        }
    }

    # метод возвращает ключ(тип) файла который будет закачиваться
    private function returnExistFileType($type)
    {
        $typeArray = array(
            'img' => array(
                'image/png',
                'image/jpg',
                'image/jpeg',
            ),
            'pdf' => array(
                'application/pdf',
                'application/x-pdf',
            )
        );

        foreach ($typeArray as $key => $value) {
            if (in_array($type, $value)) {
                return $key;
            }
        }

        return null;
    }

    # Тут собственно все и происходит. Загрузка, присвоение имени, перемещение в папку
    private function upload($file)
    {
        $filePath = $this->getFoldersForUploadFile($file['type']);

        if (null === $this->getFileInfo($file['name']) || $filePath === null) {

            return null;
        }
        $pathInfo = $this->getFileInfo($file['name']);
        $path = $this->fileUniqueName() . '.' . $pathInfo['extension'];
        $this->uploadFileToFolder($file['tmp_name'], $path, $filePath['root_dir']);

        unset($file);
        return $filePath['dir'] . DIRECTORY_SEPARATOR . $path;
    }

    # возвращает всю информацию о загруженном фале (что бы это не было)
    private function getFileInfo($file)
    {

        return $file !== null ? (array)pathinfo($file) : null;
    }

    # формирует уникальное имя
    private function fileUniqueName()
    {

        return sha1(uniqid(mt_rand(), true));
    }
    
    # перемещает файл в необходимую папку
    private function uploadFileToFolder($tmpFile, $newFileName, $rootFolder)
    {
        $e = new File($tmpFile);
        $e->move($rootFolder, $newFileName);
    }

Как оказалось в итоге, не так страшен черт. Возможно упустил какой то момент... Но ты, уважаемый читатель, волен писать в комментариях свои вопросы, пожелания по коду и его оптимизации. Собственно в данной статье я показал самый простой способ реализации своего загрузчика в Symfony. Естественно эти методы следует вынести в сервис и вызывать только его. В скором времени так и сделаем, но это уже отдельная история. До новых встреч на просторах 4devs.io. Мы рады вам. Всем удачного кодинга. С Вами был m4a1fox.

 

P.S. Путь к папкам для закачки файлов выглядит так

// app/config/config.yml
parameters:
   upload_img_root_directory: %kernel.root_dir%/../web/upload/img
   upload_img_directory: upload/img

Папки upload и папки img внутри нее нет, их необходимо создать.

Так же вы вольны написать метод который это будет делать при условии не существования этих папок. Не забудьте поставить права на запись для них.

Читайте также:

Docker установка и настройка

При разработке используется множество технологий. К примеру данный блог использует php(Symfony 2), mongodb, elastic, nginx это основные но также используется nodejs к примеру для минификации css, js. При разработке приходится настраивать все технологии как показано в статье. Но что делать если проектов несколько или они используют разные технологии, например другой проект использует MySQL, или еще могут использовать разные версии php или других библиотек. Для разработки и поддержки проектов на разных технологиях можно использовать Виртуальную машину и поставить на нее к примеру centos, но это не совсем удобно, надо все равно настраивать подобное окружение как на сервере и на других машинах разработчиков. Мы будет использовать контейнеры, такие как Docker. Настроем блог разработчиков чтобы использовать Docker для разработки.

Как использовать произвольное хранилище пользователей в FOSUserBundle

Практически все используют FOSUserBundle в своих Symfony проектах т.к. он ускоряет разработку и обладает хорошим набором функциональности для управления пользователями. Бандл предоставляет несколько готовых реализаций хранилищ данных: Propel и несколько для Doctrine (ORM и ODM). Это здорово, но иногда возникает необходимость работы с другими хранилищами данных. FOSUserBundle достаточно гибок и позволяет реализовать, и использовать произвольное хранилище. Для того, чтобы использовать все возможности FOSUserBundle Вам достаточно будет написать свой менеджер пользователей под конкретного провайдера.

Интеграция Paysera в Symfony

Сегодня сложно представить себе серьезный проект, где не понадобилась бы интеграция с платежными системами. Не смотря на то что существует множество популярных систем и аггрегаторов, таких как PayPal, RBKMoney, Paymentwall, Robokassa и т.д., я хочу рассказать о Paysera. Это еще одна, довольно новая платежная система. Они позиционируют себя как выгодных с точки зрения комиссий за их услуги. Paysera позволяет вашим пользователям расплачиваться карточками, SMS и т.д. Интеграция довольно простая, однако имеет некоторые неочевидные моменты, которые я и хочу осветить.

Настройка php, MySQL, nodejs, nginx и mongodb в OS X El Capitan

Недавно вышла OS X El Capitan, давайте обновим систему. Систему будем устанавливать с usb-flash. Из рабочего окружения мы поставим  php, nginx, mariadb, mongodb, elasticsearch, nodejs используя brew и настроем проект на symfony2.

Создание сайта “Обратный отсчет” на Symfony2

Мы иногда видим сайты с обратным отсчетом, проект стартует через … Его сделать достаточно просто, и не займет много времени. Мы воспользуемся проектом fdevs/coming-soon, который основан на Symfony2. Также будем сохранять введеный пользователями адреса электронной почтой в базу данных MongoDB. У нас есть настроенное рабочее окружение Osx, о настройке можно прочитать в статье Yosemite настройка рабочего окружения. Но главное версия php не меньше 5.4. В проекте можно также использовать реляционную базу данных типа MySQL. По умолчанию в проекте вообще не используется база данных, а введеный email отправляется на почту.