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!
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.
Hi glioburd!
Thanks for the nice message and the note about the groups! I'm not sure why that change would be required... but we're going to take a look at it and see if we can repeat. If we can, we'll add a note to the tutorial :).
Cheers!
Hi again!
I just wanted to follow-up. I wasn't able to repeat this issue at the step in the tutorial after upgrading to Symfony 5.3... so I'm not sure what's going on :). Passing ['groups' => ['main']]
and >['groups' => 'main']` behave identically for me... which is what I would expect - this is the deep, dark function internally that fetches these and makes sure they're an array if you pass a scalar: https://github.com/symfony/symfony/blob/d679ac56503c54724515044551b61279bf23f0ef/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php#L267-L272
So, who know what happened - but I think things are all good here. But if you do ever run into this again and can repeat it, let us know1
Cheers!
How could you parse violations into json format as {"type"=>"","title"=>"","detail"=>""}?
I tried . It return like an json array with lots message.
[ 0 => {
cause
code
constraint
binaryFormat
defaultOption
disallowEmptyMessage
maxSizeMessage
mimeTypes
mimeTypesMessage
notFoundMessage: "The file could not be found."
notReadableMessage
payload
requiredOptions
targets: "property"
uploadCantWriteErrorMessage
uploadErrorMessage
uploadExtensionErrorMessage
uploadFormSizeErrorMessage
uploadIniSizeErrorMessage
uploadNoFileErrorMessage
uploadNoTmpDirErrorMessage: "No temporary folder was configured in php.ini."
uploadPartialErrorMessage: "The file was only partially uploaded."
invalidValue
message
messageTemplate
parameters
plural
propertyPath
root
}
]
Hey Wei lun L.
Yep, as you already know from that blog post, the ability to directly serialize a ConstraintViolationList
object it's a feature of Symfony 4.1
Cheers!
File upload works fine in Chrome but having an issue in IE when I select multiple files they are not uploaded (file is visible in Dropzone with Cross "X") with this Error message:
{"type":"https:\/\/symfony.com\/errors\/validation","title":"Validation Failed","detail":"Please select a file to upload","violations":[{"propertyPath":"","title":"Please select a file to upload","type":"urn:uuid:c1051bb4-d103-4f74-8988-acbcafc7fdc3"}]}
my feeling is that Ie doesnt support those arrow functions...
I dont use IE so I dont need a fix but just saying for those who might be
Hey Peter K.
Thanks for sharing it. If the problem is what you believe, then using a polyfill might solve the issue, or maybe, Dropzone library has a bug while working on IE? Anyways, in my opinion we should let IE just die in peace:) hehehe
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"aws/aws-sdk-php": "^3.87", // 3.87.10
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"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.9", // v1.17.6
"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
}
}
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!