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.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

The 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.

Leave a comment!

This tutorial is built on Symfony 4 but works great in Symfony 5!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "aws/aws-sdk-php": "^3.87", // 3.87.10
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.9.0
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.22
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "liip/imagine-bundle": "^2.1", // 2.1.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.1.0
        "oneup/flysystem-bundle": "^3.0", // 3.0.3
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.4
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.2.3
        "symfony/console": "^4.0", // v4.2.3
        "symfony/flex": "^1.0", // v1.6.2
        "symfony/form": "^4.0", // v4.2.3
        "symfony/framework-bundle": "^4.0", // v4.2.3
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.2.3
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "^4.0", // v4.2.3
        "symfony/validator": "^4.0", // v4.2.3
        "symfony/web-server-bundle": "^4.0", // v4.2.3
        "symfony/yaml": "^4.0", // v4.2.3
        "twig/extensions": "^1.5" // v1.5.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.1.0
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.2.3
        "symfony/dotenv": "^4.0", // v4.2.3
        "symfony/maker-bundle": "^1.0", // v1.11.3
        "symfony/monolog-bundle": "^3.0", // v3.3.1
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.2.3
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.2.3
    }
}