Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

The Dashboard Page

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 $12.00

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

Login Subscribe

We 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!

Adding a Chart!

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.

Leave a comment!

15
Login or Register to join the conversation
Ek24 Avatar
Ek24 Avatar Ek24 | posted 1 month ago | edited

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.

Reply

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!

Reply

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 ?

Reply

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!

Reply

Solved - S6 Php8.1 - need to restart local web server (symfony serve) - any commands like yarn watch and other not help me.

Reply

Hey Maxim,

Thank you for sharing this tip with others! I'm happy to hear it fixed the problem for you

Cheers!

Reply
Nick F. Avatar

Do you know why I'm not getting autocompletion for path() within the admin twig templates in phpstorm anymore and how to fix it?

Reply

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!

Reply
Nick F. Avatar

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

Reply

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 :/.

Reply

Many thanks! Could you please share with us the way you have created findLatest() or topVoted() methods please?

Reply

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!

1 Reply
Default user avatar

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?

Reply

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!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.0.2",
        "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
    }
}