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!

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
    }
}