API Endpoint & Errors with Dropzone
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThe AJAX upload finishes successfully... but the response is a redirect... which doesn't break anything technically... but it's weird. Our endpoint isn't setup to be an API endpoint - it's 100% traditional: we're redirecting on error and success.
But now that we are using this as an API endpoint, let's fix that! And... this kinda simplifies things. For the validation error, we can say return $this->json($violations, 400)
.
// ... lines 1 - 18 | |
class ArticleReferenceAdminController extends BaseController | |
{ | |
// ... lines 21 - 24 | |
public function uploadArticleReference(Article $article, Request $request, UploaderHelper $uploaderHelper, EntityManagerInterface $entityManager, ValidatorInterface $validator) | |
{ | |
// ... lines 27 - 51 | |
if ($violations->count() > 0) { | |
return $this->json($violations, 400); | |
} | |
// ... lines 55 - 66 | |
} | |
// ... lines 68 - 91 | |
} |
How nice is that? And at the bottom, we don't really need to return anything yet, but it's pretty standard to return the JSON of a resource after creating it. So, return $this->json($articleReference)
.
// ... lines 1 - 18 | |
class ArticleReferenceAdminController extends BaseController | |
{ | |
// ... lines 21 - 24 | |
public function uploadArticleReference(Article $article, Request $request, UploaderHelper $uploaderHelper, EntityManagerInterface $entityManager, ValidatorInterface $validator) | |
{ | |
// ... lines 27 - 64 | |
return $this->json($articleReference); | |
} | |
// ... lines 68 - 91 | |
} |
Let's try it! Move over, refresh... even though we don't need to... and select astronaut.jpg
. This time... it fails! Let's see what the error looks like. Hmm, actually, better: click to open the profiler - you can always see the error there. Oh:
A circular reference has been detected when serializing object of class
Article
.
This is a super common problem with the serializer, and we saw it earlier. We're serializing ArticleReference
. And, by default, that will serialize all the properties that have getter methods... including the article
property. Then when it serializes the Article
, it finds the $articleReferences
property and tries to serialize the ArticleReference
objects... in an endless loop.
The easiest way to fix this is to define a serialization group. In ArticleReference
, above the id
property, add @Groups
and let's invent one called main
. Put this above all the fields that we actually want to serialize, how about $id
, $filename
, $originalFilename
and $mimeType
. We're not actually using the JSON response yet so it doesn't matter - but we will use it in a few minutes.
// ... lines 1 - 6 | |
use Symfony\Component\Serializer\Annotation\Groups; | |
// ... lines 8 - 11 | |
class ArticleReference | |
{ | |
/** | |
// ... lines 15 - 17 | |
* @Groups("main") | |
*/ | |
private $id; | |
// ... lines 21 - 27 | |
/** | |
// ... line 29 | |
* @Groups("main") | |
*/ | |
private $filename; | |
// ... line 33 | |
/** | |
// ... line 35 | |
* @Groups("main") | |
*/ | |
private $originalFilename; | |
// ... line 39 | |
/** | |
// ... line 41 | |
* @Groups("main") | |
*/ | |
private $mimeType; | |
// ... lines 45 - 100 | |
} |
Back in the controller, let's break this onto multiple lines. The second argument is the status code and we should actually use 201
- that's the proper status code when you've created a resource. Next is headers - we don't need anything custom, and, for context, add an array with groups
set to ['main']
.
// ... lines 1 - 18 | |
class ArticleReferenceAdminController extends BaseController | |
{ | |
// ... lines 21 - 24 | |
public function uploadArticleReference(Article $article, Request $request, UploaderHelper $uploaderHelper, EntityManagerInterface $entityManager, ValidatorInterface $validator) | |
{ | |
// ... lines 27 - 65 | |
return $this->json( | |
$articleReference, | |
201, | |
[], | |
[ | |
'groups' => ['main'] | |
] | |
); | |
} | |
// ... lines 75 - 98 | |
} |
Let's see if that fixed things. Close the profiler and select "stars". Duh - I totally forgot - the stars file is too big - you can see it failed. But when you hover over it... object Object
? That's not a great error message... We'll fix that in a minute.
Select Earth from the Moon.jpg
and... nice! It works and the JSON response looks awesome!
Displaying Errors Correctly
Ok, let's look back at what happened with stars. This failed validation and so the server returned a 400 status code. Dropzone did notice that - it knows it failed. But, by default, Dropzone expects the Response to be just a string with the error message, not a nice JSON structure with a detail
key like we have.
No worries: we just need a little extra JavaScript to help this along. Back in admin_article_form.js
, add another option called init
and set that to a function
.
// ... lines 1 - 31 | |
function initializeDropzone() { | |
// ... lines 33 - 37 | |
var dropzone = new Dropzone(formElement, { | |
// ... line 39 | |
init: function() { | |
// ... lines 41 - 45 | |
} | |
}); | |
} |
Dropzone calls this when it's setting itself up, and it's a great place to add extra behavior via events. For example, want to do something whenever there's an error? Call this.on('error')
and pass that a callback with two arguments: a file
object that holds details about the file that was uploaded and data
- the data sent back from the server.
// ... lines 1 - 31 | |
function initializeDropzone() { | |
// ... lines 33 - 37 | |
var dropzone = new Dropzone(formElement, { | |
// ... line 39 | |
init: function() { | |
this.on('error', function(file, data) { | |
// ... lines 42 - 44 | |
}); | |
} | |
}); | |
} |
Because the real validation message lives on the detail
key, we can say: if data.detail
, this.emit('error')
passing file
and the actual error message string: data.detail
.
// ... lines 1 - 31 | |
function initializeDropzone() { | |
// ... lines 33 - 37 | |
var dropzone = new Dropzone(formElement, { | |
// ... line 39 | |
init: function() { | |
this.on('error', function(file, data) { | |
if (data.detail) { | |
this.emit('error', file, data.detail); | |
} | |
}); | |
} | |
}); | |
} |
That's it! Refresh the whole thing... and upload the stars file again. It failed... but when we hover on it! Nice! There's our validation error.
Next: now that our files are automatically uploaded via AJAX, the reference list should also automatically update when each upload finishes. Let's render that whole section with JavaScript.
Hello, for information, on Symfony 5.2.6, I got an empty JSON in my response (for the part at 3:06).
It seems that now, in case of a single "group", you need to set the group directly in a string instead of an array.
More concisely:
`
return $this->json(
);
`
Instead of:
`
return $this->json(
);
`
Otherwise, you'll get an empty JSON as Response.
Also huge thanks to the SymfonyCasts crew, your tutorials are amazing!