Skip to content

Lab 2: Building an Admin Panel

In this lab, you will build the foundation of an admin panel for your e-commerce application. You will create a dashboard controller and model that query real statistics from your database, set up a route group to organize all admin routes under the /admin prefix, and build a reusable admin layout with sidebar navigation. By the end of this lab, you will have a professional-looking admin dashboard ready for CRUD operations in future labs.


By completing this lab, you will:

  • Use Slim 4 route groups to organize related routes under a common prefix.
  • Build a dashboard controller that queries aggregate statistics from the database.
  • Use the BaseModel’s count() method to retrieve summary data.
  • Create a reusable admin layout with header, sidebar navigation, and footer.
  • Understand the data flow from controller to view using the $data array.
  • Apply htmlspecialchars() for secure output in views.

Before starting this lab, ensure you have:

  • A working Slim 4 application with the MVC structure.
  • The BaseModel and BaseController classes set up.
  • Bootstrap CSS included in your views.

In this part, you will create the categories and products tables in your database. These tables are needed for the admin dashboard to query statistics from.

Objective: Create the categories table to organize products into logical groups.

Instructions:

  1. Open phpMyAdmin and select your e-commerce database
  2. Go to the SQL tab and write a CREATE TABLE statement for the categories table with the following columns:
ColumnTypeConstraintsDescription
idINTPRIMARY KEY, AUTO_INCREMENTCategory ID
nameVARCHAR(100)UNIQUE, NOT NULLCategory name
descriptionTEXTNULLCategory description
created_atDATETIMEDEFAULT CURRENT_TIMESTAMPCreation timestamp
  1. Run the query and verify the table was created by checking the left sidebar in phpMyAdmin

Objective: Create the products table to store product information.

Instructions:

  1. In phpMyAdmin, go to the SQL tab and write a CREATE TABLE statement for the products table with the following columns:
ColumnTypeConstraintsDescription
idINTPRIMARY KEY, AUTO_INCREMENTProduct ID
category_idINTFOREIGN KEY → categories(id)Category this product belongs to
nameVARCHAR(100)NOT NULLProduct name
descriptionTEXTNULLProduct details
priceDECIMAL(10,2)NOT NULLProduct price
stock_quantityINTDEFAULT 0Units available
created_atDATETIMEDEFAULT CURRENT_TIMESTAMPCreation date
updated_atDATETIMEON UPDATE CURRENT_TIMESTAMPLast update date
  1. Run the query and verify the table was created and that the foreign key relationship to categories is in place

Objective: Add some sample data so the dashboard has something to display.

Instructions:

  1. Insert a few categories:
INSERT INTO categories (name, description) VALUES
('Electronics', 'Electronic devices and accessories'),
('Books', 'Physical and digital books'),
('Clothing', 'Apparel and fashion items');
  1. Insert a few products:
INSERT INTO products (category_id, name, description, price, stock_quantity) VALUES
(1, 'Wireless Mouse', 'Ergonomic wireless mouse', 29.99, 50),
(1, 'USB-C Hub', '7-in-1 USB-C adapter', 49.99, 3),
(2, 'PHP in Action', 'A comprehensive guide to PHP', 39.99, 15),
(3, 'Developer T-Shirt', '100% cotton, black', 24.99, 0);

Objective: Create a model that queries aggregate statistics from the database for the admin dashboard.

Instructions:

  1. Navigate to your app/Domain/Models/ directory

  2. Create a new file named AdminDashboardModel.php

  3. Set up the class:

    • Use the namespace App\Domain\Models
    • Import App\Helpers\Core\PDOService
    • Create a class AdminDashboardModel that extends BaseModel
    • Add a constructor that accepts a PDOService parameter and passes it to the parent constructor
  4. Implement the following two methods in the class:

    1. getTotalProducts(): int: Returns the total number of products in the database. Use $this->count() with a SELECT COUNT(*) query on the products table.

    2. getTotalCategories(): int: Returns the total number of categories. Follow the same pattern as getTotalProducts() but query the categories table.

  5. Save the file

