Każdy chce stworzyć serwisy ładne, funkcjonalne i z wieloma bajerami. Fajną rzeczą jaką można udostępnić użytkownikom, to wgrywanie plików z podglądem w czasie rzeczywistym. Ciekawym rozwiązaniem jest jQuery File Upload który dostarcza nam frontend i backend.
W symfony2/3 bez problemu wgramy frontend (o tym kiedyś indziej), problemem może być backend. Istnieją już gotowe bundle. Wiele z nich testowałem. Niestety nie spełniały moich oczekiwań, dlatego napisałem własne rozwiązanie.
Zakładamy dość prosty model na którym będziemy pracować, możemy go dowolnie rozwijać.
Przykładowy interfejs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
namespace AppBundle\Entity; interface FileInterface { public function getId(); public function setFilename($filename); public function getFilename(); public function setSize($size); public function getSize(); } |
Model:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; /** * @ORM\Entity(repositoryClass="AppBundle\Repository\FileRepository") * @ORM\Table(name="file") */ class File implements FileInterface{ /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="string") */ protected $filename; /** * * @ORM\Column(type="integer") */ protected $size; /** * * @ORM\Column(type="datetime") */ protected $date; } |
Pozwolicie, że getery i setery wygenerujecie sobie sami.
Tworzymy serwis bazowy do obsługi: wyświetlania, zapisu oraz usuwania plików. Możemy ją używać bezpośrednio, do obsługi modelu który zaprezentowałem wyżej, lub rozwinąć.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
namespace AppBundle\Services; use Doctrine\ORM\EntityManager; use Symfony\Component\DependencyInjection\ContainerInterface; class Upload { protected $repository; protected $path; protected $container; protected $em; public function __construct(EntityManager $entityManager, ContainerInterface $container) { $this->em = $entityManager; $this->container = $container; } public function setParam($repository, $path) { $this->path = $path; $this->repository = $repository; } public function request($files, $object, $container_delete) { if ($files) { return $this->save($files, $object, $container_delete); } else { return $this->getFiles($container_delete); } } public function save($files, $object, $container_delete) { $response['files'] = array(); if (is_array($files)) { foreach ($files as $file) { $filename = $this->processImage($file, $this->path); $file = $object; $this->setObject($file, $filename); $this->em->persist($file); $this->em->flush(); $f = array( 'name' => $filename, 'url' => $this->path . $filename, 'thumbnailUrl' => '/' . $this->path . $file->getFilename(), 'size' => $file->getSize(), 'deleteUrl' => $this->deleteLink($file->getId(), $container_delete), 'deleteType' => "GET", 'id' => $file->getId() ); array_push($response['files'], $f); } } return $response; } protected function setObject($file, $filename){ $file->setFilename($filename); $file->setSize(filesize($this->path . $filename)); } public function getFiles($container_delete) { $files = $this->repository; $response['files'] = array(); foreach ($files as $file) { $f = array( 'name' => $file->getFilename(), 'url' => '/' . $this->path . $file->getFilename(), 'thumbnailUrl' => '/' . $this->path . $file->getFilename(), 'size' => $file->getSize(), 'deleteUrl' => $this->deleteLink($file->getId(), $container_delete), 'deleteType' => "GET" ); array_push($response['files'], $f); } return $response; } public function delete($path, $file) { $this->deleteFile($path . $file); $response = array( $file => true ); return $response; } protected function deleteLink($id, $name) { return $this->container->get('router')->generate($name, array('id' => $id)); } /** * Usuwanie pliku * @param type $file */ protected function deleteFile($file) { $file_path = $file; if (file_exists($file_path)) { unlink($file_path); } } /** * Generowanie miniaturki * @param type $filename * @param type $path * @param type $save_path * @param type $width * @param type $height */ protected function ImagesThumnb($filename, $path, $save_path, $width, $height) { $a = getimagesize($path . $filename); $image_type = $a[2]; if (in_array($image_type, array(IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_BMP))) { $thumb = new \Imagick(); $thumb->readImage($path . $filename); //$thumb->resizeImage($width,$height,\Imagick::FILTER_LANCZOS,1); $thumb->scaleimage($width, $height, true); $save = $save_path . $filename; $thumb->writeImage($save); $thumb->clear(); $thumb->destroy(); } } /** * Zapis zdjęcia * @param type $uploaded_file * @param type $path * @return string */ protected static function processImage($uploaded_file, $path) { $uploaded_file_info = pathinfo($uploaded_file->getClientOriginalName()); $filename = $uploaded_file_info['filename'] . '_' . uniqid() . "." . $uploaded_file_info['extension']; $uploaded_file->move($path, $filename); return $filename; } } |
Inicjacja serwisu:
1 2 3 |
Upload: class: AppBundle\Services\Upload arguments: [ "@doctrine.orm.entity_manager", '@service_container'] |
Potrzebujemy 2 akcji, pierwsza która będzie nam zwracać listę plików oraz zapisywać, druga która zajmie się usuwaniem plików.
Przykład:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
namespace AppBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\JsonResponse; use Doctrine\ORM\EntityRepository; use AppBundle\Entity\File; /** * @Route("/api/upload", name="controller_api_upload") */ class adminApiUploadController extends Controller { /** * @Route("/files", name="controller_api_upload_files") */ public function uploadFilesAction(Request $request) { $em = $this->getDoctrine()->getManager(); $files = $request->files->get("files"); $uploadServices = $this->container->get("Upload"); $fileRepository = $em->getRepository("AppBundle:File")->findAll(); $uploadServices->setParam($fileRepository, 'upload/files/'); $json = $uploadServices->request($files, new File(), 'controller_api_upload_delete'); return new JsonResponse($json); } /** * @Route("/delete/{id}", name="controller_api_upload_delete") */ public function deteleFilesAction($id){ $em = $this->getDoctrine()->getManager(); $uploadServices = $this->container->get("Upload"); $file = $em->find("AppBundle:File", $id); $json = $uploadServices->delete('upload/files/', $file->getFilename()); //fizyczne usunięcie pliku z serwera $em->remove($file); $em->flush(); return new JsonResponse($json); } } |
Teraz wystarczy wygenerować odnośniki do naszych kontrolerów:
1 2 3 |
<form id="fileupload" data-url="{{ path('controller_api_upload_files') }}" action="{{ path('controller_api_upload_files') }}" method="post" enctype="multipart/form-data"> |
Gdybyśmy chcieli użyć tej klasy kilka razy, na różnych modelach, przykładowo w galerii gdzie potrzebna jest kategoryzacja plików bez problemu możemy przeładować metodę setObejct()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
namespace AppBundle\Services; use AppBundle\Entity\CategoryGallery; class UploadGallery extends Upload{ protected $category; public function setCategory(CategoryGallery $category) { $this->category = $category; } protected function setObject($file, $filename) { $file->setCategory($this->category); parent::setObject($file, $filename); } } |
1 2 3 |
UploadGallery: class: AppBundle\Services\UploadGallery arguments: [ "@doctrine.orm.entity_manager", '@service_container'] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
namespace AppBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\JsonResponse; use AppBundle\Entity\PhotoGallery; /** * @Route("/api/photo") */ class adminApiPhotoController extends Controller { /** * @Route("/files/{id}", name="controller_admin_api_photo_files") */ public function filesAction(Request $request, $id) { $em = $this->getDoctrine()->getManager(); $files = $request->files->get("files"); $uploadServices = $this->container->get("UploadGallery"); $fileRepository = $em->getRepository("AppBundle:PhotoGallery")->findGallery($id); $uploadServices->setParam($fileRepository, 'upload/gallery/'); $category = $em->find("AppBundle:CategoryGallery", $id); $uploadServices->setCategory($category); $json = $uploadServices->request($files, new PhotoGallery(), 'controller_api_upload_delete'); return new JsonResponse($json); } } |
Oczywiście brakuje jeszcze akcji do usuwania, ale myślę że sobie poradzicie.
Klasę można oczywiście udoskonalić. Jest dość prosta i obsługuje podstawowe mechanizmy do obsługi jQuery File Upload. Ma też implementacje generowania miniaturek.