Пользователь
0,0
рейтинг
29 октября 2013 в 17:24

Разработка → 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')
    }
}


Больше нам тут ничего не надо делать. На минуту остановимся, и пойдем по этой ссылке — 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 web --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.fileLoadByDefault.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;
}


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

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

Далее по файлу 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. Естественно эти методы следует вынести в сервис и вызывать только его. В скором времени так и сделаем, но это уже отдельная история.

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

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


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

Так же вы вольны написать метод который это будет делать при условии не существования этих папок. Не забудьте поставить права на запись для них.
Максим @m4a1fox
карма
2,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • 0
    Хорошая статья. Побольше статей по Symfony2!
  • +2
    $filename = $_FILES['file']; # принимает наш файл
    


    а почему не так?
    $this->getRequest()->files->get('file')
    


    Да и UploadedFile вообще использовать?
    • 0
      и еще:

      return new Response(json_encode(array(
       ...
      


      Есть же JsonResponse
    • 0
      Со всем уважением, если у вас получится сделать так, как вы написали в комментарии, то буду признателен, если скажете как… Я потратил на это часа 2-3 но так ничего не получилось. Файл в
      $this->getRequest()->files->get('file')
      

      Sonata просто не видит, что очень печально, ибо изначальный вариант был такой же как вы и предложили.
      • 0
        я бы вообще сделал через
        $form = $this
            ->createFormBuilder(null, [
                'csrf_protection' => false
            ])
            ->add('file', 'file', [
                'constraints' => [
                    // тут набор валидаторов (размер, тип файла и т.п.)
                ]
            ])->getForm();
        
        $form->handleRequest($this->getRequest());
        
        if ($form->isValid()) {
            // сохраняем и т.п.
        } else {
             // возвращаем ошибку
        }
        
  • 0
    $.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); #
    


    1. есть dataType: 'json', чтобы не делать parseJSON вручную
    2. ничего страшного не случится если «добрый» админ в поле запишет /etc/passwd?
    • 0
      Вполне возможно что из-за отсутствия dataType: 'json' не отрабатывал $this->getRequest()->files->get('file'), но сказать не могу…

      Насчет /etc/passwd, возможно вы правы… Но админы априори ж не злые? Надеюсь.

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