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 SubscribeLet's upload all of these files. How nice is that? One fails because it's the wrong type and another fails because it's too big. But we get nice errors and all the rest worked. And this gives us a lot more to play with for reordering!
To make an AJAX call when we finishing dragging, add a new option: onEnd
set to an arrow function. Inside console.log(this.sortable)
- that's the sortable object we stored earlier .toArray()
.
... lines 1 - 34 | |
class ReferenceList | |
{ | |
constructor($element) { | |
... line 38 | |
this.sortable = Sortable.create(this.$element[0], { | |
... lines 40 - 41 | |
onEnd: () => { | |
console.log(this.sortable.toArray()); | |
} | |
}); | |
... lines 46 - 62 | |
} | |
... lines 64 - 117 | |
} | |
... lines 119 - 144 |
Check it out: refresh the page, drag one of these... and go look at the console. Woh! Those are the reference ids... in the right order! Try it again: move this one up and... yep! The id 11 just moved up a few spots.
But... how the heck is this working? How does sortable know what the ids are? Well, honestly... we got lucky. It knows thanks to the data-id
attribute that we put on each li! We added that for our own JavaScript... but the Sortable library also knows to read that!
This is amazing! This is the exact data we need to send to the server! Open up ArticleReferenceAdminController
and find downloadArticleReference()
. If you look closely, about half of the methods in this controller have an {id}
route wildcard where the id is for an ArticleReference
. Those endpoints are actions that operating on a single item. The other half of the endpoints, the ones on top, also have an {id}
wildcard, but these are for the Article
.
What about our new endpoint? We'll be reordering all of the references for one article... so it's a bit more like these ones on top. Copy this entire action for getting article references, change the name to reorderArticleReferences
and put /reorder
on the URL. Make this a method="POST"
and name it admin_article_reorder_references
.
... lines 1 - 20 | |
class ArticleReferenceAdminController extends BaseController | |
{ | |
... lines 23 - 93 | |
/** | |
* @Route("/admin/article/{id}/references/reorder", methods="POST", name="admin_article_reorder_references") | |
* @IsGranted("MANAGE", subject="article") | |
*/ | |
public function reorderArticleReferences(Article $article) | |
{ | |
return $this->json( | |
$article->getArticleReferences(), | |
200, | |
[], | |
[ | |
'groups' => ['main'] | |
] | |
); | |
} | |
... lines 109 - 184 | |
} |
If you're wondering about the URL or the method POST
, well, this endpoint isn't very RESTful.. it doesn't fit into the nice create-read-update-delete model... and that's ok. Usually when I have a weird endpoint like this, I use POST.
Inside the method, here's the plan: our JavaScript will send a JSON body containing an array of the ids in the right order. This array exactly. Add the Request
argument so we can get read that data and the EntityManagerInterface
so we can save stuff.
... lines 1 - 20 | |
class ArticleReferenceAdminController extends BaseController | |
{ | |
... lines 23 - 97 | |
public function reorderArticleReferences(Article $article, Request $request, EntityManagerInterface $entityManager) | |
{ | |
... lines 100 - 121 | |
} | |
... lines 123 - 198 | |
} |
To decode the JSON this time, it's so simple! I'm going to skip using Symfony's serializer. Say $orderedIds = json_decode()
passing that $request->getContent()
and true so it gives us an associative array.
... lines 1 - 20 | |
class ArticleReferenceAdminController extends BaseController | |
{ | |
... lines 23 - 97 | |
public function reorderArticleReferences(Article $article, Request $request, EntityManagerInterface $entityManager) | |
{ | |
$orderedIds = json_decode($request->getContent(), true); | |
... lines 101 - 121 | |
} | |
... lines 123 - 198 | |
} |
Then, if orderedIds === false
, something went wrong. Let's return this->json()
and, to at least somewhat match the validation responses we've had so far, let's set a detail key to, how about, Invalid body
with 400 for the status code.
... lines 1 - 20 | |
class ArticleReferenceAdminController extends BaseController | |
{ | |
... lines 23 - 97 | |
public function reorderArticleReferences(Article $article, Request $request, EntityManagerInterface $entityManager) | |
{ | |
$orderedIds = json_decode($request->getContent(), true); | |
if ($orderedIds === null) { | |
return $this->json(['detail' => 'Invalid body'], 400); | |
} | |
... lines 105 - 121 | |
} | |
... lines 123 - 198 | |
} |
Ok, cool: we've got the array of ids in the new order we want. Use this to say $orderedIds = array_flip($orderedIds)
. This deserves some explanation. The original array is a map from the position to the id - the keys are 0, 1, 2, 3 and so on. After the flip, we have a very handy array: the key is the id and the value is its new position.
... lines 1 - 20 | |
class ArticleReferenceAdminController extends BaseController | |
{ | |
... lines 23 - 97 | |
public function reorderArticleReferences(Article $article, Request $request, EntityManagerInterface $entityManager) | |
{ | |
$orderedIds = json_decode($request->getContent(), true); | |
if ($orderedIds === null) { | |
return $this->json(['detail' => 'Invalid body'], 400); | |
} | |
// from (position)=>(id) to (id)=>(position) | |
$orderedIds = array_flip($orderedIds); | |
... lines 108 - 121 | |
} | |
... lines 123 - 198 | |
} |
To use this, foreach
over $article->getArticleReferences() as $reference
. And inside, $reference->setPosition()
passing this $orderedIds[$reference->getId()]
to look up the new position.
... lines 1 - 20 | |
class ArticleReferenceAdminController extends BaseController | |
{ | |
... lines 23 - 97 | |
public function reorderArticleReferences(Article $article, Request $request, EntityManagerInterface $entityManager) | |
{ | |
$orderedIds = json_decode($request->getContent(), true); | |
if ($orderedIds === null) { | |
return $this->json(['detail' => 'Invalid body'], 400); | |
} | |
// from (position)=>(id) to (id)=>(position) | |
$orderedIds = array_flip($orderedIds); | |
foreach ($article->getArticleReferences() as $reference) { | |
$reference->setPosition($orderedIds[$reference->getId()]); | |
} | |
... lines 111 - 121 | |
} | |
... lines 123 - 198 | |
} |
And yes, we could code more defensively - like checking to make sure each array key was actually sent. And I would do that if this were a public API that other people used, or if invalid data could cause some harm.
Anyways, at the bottom, save: $entityManager->flush()
.
... lines 1 - 20 | |
class ArticleReferenceAdminController extends BaseController | |
{ | |
... lines 23 - 97 | |
public function reorderArticleReferences(Article $article, Request $request, EntityManagerInterface $entityManager) | |
{ | |
$orderedIds = json_decode($request->getContent(), true); | |
if ($orderedIds === null) { | |
return $this->json(['detail' => 'Invalid body'], 400); | |
} | |
// from (position)=>(id) to (id)=>(position) | |
$orderedIds = array_flip($orderedIds); | |
foreach ($article->getArticleReferences() as $reference) { | |
$reference->setPosition($orderedIds[$reference->getId()]); | |
} | |
$entityManager->flush(); | |
... lines 113 - 121 | |
} | |
... lines 123 - 198 | |
} |
Ok, let's hook up the JavaScript! Back in admin_article_form.js
, scroll up... let's see - find the onEnd()
of sortable. Say $.ajax()
and give this the url
key. For the URL, remember, the ul
element has a data-url
attribute, which is the path to the admin_article_list_references
route, so /admin/article/{id}/references
. Not by accident, the URL that we want is that plus /reorder
.
... lines 1 - 34 | |
class ReferenceList | |
{ | |
constructor($element) { | |
... lines 38 - 41 | |
onEnd: () => { | |
$.ajax({ | |
... lines 44 - 46 | |
}); | |
} | |
}); | |
... lines 50 - 66 | |
} | |
... lines 68 - 121 | |
} | |
... lines 123 - 148 |
So let's do a little bit of code re-use... and a little bit of hardcoding: in general, I don't worry too much about hardcoding URLs in JavaScript. Copy this.$element.data('url')
from below, paste, and add /reorder
. Then, method set to POST
and data
set to JSON.stringify(this.sortable.toArray())
.
... lines 1 - 34 | |
class ReferenceList | |
{ | |
constructor($element) { | |
... lines 38 - 41 | |
onEnd: () => { | |
$.ajax({ | |
url: this.$element.data('url')+'/reorder', | |
method: 'POST', | |
data: JSON.stringify(this.sortable.toArray()) | |
}); | |
} | |
}); | |
... lines 50 - 66 | |
} | |
... lines 68 - 121 | |
} | |
... lines 123 - 148 |
Ok, let's do this! Move over and refresh. No errors yet... Move "astronaut-1.jpg" down two spots and... hey! A 200 status code on that AJAX request! That's a good sign. Refresh and... aw! It's right back up on top!
Oh wait... the problem is that we're not rendering the list correctly! This list loads by making an Ajax request. In the controller... here's the endpoint: getArticleReferences()
. And it gets the data from $article->getArticleReferences()
. The problem is that this method doesn't know that it should order the reference's by position.
Open up the Article
entity and, above $articleReferences
, add @ORM\OrderBy({"position"="ASC"})
.
... lines 1 - 18 | |
class Article | |
{ | |
... lines 21 - 89 | |
/** | |
* @ORM\OneToMany(targetEntity="App\Entity\ArticleReference", mappedBy="article") | |
* @ORM\OrderBy({"position"="ASC"}) | |
*/ | |
private $articleReferences; | |
... lines 95 - 323 | |
} |
Let's go check out the endpoint: I'll click to open the URL in a new tab. Woohoo! astronaut-1.jpg
is third! Refresh the main page. Boom! The astronaut is right were we sorted it. Let's move it down a bit further... move the Symfony Best Practices up from the bottom and refresh. The sorting sticks. Awesome!
Next, instead of saving the uploaded files locally, let's upload them to AWS S3.
Hey Sylvain C.
What kind of assets do you have? I mean it's not a common situation when you are using var/
directory for public static assets. But if you have a private assets it was described in chapter 23/24. Oh and yes you can't use {{ asset() }}
for it, you should create a public endpoint and use {{ path|url() }}
functions
Cheers!
Sorry, but what about the real multiple files uploading?The Dropzone.js uploads the files one by one. Can you please advice how to count then the uploaded files (at once) on the server side?
Hey Anton,
I see it's a duplicate of https://symfonycasts.com/sc... - let's follow the conversation there as it's related to Dropzone. We will try to answer your question soon.
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 SFcasts team ! thx for this course. I have a question about assets paths.
In fact, i don't want to use S3, but actually when i want to show references in frontend, i don't success with traditionnal {{ asset(' ') }} code.
Because we are storing data in var directory, i thinks it's not possible to access in frontend.
Have you got a solution to fix this that ? In next chapter you are speaking about assets path but it's for S3.
Sorry for my english form Nice city (FR).