recipe_file_uploads.rst 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. Uploading and saving documents (including images) using DoctrineORM and SonataAdmin
  2. ===================================================================================
  3. This is a full working example of a file upload management method using
  4. SonataAdmin with the DoctrineORM persistence layer.
  5. Pre-requisites
  6. --------------
  7. - you already have SonataAdmin and DoctrineORM up and running
  8. - you already have an Entity class that you wish to be able to connect uploaded
  9. documents to, in this example that class will be called ``Image``.
  10. - you already have an Admin set up, in this example it's called ``ImageAdmin``
  11. - you understand file permissions on your web server and can manage the permissions
  12. needed to allow your web server to upload and update files in the relevant
  13. folder(s)
  14. The recipe
  15. ----------
  16. First we will cover the basics of what your Entity needs to contain to enable document
  17. management with Doctrine. There is a good cookbook entry about
  18. `uploading files with Doctrine and Symfony`_ on the Symfony website, so I will show
  19. code examples here without going into the details. It is strongly recommended that
  20. you read that cookbook first.
  21. To get file uploads working with SonataAdmin we need to:
  22. - add a file upload field to our ImageAdmin
  23. - 'touch' the Entity when a new file is uploaded so its lifecycle events are triggered
  24. Basic configuration - the Entity
  25. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  26. Following the guidelines from the Symfony cookbook, we have an Entity definition
  27. that looks something like the YAML below (of course, you can achieve something
  28. similar with XML or Annotation based definitions too). In this example we are using
  29. the ``updated`` field to trigger the lifecycle callbacks by setting it based on the
  30. upload timestamp.
  31. .. configuration-block::
  32. .. code-block:: yaml
  33. # src/AppBundle/Resources/config/Doctrine/Image.orm.yml
  34. AppBundleBundle\Entity\Image:
  35. type: entity
  36. repositoryClass: AppBundle\Entity\Repositories\ImageRepository
  37. table: images
  38. id:
  39. id:
  40. type: integer
  41. generator: { strategy: AUTO }
  42. fields:
  43. filename:
  44. type: string
  45. length: 100
  46. # changed when files are uploaded, to force preUpdate and postUpdate to fire
  47. updated:
  48. type: datetime
  49. nullable: true
  50. # ... other fields ...
  51. lifecycleCallbacks:
  52. prePersist: [ lifecycleFileUpload ]
  53. preUpdate: [ lifecycleFileUpload ]
  54. We then have the following methods in our ``Image`` class to manage file uploads:
  55. .. code-block:: php
  56. <?php
  57. // src/AppBundle/Bundle/Entity/Image.php
  58. const SERVER_PATH_TO_IMAGE_FOLDER = '/server/path/to/images';
  59. /**
  60. * Unmapped property to handle file uploads
  61. */
  62. private $file;
  63. /**
  64. * Sets file.
  65. *
  66. * @param UploadedFile $file
  67. */
  68. public function setFile(UploadedFile $file = null)
  69. {
  70. $this->file = $file;
  71. }
  72. /**
  73. * Get file.
  74. *
  75. * @return UploadedFile
  76. */
  77. public function getFile()
  78. {
  79. return $this->file;
  80. }
  81. /**
  82. * Manages the copying of the file to the relevant place on the server
  83. */
  84. public function upload()
  85. {
  86. // the file property can be empty if the field is not required
  87. if (null === $this->getFile()) {
  88. return;
  89. }
  90. // we use the original file name here but you should
  91. // sanitize it at least to avoid any security issues
  92. // move takes the target directory and target filename as params
  93. $this->getFile()->move(
  94. self::SERVER_PATH_TO_IMAGE_FOLDER,
  95. $this->getFile()->getClientOriginalName()
  96. );
  97. // set the path property to the filename where you've saved the file
  98. $this->filename = $this->getFile()->getClientOriginalName();
  99. // clean up the file property as you won't need it anymore
  100. $this->setFile(null);
  101. }
  102. /**
  103. * Lifecycle callback to upload the file to the server
  104. */
  105. public function lifecycleFileUpload()
  106. {
  107. $this->upload();
  108. }
  109. /**
  110. * Updates the hash value to force the preUpdate and postUpdate events to fire
  111. */
  112. public function refreshUpdated()
  113. {
  114. $this->setUpdated(new \DateTime());
  115. }
  116. // ... the rest of your class lives under here, including the generated fields
  117. // such as filename and updated
  118. When we upload a file to our Image, the file itself is transient and not persisted
  119. to our database (it is not part of our mapping). However, the lifecycle callbacks
  120. trigger a call to ``Image::upload()`` which manages the actual copying of the
  121. uploaded file to the filesystem and updates the ``filename`` property of our Image,
  122. this filename field *is* persisted to the database.
  123. Most of the above is simply from the `uploading files with Doctrine and Symfony`_ cookbook
  124. entry. It is highly recommended reading!
  125. Basic configuration - the Admin class
  126. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  127. We need to do two things in Sonata to enable file uploads:
  128. 1. Add a file upload widget
  129. 2. Ensure that the Image class' lifecycle events fire when we upload a file
  130. Both of these are straightforward when you know what to do:
  131. .. code-block:: php
  132. <?php
  133. // src/AppBundle/Admin/ImageAdmin.php
  134. class ImageAdmin extends AbstractAdmin
  135. {
  136. protected function configureFormFields(FormMapper $formMapper)
  137. {
  138. $formMapper
  139. ->add('file', 'file', array(
  140. 'required' => false
  141. ))
  142. // ...
  143. ;
  144. }
  145. public function prePersist($image)
  146. {
  147. $this->manageFileUpload($image);
  148. }
  149. public function preUpdate($image)
  150. {
  151. $this->manageFileUpload($image);
  152. }
  153. private function manageFileUpload($image)
  154. {
  155. if ($image->getFile()) {
  156. $image->refreshUpdated();
  157. }
  158. }
  159. // ...
  160. }
  161. We mark the ``file`` field as not required since we do not need the user to upload a
  162. new image every time the Image is updated. When a file is uploaded (and nothing else
  163. is changed on the form) there is no change to the data which Doctrine needs to persist
  164. so no ``preUpdate`` event would fire. To deal with this we hook into SonataAdmin's
  165. ``preUpdate`` event (which triggers every time the edit form is submitted) and use
  166. that to update an Image field which is persisted. This then ensures that Doctrine's
  167. lifecycle events are triggered and our Image manages the file upload as expected.
  168. And that is all there is to it!
  169. However, this method does not work when the ``ImageAdmin`` is embedded in other
  170. Admins using the ``sonata_type_admin`` field type. For that we need something more...
  171. Advanced example - works with embedded Admins
  172. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  173. When one Admin is embedded in another Admin, the child Admin's ``preUpdate()`` method is
  174. not triggered when the parent is submitted. To deal with this we need to use the parent
  175. Admin's lifecycle events to trigger the file management when needed.
  176. In this example we have a Page class which has three one-to-one Image relationships
  177. defined, linkedImage1 to linkedImage3. The PostAdmin class' form field configuration
  178. looks like this:
  179. .. code-block:: php
  180. <?php
  181. // src/AppBundle/Admin/PostAdmin.php
  182. class PostAdmin extends AbstractAdmin
  183. {
  184. protected function configureFormFields(FormMapper $formMapper)
  185. {
  186. $formMapper
  187. ->add('linkedImage1', 'sonata_type_admin', array(
  188. 'delete' => false
  189. ))
  190. ->add('linkedImage2', 'sonata_type_admin', array(
  191. 'delete' => false
  192. ))
  193. ->add('linkedImage3', 'sonata_type_admin', array(
  194. 'delete' => false
  195. ))
  196. // ...
  197. ;
  198. }
  199. // ...
  200. }
  201. This is easy enough - we have embedded three fields, which will then use our ``ImageAdmin``
  202. class to determine which fields to show.
  203. In our PostAdmin we then have the following code to manage the relationships' lifecycles:
  204. .. code-block:: php
  205. <?php
  206. // src/AppBundle/Admin/PostAdmin.php
  207. class PostAdmin extends AbstractAdmin
  208. {
  209. // ...
  210. public function prePersist($page)
  211. {
  212. $this->manageEmbeddedImageAdmins($page);
  213. }
  214. public function preUpdate($page)
  215. {
  216. $this->manageEmbeddedImageAdmins($page);
  217. }
  218. private function manageEmbeddedImageAdmins($page)
  219. {
  220. // Cycle through each field
  221. foreach ($this->getFormFieldDescriptions() as $fieldName => $fieldDescription) {
  222. // detect embedded Admins that manage Images
  223. if ($fieldDescription->getType() === 'sonata_type_admin' &&
  224. ($associationMapping = $fieldDescription->getAssociationMapping()) &&
  225. $associationMapping['targetEntity'] === 'AppBundle\Entity\Image'
  226. ) {
  227. $getter = 'get'.$fieldName;
  228. $setter = 'set'.$fieldName;
  229. /** @var Image $image */
  230. $image = $page->$getter();
  231. if ($image) {
  232. if ($image->getFile()) {
  233. // update the Image to trigger file management
  234. $image->refreshUpdated();
  235. } elseif (!$image->getFile() && !$image->getFilename()) {
  236. // prevent Sf/Sonata trying to create and persist an empty Image
  237. $page->$setter(null);
  238. }
  239. }
  240. }
  241. }
  242. }
  243. // ...
  244. }
  245. Here we loop through the fields of our PageAdmin and look for ones which are ``sonata_type_admin``
  246. fields which have embedded an Admin which manages an Image.
  247. Once we have those fields we use the ``$fieldName`` to build strings which refer to our accessor
  248. and mutator methods. For example we might end up with ``getlinkedImage1`` in ``$getter``. Using
  249. this accessor we can get the actual Image object from the Page object under management by the
  250. PageAdmin. Inspecting this object reveals whether it has a pending file upload - if it does we
  251. trigger the same ``refreshUpdated()`` method as before.
  252. The final check is to prevent a glitch where Symfony tries to create blank Images when nothing
  253. has been entered in the form. We detect this case and null the relationship to stop this from
  254. happening.
  255. Notes
  256. -----
  257. If you are looking for richer media management functionality there is a complete SonataMediaBundle
  258. which caters to this need. It is documented online and is created and maintained by the same team
  259. as SonataAdmin.
  260. To learn how to add an image preview to your ImageAdmin take a look at the related cookbook entry.
  261. .. _`uploading files with Doctrine and Symfony`: http://symfony.com/doc/current/cookbook/doctrine/file_uploads.html