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 SubscribeWe know that, on a technical level, the dashboard is the key to everything. All Crud controllers run in the context of the dashboard that link to them, which allows us to control things on a global level by adding methods to the dashboard controller.
But the dashboard is also... just a page! A page with a controller that's the homepage of our admin. And so, we can - and should - do something with that page!
The simplest option is just to redirect to a specific CRUD section... so that when the user goes to /admin
, they're immediately redirected to, for example, the question admin. In a little while, we'll learn how to generate URLs to specific Crud controllers.
Or to be a little more fun, we can render something real on this page. Let's do that: let's render some stats and a chart.
To get the stats that I want to show, we need to query the database. Specifically, we need to query from QuestionRepository
. DashboardController
is a normal controller... which means that it's also a service. And so, when a service needs access to other services, we use dependency injection!
Add a constructor... then autowire QuestionRepository $questionRepository
. I'll hit Alt+Enter and go to initialize properties to create that property and set it.
... lines 1 - 8 | |
use App\Repository\QuestionRepository; | |
... lines 10 - 22 | |
class DashboardController extends AbstractDashboardController | |
{ | |
private QuestionRepository $questionRepository; | |
public function __construct(QuestionRepository $questionRepository) | |
{ | |
$this->questionRepository = $questionRepository; | |
} | |
... lines 31 - 93 | |
} |
If you're wondering why I'm not using action injection - where we add the argument to the method - I'll explain why in a few minutes. But it is possible.
Before we render a template, let's prepare a few variables: $latestQuestions
equals $this->questionRepository->findLatest()
. That's a custom method I added before we started. Also set $topVoted
to $this->questionRepository->findTopVoted()
: another custom method.
... lines 1 - 33 | |
public function index(): Response | |
{ | |
$latestQuestions = $this->questionRepository | |
->findLatest(); | |
$topVoted = $this->questionRepository | |
->findTopVoted(); | |
... lines 40 - 44 | |
} | |
... lines 46 - 95 |
Finally, at the bottom, like almost any other controller, return $this->render()
to render, how about, admin/index.html.twig
. Pass in the two variables: latestQuestions
and topVoted
.
... lines 1 - 33 | |
public function index(): Response | |
{ | |
... lines 36 - 40 | |
return $this->render('admin/index.html.twig', [ | |
'latestQuestions' => $latestQuestions, | |
'topVoted' => $topVoted, | |
]); | |
} | |
... lines 46 - 95 |
Awesome! Let's go add that! In templates/admin/
, create a new index.html.twig
... and I'll paste in the contents.
{% extends '@EasyAdmin/page/content.html.twig' %} | |
{% block page_title %} | |
Cauldron Overflow Dashboard | |
{% endblock %} | |
{% block main %} | |
<div class="row"> | |
<div class="col-6"> | |
<h3>Latest Questions</h3> | |
<ol> | |
{% for question in latestQuestions %} | |
<li> | |
<a href="{{ path('app_question_show', {'slug': question.slug}) }}">{{ question.name }}</a> | |
<br>- {{ question.createdAt|date }} | |
</li> | |
{% endfor %} | |
</ol> | |
</div> | |
<div class="col-6"> | |
<h3>Top Voted</h3> | |
<ol> | |
{% for question in topVoted %} | |
<li> | |
<a href="{{ path('app_question_show', {'slug': question.slug}) }}">{{ question.name }}</a> ({{ question.votes }}) | |
</li> | |
{% endfor %} | |
</ol> | |
</div> | |
</div> | |
{% endblock %} |
But there's nothing tricky here. I am extending @EasyAdmin/page/content.html.twig
. If you ever need to render a custom page... but one that still looks like it lives inside the admin area, this is the template you want.
If you open it up... hmm, there's not much here! But check out the extends: ea.templatePath('layout')
. If you look in the views/
directory of the bundle itself, this is a fancy way of extending layout.html.twig
. And this is a great way to discover all of the different blocks that you can override.
Back in our template, the main
block holds the content, we loop over the latest questions... and the top voted. Very straightforward. And if you refresh the page, instead of the EasyAdmin welcome message, we see our stuff!
Let's have some fun and render a chart on this page. To do this, we'll use a Symfony UX library. At your terminal, run:
composer require symfony/ux-chartjs
While that's installing, I'll go to the GitHub page for this library and load up its documentation. These days, the docs live on symfony.com and you'll find a link there from here.
Ok, so after installing the library, we need to run:
yarn install --force
And then... sweet! Just like that, we have a new Stimulus controller that has the ability to render a chart via Chart.js.
But I don't want talk too much about this chart library. Instead, we're going to steal the example code from the docs. Notice that we need a service in order to build a chart called ChartBuilderInterface
. Add that as a second argument to the controller: ChartBuilderInterface $chartBuilder
. I'll hit Alt+Enter and go to initialize properties to create that property and set it.
... lines 1 - 21 | |
use Symfony\UX\Chartjs\Builder\ChartBuilderInterface; | |
... lines 23 - 24 | |
class DashboardController extends AbstractDashboardController | |
{ | |
... line 27 | |
private ChartBuilderInterface $chartBuilder; | |
... line 29 | |
public function __construct(QuestionRepository $questionRepository, ChartBuilderInterface $chartBuilder) | |
{ | |
... line 32 | |
$this->chartBuilder = $chartBuilder; | |
} | |
... lines 35 - 125 | |
} |
Then, all the way at the bottom... just to keep things clean... create a new private function called createChart()
... that will return a Chart
object. Now steal the example code from the docs - everything except for the render - paste it into the method... and, at the bottom return $chart
.
Oh, and $chartBuilder
needs to be $this->chartBuilder
. I'm not going to bother making any of this dynamic: I just want to see that the chart does render.
... lines 1 - 99 | |
private function createChart(): Chart | |
{ | |
$chart = $this->chartBuilder->createChart(Chart::TYPE_LINE); | |
$chart->setData([ | |
'labels' => ['January', 'February', 'March', 'April', 'May', 'June', 'July'], | |
'datasets' => [ | |
[ | |
'label' => 'My First dataset', | |
'backgroundColor' => 'rgb(255, 99, 132)', | |
'borderColor' => 'rgb(255, 99, 132)', | |
'data' => [0, 10, 5, 2, 20, 30, 45], | |
], | |
], | |
]); | |
$chart->setOptions([ | |
'scales' => [ | |
'y' => [ | |
'suggestedMin' => 0, | |
'suggestedMax' => 100, | |
], | |
], | |
]); | |
return $chart; | |
} | |
... lines 126 - 127 |
Back up in the index()
method, pass a new chart
variable to the template set to $this->createChart()
.
... lines 1 - 37 | |
public function index(): Response | |
{ | |
... lines 40 - 44 | |
return $this->render('admin/index.html.twig', [ | |
... lines 46 - 47 | |
'chart' => $this->createChart(), | |
]); | |
} | |
... lines 51 - 127 |
Finally, to render this, over in index.html.twig
, add one more div
with class="col-12"
... and, inside, render_chart(chart)
... where render_chart()
is a custom function that comes from the library that we just installed.
... lines 1 - 6 | |
{% block main %} | |
<div class="row"> | |
... lines 9 - 29 | |
<div class="col-12"> | |
{{ render_chart(chart) }} | |
</div> | |
</div> | |
{% endblock %} |
And... that should be it! Find your browser, refresh and... nothing! Um, force refresh? Still nothing. In the console... a big error.
Ok, over in the terminal tab that holds Encore, it wants me to run yarn install --force
... which I already did. Hit Ctrl+C to stop Encore... then restart it so that it sees the new files from the UX library:
yarn watch
And... yes! Build successful. And in the browser... we have a chart!
Next: let's do the shortest chapter ever where we talk about the pros, cons and limitations of injecting services into the action methods of your admin controllers versus through the constructor.
Hey @Delenclos-A
You'll need to use the AdminUrlGenerator
service https://symfony.com/bundles/EasyAdminBundle/current/crud.html#generating-admin-urls
It has a setRoute()
method that allows you te define the route name and parameters, then, you get back the final URL, you need to call generateUrl()
I hope it helps. Cheers!
I have followed all the steps, upgraded everything, and I am not able to see any chart. There are no errors displayed in the console or the development panel. I have executed commands like yarn install --force
and yarn watch
, but the chart still doesn't appear. Has anyone successfully run it recently with the current library versions?
The environment I'm using is as follows: PHP 8.1.13, Symfony 6.0.2, Google Chrome, and Mac OS X.
Hey Marek-J,
Did you download the course code and start following the course from the start/ directory? Did you run composer update
command or only composer install
?
Could you try to clear the cache also with rm -rf var/cache/
just in case? Also, try to open the page in Chrome Incognito mode, it might still be browser cache, though force refresh should do the trick I think.
Cheers!
I downloaded Start/ at the beginning of the course and followed the coding instructions up until this chapter. Now I run 'composer udpate', rm -rf var/cache, repeated yarn install --force and yarn watch, forced cleard cache, opened chrome in icognito mode, but it still doesn't work without any error.
I checked source code and I have chart code, but I'cant see it.
<div class="col-12"><canvas data-controller="symfony--ux-chartjs--chart" data-symfony--ux-chartjs--chart-view-value="{"type":"line","data":{"labels":["January","February","March","April","May","June","July"],"datasets":[{"label":"My First dataset","backgroundColor":"rgb(255, 99, 132)","borderColor":"rgb(255, 99, 132)","data":[0,10,5,2,20,30,45]}]},"options":{"scales":{"y":{"suggestedMin":0,"suggestedMax":100}}}}"></canvas>
</div>
Hey Marek-J,
Ah, we do not recommend to run composer update
command while following the course because it may lead to some unexpected BC breaks. If it possible - I'd recommend you to revert changes that composer update
did and just do composer install
or composer require
if it's shown in the videos. This guarantee that you will have the exact code that the course author has in the video.
It's difficult to say what changes you need to do after you upgraded your project to newer versions.
Cheers!
Hi, I read all the tutorial and it's very hard for me (english and EasyAdmin). I have worked with SF2 (in the past). I appreciate tutorials because you go on detal and it makes sense to develop comprehension.
Imagine many admins who,works in one or many countries.
So they have to manage only their countries custommers, products...
I imagine that all is starting in the Dashboard. A query is generate to get all the entities we need in the cruds and only for the connected adminUser country.
I don't know how to do that. If someone can help me I appreciate. Thank's
Hey @Delenclos-A ,
Actually, not really in the Dashboard :) I suppose you want the Dashboard will look the same for all your customers, i.e. same links to the users list page, etc. But when you click on that Users link - you will see only users related to your country. So, this could be done by overriding the index page query builder in your UsersCrudController
, i.e. specifically createIndexQueryBuilder()
method - you can add any special conditions to the query to filter the list of users returned. But you would probably want to add some extra checks to avoid guessing the user URL in the address bar, otherwise some admins may open not their own users by going directly to the user id page, i.e. changing entity_id query parameter in the addres bar :)
Actually, I would recommend you to watch this tutorial till the end, we're talking about restricting access to some users in it :)
Cheers!
For me nether the simulus app for the text-field-preview from the lesson before or the chart is working. No error in the console, nothing.
Tried several package-updates or copying the original code from the zip-file. must be missing something here. node v16, npm v8.18 and yarn v 1.21
not that important to me. more a nice-to-have.
Hey @Ek24!
Well that is no fun! Hmm. Here are some things to check:
A) First, do you see the data-controller="" attribute in the HTML on the rendered page? This is kind of obvious.... we just want to make sure that the data-controller IS actually rendering.
B) If data-controller IS rendering, then it means that the corresponding Stimulus controllers are missing for some reasons. If the Stimulus controllers were registered, then they would at least load and then have an error. Not seeing anything means that either the data-controller="" is missing, the Stimulus controller is missing, or there is a naming mismatch between them. But the chart controller name is all handled pretty automatically, so I doubt that it's a name mismatch.
So... my guess is that the problem is (B) that, somehow, the Stimulus controllers are missing. I'm not really sure how that could happen - but I would "back up" and try creating a really simple assets/controllers/hello_controller.js
with:
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
connect() {
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
}
}
Then, on a non-admin page, add <div data-controller="hello">
onto the page. When you refresh, do you see the text above added to the element? We're testing to see if the Stimulus controllers are working at all.
Oh, ALSO make sure you're including the Encore assets in your admin area - https://symfonycasts.com/screencast/easyadminbundle/assets#codeblock-9cb529831f - in that case, we created a new "admin" entry to hold the admin stuff (but use addEntry()
not addStyleEntry()
as you'll also be including JavaScript... and point addEntry()
at admin.js
like we do here https://symfonycasts.com/screencast/easyadminbundle/field-javascript#codeblock-0353f83ac2). To make sure Stimulus is loading, that file needs to import the ./bootstrap
file - we show that later - https://symfonycasts.com/screencast/easyadminbundle/field-javascript#codeblock-b7c4c00273
Let me know if that helps! Cheers!
Hello, so for anyone having the same issue, like I was, this was the answer that helped me. If you want the easy steps to follow, here is what I did (I am using Symfony 6.2.6, PHP 8.2.3, Node v19.7.0 and NPM 9.6.0) :
assets
directory (name it however you want, here let's say it's dashboard.js)import './bootstrap';
webpack.config.js
file and add this entry : .addEntry('dashboard', './assets/dashboard.js')
src/Admin/DashboardController.php
:public function configureAssets(): Assets
{
return parent::configureAssets()
->addWebpackEncoreEntry('dashboard')
;
}
(Don't forget to import Assets
)
symfony serve
& npm run watch
Hi @Faez-B!
Thanks for posting that! I can't remember why I didn't need to do this in the actual tutorial, but this is sound advice. The point is: somehow, you need to make sure the Stimulus engine is running. Creating an admin-specific entry (that imports './bootstrap'
) like you suggest is a completely valid way to do that. The other way would be to make sure your normal app
entrypoint is included in your admin area, though this could also introduce extra styling from your frontend that you don't want.
Anyways - good stuff, thanks for sharing this 👍!
Hi, I have problem with displaying chart (I use Mac, Safari) I can see that render is complete
<div class="col-12">
<canvas data-controller="symfony--ux-chartjs--chart" data-symfony--ux-chartjs--chart-view-value="{"type":"line","data":{"labels":["January","February","March","April","May","June","July"],"datasets":[{"label":"My First dataset","backgroundColor":"rgb(255, 99, 132)","borderColor":"rgb(255, 99, 132)","data":[0,10,5,2,20,30,45]}]},"options":{"scales":{"y":{"suggestedMin":0,"suggestedMax":100}}}}"></canvas>
</div>
But I can not see the result, try yarn install --force and after yarn watch, no result. In console no errors...
What I do wrong ?
Hey Mepcuk!
Sorry for the slow reply! Hmm. When absolutely nothing happens (including no errors), it "feels" like the Stimulus controller isn't been registered. What does your controllers.json
file look like? Also package.json
? Something is short-circuiting I think... but I'm not sure what. Also, if you have the latest of all of the js packages (e.g. run yarn upgrade
or npm update
), then you should see some console logs from Stimulus itself. It'll say something about Stimulus "starting" and then tell you about any controllers that are starting. Even if the controller is missing, if Stimulus is running, you should see some logs from Stimulus about IT booting up.
Let me know what you find out :).
Cheers!
Solved - S6 Php8.1 - need to restart local web server (symfony serve) - any commands like yarn watch and other not help me.
Hey Maxim,
Thank you for sharing this tip with others! I'm happy to hear it fixed the problem for you
Cheers!
Do you know why I'm not getting autocompletion for path() within the admin twig templates in phpstorm anymore and how to fix it?
Unfortunately, I don't know how to fix it :/. That's... surprising that you've lost auto-complete... and I'd definitely check that the Symfony plugin is enabled for the project. But sometimes, autocompletion in Twig gets a bit inconsistent (it's usually inconsistent with what variables it autocompletes... which makes sense, since it's tricky to figure out which controller is rendering the template... to know which variables it's providing), but I don't have as many problems with actual Twig function, like path().
Cheers!
oh, the path() function itself autocompletes, but none of the route names are autocompleting within the call to path.
Actually that's not true, only the paths in the Controller/Admin directory controllers as well as web profiler paths; but none of the paths in the controllers in the Controllers directory like "app_profiler_show" and "app_homepage". The symfony plugin is enabled
Ah yes, this happens to me sometimes too... but I'm not sure why. This magic comes from the Symfony plugin... so something isn't quite right there, but I don't know how you'd go about fixing or debugging that :/.
Many thanks! Could you please share with us the way you have created findLatest() or topVoted() methods please?
Hey Lubna
There it is:
public function findLatest(): array
{
return $this->createQueryBuilder('question')
->orderBy('question.createdAt', 'DESC')
->setMaxResults(3)
->getQuery()
->getResult();
}
public function findTopVoted(): array
{
return $this->createQueryBuilder('question')
->orderBy('question.votes', 'DESC')
->setMaxResults(5)
->getQuery()
->getResult();
}
By the way, you can find them inside the QuestionRepository.php
file.
Cheers!
Dear all, thanks for all those great courses. I have a doubt: I can set different controllers for the same entity (alternate detail pages), but can I set a common url for the actions?
Hey @Luc!
Hmm. It sounds like you want to, for example, "share" (for example) the "edit" action (or some other action) between two Crud controllers for the same entity? If so, I do not believe this is possible: the controllers operate completely separately. However, if you have custom logic inside of one of the actions that you want to share, you could certainly add that custom logic to one of your Crud controllers and make the other *extend* that controller to share the logic.
Let me know if that helps :).
Cheers!
// composer.json
{
"require": {
"php": ">=8.1.0",
"ext-ctype": "*",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99.4
"doctrine/doctrine-bundle": "^2.1", // 2.5.5
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.2.1
"doctrine/orm": "^2.7", // 2.10.4
"easycorp/easyadmin-bundle": "^4.0", // v4.0.2
"handcraftedinthealps/goodby-csv": "^1.4", // 1.4.0
"knplabs/knp-markdown-bundle": "dev-symfony6", // dev-symfony6
"knplabs/knp-time-bundle": "^1.11", // 1.17.0
"sensio/framework-extra-bundle": "^6.0", // v6.2.5
"stof/doctrine-extensions-bundle": "^1.4", // v1.7.0
"symfony/asset": "6.0.*", // v6.0.1
"symfony/console": "6.0.*", // v6.0.2
"symfony/dotenv": "6.0.*", // v6.0.2
"symfony/flex": "^2.0.0", // v2.0.1
"symfony/framework-bundle": "6.0.*", // v6.0.2
"symfony/mime": "6.0.*", // v6.0.2
"symfony/monolog-bundle": "^3.0", // v3.7.1
"symfony/runtime": "6.0.*", // v6.0.0
"symfony/security-bundle": "6.0.*", // v6.0.2
"symfony/stopwatch": "6.0.*", // v6.0.0
"symfony/twig-bundle": "6.0.*", // v6.0.1
"symfony/ux-chartjs": "^2.0", // v2.0.1
"symfony/webpack-encore-bundle": "^1.7", // v1.13.2
"symfony/yaml": "6.0.*", // v6.0.2
"twig/extra-bundle": "^2.12|^3.0", // v3.3.7
"twig/twig": "^2.12|^3.0" // v3.3.7
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.1
"symfony/debug-bundle": "6.0.*", // v6.0.2
"symfony/maker-bundle": "^1.15", // v1.36.4
"symfony/var-dumper": "6.0.*", // v6.0.2
"symfony/web-profiler-bundle": "6.0.*", // v6.0.2
"zenstruck/foundry": "^1.1" // v1.16.0
}
}
Hi team,
I'm trying to create a menuItem::linktoCrud with parameter for an Index Page. In a classic controller you can do that easily and get it in the route. I try it with a linkToUrl and it's works fine. However I don't know how to do that in EA. Have you got a solution? How to add a parameter in the url, the same as setEntityId() (or to disabled test when loading setEntity()... CrudMenuItem is a final class so I can't override it (I'm not sure because it's hard for me). Thak's for help