Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: [Symfony2][Symfony] file upload nie podoba mi się
Forum PHP.pl > Forum > PHP > Frameworki
Foxx
Właśnie po raz pierwszy zaimplementowałem upload obrazka + Doctrine i mam pewne wątpliwości. Chodzi o nakład pracy w kodzie potrzebny do przeprowadzenia tej operacji. Zrobiłem to w oparciu o lifecycle callbacks i musiały powstać 3 pola i 11 metod w obiekcie. Fajnie, że nie trzeba nic robić w kontrolerze i rozumiem, że część tych metod to rzeczy w stylu getWebPath i jak mi się nie podoba to nie muszę koniecznie ich używać, ale mimo wszystko to wydaje mi się trochę dziwne. Strach pomyśleć co się stanie jak będę miał 6 plików przy obiekcie.

A na koniec jeszcze czytam w cookbook, że "The PreUpdate and PostUpdate callbacks are only triggered if there is a change in one of the entity's field that are persisted" więc jak zmienię tylko awatar w profilu usera to on mi się nie zapisze :/ Trochę to załamujące, czy to podejście z cookbook jest naprawdę optymalne?
Crozin
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:
  1. namepsace ...\Form\Domain;
  2.  
  3. class Image {
  4. /**
  5.   * @Assert\NotBlank
  6.   * @Assert\Image(minWidth=50, maxWidth=200, ..., groups={"UserAvatar"})
  7.   * @Assert\Image(minWidth=500, maxWidth=5000, ..., groups={"UserProfile"})
  8.   */
  9. private $file;
  10.  
  11. /**
  12.   * @Assert\Type("boolean")
  13.   */
  14. private $delete = false;
  15.  
  16. // gettery/settery i cokolwiek uznasz za stosowne
  17. }
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:
  1. namespace ...\Entity;
  2.  
  3. class User {
  4. private $id;
  5. private $username;
  6. private $password;
  7.  
  8. private $avatarPath;
  9. }
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:
  1. namespace ...\Form\Domain;
  2.  
  3. class ImageUpload {
  4. /** @Assert\Valid(deep=true) */
  5. private $object;
  6.  
  7. /** @Assert\Valid(deep=true) */
  8. private $image;
  9.  
  10. private $imagePath;
  11.  
  12. public function __construct($object, Image $image, $imagePath) {
  13. ...
  14. }
  15. }
$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:
  1. public function postBind(FormEvent $event) {
  2. $data = $event->getData();
  3.  
  4. if (!$data instanceof ImageUpload) {
  5. return;
  6. }
  7.  
  8. if ($data->getImage()->delete()) {
  9. // usuwamy plik i aktualizujemy nasz obiekt
  10. PropertyAccess::getPropertyAccessor()->setValue($data->getObject(), null);
  11.  
  12. return;
  13. }
  14.  
  15. // Tutaj wykonujemy zapis pliku w odpowiednym miejscu, a korzystając z ProeprtyAccess ustawiamy ścieżkę do pliku:
  16. PropertyAccess::getPropertyAccessor()->setValue($data->getObject(), $data->getImagePath());
  17. }

3. Obsługa tego w jakimś kontrolerze:
  1. $user = ...;
  2.  
  3. $data = new ImageUpload($user, new Image(), 'object.avatarPath');
  4. $form = $this->createForm(new SomeFormType(), $data);
  5.  
  6. if (request post) {
  7. $form->bindRequest($request);
  8.  
  9. if ($form->isValid()) {
  10. $formData = $form->getData()->getObject();
  11.  
  12. // ...
  13. }
  14. }


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.
Foxx
O rany, Crozin, dzięki. Tamto rozwiązanie z cookbook działa i już miałem je pozostawić, ale wobec takiego wyłożenia tematu przez Ciebie zrobię to od razu na nowo.
q3trm
Cytat(Crozin @ 17.04.2013, 17:14:01 ) *
3. Obsługa tego w jakimś kontrolerze:
  1. $user = ...;
  2.  
  3. $data = new ImageUpload($user, new Image(), 'object.avatarPath');
  4. $form = $this->createForm(new SomeFormType(), $data);
  5.  
  6. if (request post) {
  7. $form->bindRequest($request);
  8.  
  9. if ($form->isValid()) {
  10. $formData = $form->getData()->getObject();
  11.  
  12. // ...
  13. }
  14. }


Dedukuję, że "SomeFormType()"zawiera nie mapowane pola user,password itd., w którym miejscu mają zostać przekazane dane do atrybutów obiektu User?.
Crea17
Witam.