Hints:


Step 5: Create the AdminDashboardController

Section titled “Step 5: Create the AdminDashboardController”

Objective: Create a controller that uses the AdminDashboardModel to fetch statistics and render the dashboard view.

Instructions:

  1. Navigate to your app/Controllers/ directory
  2. Create a new file named AdminDashboardController.php
  3. Copy and paste the following code:
app/Controllers/AdminDashboardController.php
<?php
namespace App\Controllers;
use App\Domain\Models\AdminDashboardModel;
use DI\Container;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class AdminDashboardController extends BaseController
{
public function __construct(
Container $container,
private AdminDashboardModel $dashboardModel
) {
parent::__construct($container);
}
/**
* Display the admin dashboard with e-commerce statistics.
*/
public function dashboard(Request $request, Response $response, array $args): Response
{
// TODO: Call the model methods to get dashboard statistics
// - Total products → $this->dashboardModel->getTotalProducts()
// - Total categories → $this->dashboardModel->getTotalCategories()
// TODO: Build a $data array with the following keys:
// - 'title' => 'Admin Dashboard'
// - 'totalProducts' => (from model)
// - 'totalCategories' => (from model)
// - 'username' => $_SESSION['username'] ?? 'Admin'
// TODO: Render the view 'admin/dashboardView.php' and pass the $data array
}
}
  1. Implement the TODO comments in the dashboard() method
  2. Save the file

Hints:

  • Use $_SESSION['username'] ?? 'Admin' to safely get the username (falls back to ‘Admin’ if not set)
  • Refer to the BaseController documentation for the render pattern

Objective: Create a route group for all admin routes under the /admin prefix.

Instructions:

  1. Open your app/Routes/web-routes.php file
  2. Add the following import at the top of the file:
use App\Controllers\AdminDashboardController;
  1. Add the admin route group:
app/Routes/web-routes.php
// Admin Panel Routes
$app->group('/admin', function ($group) {
// Dashboard route
$group->get('/dashboard', [AdminDashboardController::class, 'dashboard'])
->setName('admin.dashboard');
// TODO (future labs): Add CRUD routes for products, categories, orders, users
// $group->get('/products', [ProductsController::class, 'index'])
// ->setName('admin.products.index');
// $group->get('/categories', [CategoriesController::class, 'index'])
// ->setName('admin.categories.index');
// $group->get('/orders', [OrdersController::class, 'index'])
// ->setName('admin.orders.index');
// $group->get('/users', [UsersController::class, 'index'])
// ->setName('admin.users.index');
});
// Note: Later you'll add ->add(AdminAuthMiddleware::class) to protect these routes
  1. Save the file

Routes Created:

MethodURIController MethodRoute Name
GET/admin/dashboardAdminDashboardController::dashboard()admin.dashboard

Section titled “Step 7: Create the Admin Layout (Header + Footer)”

Objective: Create a reusable admin layout with a sidebar navigation and top bar that can be shared across all admin views.

Instructions:

  1. Navigate to your app/Views/ directory
  2. Create a new folder named admin/ (if it doesn’t already exist)
  1. Inside app/Views/admin/, create a file named adminHeader.php
  2. Copy and paste the following code:
