Przykłady uploadu w dokumentacji formularzu Symfony są fatalne - gwałcą kilka podstawowych zasad "dobrego kodu", które sam framework mocno forsuje. Nigdy nie powinny się tam znaleźć w takiej formie.
1. Utwórz odpowiedni model:
1.1. Obiekt reprezentujący wgrywany plik:
namepsace ...\Form\Domain;
class Image {
/**
* @Assert\NotBlank
* @Assert\Image(minWidth=50, maxWidth=200, ..., groups={"UserAvatar"})
* @Assert\Image(minWidth=500, maxWidth=5000, ..., groups={"UserProfile"})
*/
private $file;
/**
* @Assert\Type("boolean")
*/
private $delete = false;
// gettery/settery i cokolwiek uznasz za stosowne
}
1.2. W obiekcie, który posiada ów obraz (powiedzmy, że rozważamy klasyczny przykład użytkownika i jego awataru) najczęściej chcemy przechować jedynie ścieżkę do pliku. Ewentualnie możemy mieć kompletnie osobną tabelę w bazie danych reprezentującą obraz oraz metadane jego dotyczące (wymiary, typ itp.) złączony z inną tabelą relacją jeden-do-jednego. W obu przypadkach mechanizm jest jednak taki sam, więc omówimy jedynie ten pierwszy:
namespace ...\Entity;
class User {
private $id;
private $username;
private $password;
private $avatarPath;
}
Obiekt domeny nie powinien mieć absolutnie nic wspólnego z uploadem plików!
1.3. Na koniec potrzebujemy osobny obiekt, który złączy to wszystko razem, tj. wgrywany plik oraz interesujący nas obiekt sam w sobie:
namespace ...\Form\Domain;
class ImageUpload {
/** @Assert\Valid(deep=true) */
private $object;
/** @Assert\Valid(deep=true) */
private $image;
private $imagePath;
public function __construct($object, Image $image, $imagePath) {
...
}
}
$object to obiekt do którego będziemy wgrywać zdjęcie, $image to samo zdjęcie, a $imagePath to ścieżka do właściwości przechowującej ścieżkę do wgrywanego pliku (patrz:
komponent PropertyAccess Symfony). Czyli w tym przypadku ścieżką taką będzie "object.avatarPath" (właściwie to pierwszy człon "object." można pominąć). Jeżeli nasz obiekt byłby bardziej złożony i tak możemy bez problemu określić ścieżkę do właściwości "...path", np.: "object.image.path", "object.profile.defaultImage" itp.
1.4. Zarówno obiekt Image jak i ImageUpload mogą być wykorzystywane z dowolnym innym obiektem (User, Profile, Photo, Album itp.)
2. Model jest już przygotowany, pozostało obsłużenie samego uploadu. Właściwie to samym uploadem zajmje się symfony, my musimy jedynie zadbać o to by z wgranego pliku ("image.image") ścieżka została przepisana do interesującej nas właściwości obiektu domeny (tutaj: "object.avatarPath"). To zadanie musi wykonać się
po walidacji formularza i
przed pobraniem z niego danych w celu dalszej obróbki:
FormEvents::POST_BIND.
2.1. Tworzymy sobie usługę, którą rejestrujemy jako listenera dla w/w zdarzenia. Ta usługa, bez problemu może w swoim konstruktorze otrzymać ścieżkę do katalogu dla uploadu czy jakąś usługę typu
FilesystemUtility, która wygeneruje unikalną nazwę dla pliku. Mając już takiego listnera możemy spokojnie przerzucić dane:
public function postBind(FormEvent $event) {
$data = $event->getData();
if (!$data instanceof ImageUpload) {
return;
}
if ($data->getImage()->delete()) {
// usuwamy plik i aktualizujemy nasz obiekt
PropertyAccess::getPropertyAccessor()->setValue($data->getObject(), null);
return;
}
// Tutaj wykonujemy zapis pliku w odpowiednym miejscu, a korzystając z ProeprtyAccess ustawiamy ścieżkę do pliku:
PropertyAccess::getPropertyAccessor()->setValue($data->getObject(), $data->getImagePath());
}
3. Obsługa tego w jakimś kontrolerze:
$user = ...;
$data = new ImageUpload($user, new Image(), 'object.avatarPath');
$form = $this->createForm(new SomeFormType(), $data);
if (request post) {
$form->bindRequest($request);
if ($form->isValid()) {
$formData = $form->getData()->getObject();
// ...
}
}
Plusy takiego rozwiązania:
1. Jest uniwersalne, można je wykorzystać z dowolnym obiektem.
2. Wszystko jest ładnie rozdzielone, mamy czysty kod.
Jeżeli chciałbyś dodać obsługę wielu plików, wystarczy że zmodyfikujesz powyższe w taki sposób by ImageUpload posiadał kolekcję obiektów Image oraz tablicę $imagePath.