Próbuję zrobić uploader wg tego co napisał Crozin, jednak po drodze napotkałem problem. Jestem początkujący w Symfony, stąd też mój problem.

Wydaje mi się, że zgubiłem się na tworzeniu listenera.

Serwis:
  1. <service id="my_user.imageupload" class="My\UserBundle\Form\Domain\ImageUpload">
  2. <tag name="kernel.event_listener" event="kernel.exception" method="onKernelException"/>
  3. </service>


Listener:
  1. <?php
  2.  
  3. namespace My\UserBundle\EventListener;
  4.  
  5. class ImageUploadListener
  6. {
  7.  
  8. public function __construct()
  9. {
  10. }
  11.  
  12. public function postBind(FormEvent $event) {
  13. $data = $event->getData();
  14.  
  15. if (!$data instanceof ImageUpload) {
  16. return;
  17. }
  18.  
  19. if ($data->getImage()->delete()) {
  20. // usuwamy plik i aktualizujemy nasz obiekt
  21. PropertyAccess::getPropertyAccessor()->setValue($data->getObject(), null);
  22.  
  23. return;
  24. }
  25.  
  26. // Tutaj wykonujemy zapis pliku w odpowiednym miejscu, a korzystając z ProeprtyAccess ustawiamy ścieżkę do pliku:
  27. PropertyAccess::getPropertyAccessor()->setValue($data->getObject(), $data->getImagePath());
  28. }
  29. }


Kontroler:
  1. $user = $this->container->get('security.context')->getToken()->getUser();
  2.  
  3. $data = new ImageUpload($user, new Image(), 'object.avatar');
  4.  
  5. $form = $this->createForm(new ProfileMainFormType(), $data);
  6.  
  7. $form->handleRequest($request);
  8.  
  9. if ($form->isValid()) {
  10.  
  11. var_dump($form->getData()->getObject());
  12.  
  13. echo 'ok form 1';
  14. }


Błąd jaki otrzymuje:
Kod
ContextErrorException: Warning: Missing argument 1 for My\UserBundle\Form\Domain\ImageUpload::__construct(), called in W:\wamp\www\symfony\app\cache\dev\appDevDebugProjectContainer.php on line 2930 and defined in W:\wamp\www\symfony\src\My\UserBundle\Form\Domain\ImageUpload.php line 14


Czy może mnie ktoś nakierować co robię źle?

*********************************************
Edit:

Powoli wraz z dokumentacją Symfony pokonuje trudności napotkane po drodze. Jednak jednego problemu nie mogę pokonać.

Kiedy uploaduje plik, otrzymuje notice
Kod
ContextErrorException: Runtime Notice: Only variables should be passed by reference in W:\wamp\www\symfony\src\My\UserBundle\Form\Type\ProfileAvatarFormType.php line 37


//ProfileAvatarFormType
  1. <?php
  2.  
  3. namespace My\UserBundle\Form\Type;
  4.  
  5. use Symfony\Component\Form\AbstractType;
  6. use Symfony\Component\Form\FormBuilderInterface;
  7. use Symfony\Component\Form\FormEvents;
  8. use Symfony\Component\Form\FormEvent;
  9. use Symfony\Component\PropertyAccess\PropertyAccess;
  10. use My\UserBundle\Form\Domain\ImageUpload;
  11.  
  12.  
  13. class ProfileAvatarFormType extends AbstractType
  14. {
  15. public function buildForm(FormBuilderInterface $builder, array $options)
  16. {
  17. $builder
  18. ->add('avatar', 'file', array('mapped' => false))
  19. ->add('save', 'submit');
  20.  
  21. $builder->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) {
  22.  
  23. $data = $event->getData();
  24.  
  25. if (!$data instanceof ImageUpload) {
  26. return;
  27. }
  28.  
  29. if ($data->getImage()->getDelete()) {
  30. // usuwamy plik i aktualizujemy nasz obiekt
  31. PropertyAccess::getPropertyAccessor()->setValue($data->getObject(), null);
  32.  
  33. return;
  34. }
  35.  
  36. //Tutaj wykonujemy zapis pliku w odpowiednym miejscu, a korzystając z ProeprtyAccess ustawiamy ścieżkę do pliku:
  37. PropertyAccess::getPropertyAccessor()->setValue($data->getObject(), $data->getImagePath());
  38. });
  39. }
  40.  
  41. public function getName()
  42. {
  43. return 'my_user_profileavatar';
  44. }
  45. }