app/Views/admin/adminHeader.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($title ?? 'Admin Panel') ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<style>
.admin-sidebar {
min-height: 100vh;
background-color: #212529;
}
.admin-sidebar .nav-link {
color: #adb5bd;
padding: 0.75rem 1rem;
border-radius: 0.25rem;
margin-bottom: 0.25rem;
}
.admin-sidebar .nav-link:hover,
.admin-sidebar .nav-link.active {
color: #fff;
background-color: #495057;
}
.admin-content {
background-color: #f8f9fa;
min-height: 100vh;
}
.stat-card {
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- Sidebar -->
<nav class="col-md-3 col-lg-2 d-md-block admin-sidebar p-0">
<div class="p-3">
<h5 class="text-white mb-0">Admin Panel</h5>
<small class="text-muted">E-Commerce Manager</small>
</div>
<hr class="text-secondary my-0">
<ul class="nav flex-column px-2 mt-2">
<li class="nav-item">
<a class="nav-link active" href="<?= APP_BASE_URL ?>/admin/dashboard">
Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?= APP_BASE_URL ?>/admin/products">
Products
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?= APP_BASE_URL ?>/admin/categories">
Categories
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?= APP_BASE_URL ?>/admin/orders">
Orders
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?= APP_BASE_URL ?>/admin/users">
Users
</a>
</li>
</ul>
<hr class="text-secondary">
<div class="px-2">
<a class="nav-link text-muted" href="<?= APP_BASE_URL ?>/">
&larr; Back to Store
</a>
</div>
</nav>
<!-- Main Content -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 admin-content">
<!-- Top bar -->
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><?= htmlspecialchars($title ?? 'Dashboard') ?></h1>
<div class="text-muted">
Logged in as: <strong><?= htmlspecialchars($username ?? 'Admin') ?></strong>
</div>
</div>
<!-- Flash Messages (uncomment when FlashMessage helper is available) -->
<!-- <div class="mb-3">
<?= App\Helpers\FlashMessage::render() ?>
</div> -->
<!-- Page content starts here -->
  1. Save the file
  1. Inside app/Views/admin/, create a file named adminFooter.php
  2. Copy and paste the following code:
app/Views/admin/adminFooter.php
<!-- Page content ends here -->
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
</body>
</html>
  1. Save the file

What you just created:

  • adminHeader.php: A reusable layout with a dark sidebar (navigation links to all admin sections), a top bar showing the page title and logged-in username, and a flash messages area.
  • adminFooter.php: Closes the HTML structure and includes Bootstrap JavaScript.
  • Together, they wrap any admin page content.
  1. Open app/Helpers/ViewHelper.php
  2. Add the following two methods to the class:
public static function loadAdminHeader(string $page_title): void
{
$page_title = $page_title ?? 'Admin Panel';
require_once APP_VIEWS_PATH . '/admin/adminHeader.php';
}
public static function loadAdminFooter(): void
{
require_once APP_VIEWS_PATH . '/admin/adminFooter.php';
}
  1. Save the file

Objective: Create the dashboard view that displays e-commerce statistics using the data passed from the controller.

Instructions:

  1. Inside app/Views/admin/, create a file named dashboardView.php
  2. Copy and paste the following code:
app/Views/admin/dashboardView.php
<?php
use App\Helpers\ViewHelper;
ViewHelper::loadAdminHeader($title);
?>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-3">
<div class="card stat-card border-primary">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">Total Products</h6>
<h3 class="mb-0"><?= htmlspecialchars($totalProducts) ?></h3>
</div>
<div class="text-primary fs-1">&#128230;</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card stat-card border-warning">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">Categories</h6>
<h3 class="mb-0"><?= htmlspecialchars($totalCategories) ?></h3>
</div>
<div class="text-warning fs-1">&#128193;</div>
</div>
</div>
</div>
</div>
</div>
<?php ViewHelper::loadAdminFooter(); ?>
  1. Save the file

What you just created:

  • A dashboard view that includes the reusable admin header and footer
  • Two stat cards showing totals for products and categories

Key PHP features used in this view:

FeatureExamplePurpose
ViewHelperViewHelper::loadAdminHeader($title)Reusable layout pattern
htmlspecialchars()htmlspecialchars($totalProducts)Prevent XSS attacks

Request: GET /admin/dashboard
Route Group (/admin)
AdminDashboardController::dashboard()
AdminDashboardModel (queries database statistics)
dashboardView.php (renders HTML with stats)
Response sent to browser