//Kontroler
  1. public function indexAction(Request $request)
  2. {
  3. $user = $this->container->get('security.context')->getToken()->getUser();
  4.  
  5. $data = new ImageUpload($user, new Image(), 'avatar');
  6.  
  7. $form = $this->createForm(new ProfileAvatarFormType(), $data);
  8.  
  9.  
  10. $form->handleRequest($request);
  11.  
  12. if ($form->isValid()) {
  13. $user = $this->container->get('security.context')->getToken()->getUser();
  14. $data = $form->getData();
  15. $userManager = $this->container->get('fos_user.user_manager');
  16. $user->setAvatar($data->getAvatar());
  17.  
  18. $userManager->updateUser($user);
  19.  
  20. echo 'ok form';
  21. }
  22.  
  23.  
  24. return array('formMain' => $form->createView());
  25. }


//ImageUpload
  1. namespace My\UserBundle\Form\Domain;
  2.  
  3. use Symfony\Component\Validator\Constraints as Assert;
  4.  
  5. class ImageUpload {
  6. /** @Assert\Valid(deep=true) */
  7. private $object;
  8.  
  9. /** @Assert\Valid(deep=true) */
  10. private $image;
  11.  
  12. private $imagePath;
  13.  
  14. public function __construct($object, Image $image, $imagePath) {
  15. $this->object = $object;
  16. $this->image = $image;
  17. $this->imagePath = $imagePath;
  18. }
  19.  
  20. /**
  21.   * @param mixed $image
  22.   */
  23. public function setImage($image)
  24. {
  25. $this->image = $image;
  26. }
  27.  
  28. /**
  29.   * @return mixed
  30.   */
  31. public function getImage()
  32. {
  33. return $this->image;
  34. }
  35.  
  36. /**
  37.   * @param mixed $imagePath
  38.   */
  39. public function setImagePath($imagePath)
  40. {
  41. $this->imagePath = $imagePath;
  42. }
  43.  
  44. /**
  45.   * @return mixed
  46.   */
  47. public function getImagePath()
  48. {
  49. return $this->imagePath;
  50. }
  51.  
  52. /**
  53.   * @param mixed $object
  54.   */
  55. public function setObject($object)
  56. {
  57. $this->object = $object;
  58. }
  59.  
  60. /**
  61.   * @return mixed
  62.   */
  63. public function getObject()
  64. {
  65. return $this->object;
  66. }
  67.  
  68. }


//Image
  1. <?php
  2.  
  3. namespace My\UserBundle\Form\Domain;
  4.  
  5. use Symfony\Component\Validator\Constraints as Assert;
  6.  
  7. class Image {
  8. /**
  9.   * @Assert\NotBlank
  10.   * @Assert\Image(minWidth=50, groups={"UserAvatar"})
  11.   * @Assert\Image(minWidth=500, maxWidth=5000, groups={"UserProfile"})
  12.   */
  13. private $file;
  14.  
  15. /**
  16.   * @Assert\Type("boolean")
  17.   */
  18. private $delete = false;
  19.  
  20. /**
  21.   * @param mixed $delete
  22.   */
  23. public function setDelete($delete)
  24. {
  25. $this->delete = $delete;
  26. }
  27.  
  28. /**
  29.   * @return mixed
  30.   */
  31. public function getDelete()
  32. {
  33. return $this->delete;
  34. }
  35.  
  36. /**
  37.   * @param mixed $file
  38.   */
  39. public function setFile($file)
  40. {
  41. $this->file = $file;
  42. }
  43.  
  44. /**
  45.   * @return mixed
  46.   */
  47. public function getFile()
  48. {
  49. return $this->file;
  50. }
  51.  
  52. }


W PropertyAccessor metoda setValue posiada 3 parametry, jednak Crozin podał tylko 2 (w poprzedniej wersji symfony były 2 parametry?)
  1. public function setValue(&$objectOrArray, $propertyPath, $value)


Wracając do błędu, syfony pokazuje notice gdy przekazuje obiekt w parametrze $objectOrArray, tylko dlaczego?
Kolejna sprawa to jaki powinien być trzeci argument setValue()?
Crozin
Metoda PropertyAccessor::setValue oczekuje zawsze trzech argumentów, Ty zaś podajesz jej tylko dwa.

Co do błędu
Cytat
ContextErrorException: Runtime Notice: Only variables should be passed by reference in W:\wamp\www\symfony\src\My\UserBundle\Form\Type\ProfileAvatarFormType.php line 37
Prawdopodobnie chodzi o ułomność samego PHP-ca. Robisz takie coś:
  1. ...->setValue($object->getData(), ...)
PHP nie potrafi ogarnąć, że chodzi o referencję do obiektu/tablicy zwróconą przez metodę getData(). Musiałbyś to zastąpić czymś takim:
  1. $data = $object->getData();
  2. ...->setValue($data, ...)
Crea17
Ok, tylko w tym momencie dostaje error: Ta wartość nie powinna być pusta.

Jeżeli mapped=>true

Kod
Neither the property "avatar" nor one of the methods "getAvatar()", "isAvatar()", "hasAvatar()", "__get()" exist and have public access in class "My\UserBundle\Form\Domain\ImageUpload".


Z tego wynika, że powinienem mieć wymienione metody w pliku ImageUpload, ale wtedy za każdym użyciem z inną zmienną musiałbym tworzyć kolejne metody. Tak ma robić, czy coś mam źle?
pyro
Niezłe farmazony. Zapomnij o tych absolutnie zbędnych komplikacjach.

W encji ustaw właściwości dla pliku (niemapowany) oraz nazwy pliku (mapowany). W setterze pliku ustawiaj wygenerowaną nazwę pliku. Następnie po sprawdzeniu poprawności formularza przeprowadź upload do katalogu sprecyzowanego choćby w parametrach Sf2. Czytelne, proste, wygodne, nie naruszające dobrych zasad.

Pozdrowienia.
Crea17
@pyro

Czyli praktycznie tak jak jest napisane w CookBook w Basic Setup?
pyro
Nie. Zgadzam się z @Crozin, że podane tam przykłady są nienajlepsze, chociaż sam wybrał drogę umycia wieżowca szczoteczką do zębów. Przeczytaj jeszcze raz co napisałem i porównaj z Cookbookiem.
Crea17
Czyli w encji:
  1. /**
  2.   * @ORM\Column(type="string", length=255, nullable=true)
  3.   */
  4. protected $avatar;
  5.  
  6. /**
  7.   * @Assert\File(maxSize="6000000")
  8.   * @Assert\NotBlank
  9.   * @Assert\Image(minWidth=50)
  10.   * @Assert\Image(minWidth=50)
  11.   */
  12. protected $file;
  13.  
  14. /**
  15.   * Sets file.
  16.   *
  17.   * @param UploadedFile $file
  18.   */
  19. public function setFile(UploadedFile $file = null)
  20. {
  21. $this->file = $file;
  22. $ext = $this->file->guessExtension();
  23. $this->avatar = 'av-'.$this->getId().'.'.$ext;
  24. }


A w kontrolerze
  1. if ($form->isValid()) {
  2. $data = $form->getData();
  3.  
  4. $data->getFile()->move(
  5. $this->getUploadRootDir(),
  6. $user->getAvatar()
  7. );
  8.  
  9. $userManager = $this->container->get('fos_user.user_manager');
  10.  
  11. $userManager->updateUser($user);
  12. }


Dobrze zrozumiałem?
pyro
Coś w ten deseń, tak.

// EDIT

Cytat(Crea17 @ 25.02.2014, 09:52:27 ) *
  1. /**
  2.  
  3.  
  4.   /**
  5.   * @Assert\File(maxSize="6000000")
  6.   * @Assert\Image(minWidth=50)
  7.   * @Assert\Image(minWidth=50)
  8.   */
  9. protected $file;


Te 3 wymuszenia można połączyć w jedno.

Cytat(Crea17 @ 25.02.2014, 09:52:27 ) *
  1. $this->file = $file;
  2. $ext = $this->file->guessExtension();


$file to cały czas $file wink.gif
Crea17
Ok. Dziękuję bardzo za pomoc smile.gif
Crozin
@Crea17: Umknął mi gdzieś ten wątek.

Powinieneś mieć następujące typy dla formularza:
1. ImageType (pracujący na ...\Domain\Image) z definicjami dla właściwości "file" (typ "file") oraz "delete" (typ "boolean").
2. ProfileAvatarFormType (pracyjący na ...\Domain\ImageUpload) z definicjami dla właściwości "object" (typ UserType) oraz "image" (typ ImageType).
3. PropertyAccess::setValue() oczekuje zawsze 3 argumentów, których Ty nie zawsze podajesz.

@pyro: Gdzie Ty widzisz komplikacje w utrzymywaniu kodu w formie niezależnej od reszty, którą da się wielokrotnie wykorzystywać.
To jest wersja lo-fi głównej zawartości. Aby zobaczyć pełną wersję z większą zawartością, obrazkami i formatowaniem proszę kliknij tutaj.
Invision Power Board © 2001-2025 Invision Power Services, Inc.