Lab 3: Building an Admin Panel
Additional Resources
Section titled “Additional Resources”- Slim 4 Route Groups ↗
- Working with the BaseModel class
- Working with the BaseController class
- Database Tables
Overview
Section titled “Overview”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.
Learning Objectives
Section titled “Learning Objectives”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
$dataarray. - Apply
htmlspecialchars()for secure output in views.
Prerequisites
Section titled “Prerequisites”Before starting this lab, ensure you have:
- Completed Lab 2: Master-Details with MVC, or an equivalent working Slim 4 application built on the MVC starter project.
- The BaseModel and BaseController classes set up.
- Your e-commerce database created, and its name set in
config/settings.dev.php.
Part I: Create the Database Tables
Section titled “Part I: Create the Database Tables”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.
Step 1: Create the Categories Table
Section titled “Step 1: Create the Categories Table”Objective: Create the categories table to organize products into logical groups.
Instructions:
- Open phpMyAdmin and select your e-commerce database
- Go to the SQL tab and write a
CREATE TABLEstatement for thecategoriestable with the following columns:
| Column | Type | Constraints | Description |
|---|---|---|---|
id | INT | PRIMARY KEY, AUTO_INCREMENT | Category ID |
name | VARCHAR(100) | UNIQUE, NOT NULL | Category name |
description | TEXT | NULL | Category description |
created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Creation timestamp |
- Run the query and verify the table was created by checking the left sidebar in phpMyAdmin
Step 2: Create the Products Table
Section titled “Step 2: Create the Products Table”Objective: Create the products table to store product information.
Instructions:
- In phpMyAdmin, go to the SQL tab and write a
CREATE TABLEstatement for theproductstable with the following columns:
| Column | Type | Constraints | Description |
|---|---|---|---|
id | INT | PRIMARY KEY, AUTO_INCREMENT | Product ID |
category_id | INT | FOREIGN KEY → categories(id) | Category this product belongs to |
name | VARCHAR(100) | NOT NULL | Product name |
description | TEXT | NULL | Product details |
price | DECIMAL(10,2) | NOT NULL | Product price |
stock_quantity | INT | DEFAULT 0 | Units available |
created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Creation date |
updated_at | DATETIME | ON UPDATE CURRENT_TIMESTAMP | Last update date |
- Run the query and verify the table was created and that the foreign key relationship to
categoriesis in place
Step 3: Insert Sample Data
Section titled “Step 3: Insert Sample Data”Objective: Add some sample data so the dashboard has something to display.
Instructions:
- 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');- 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);Part II: Build the Admin Panel
Section titled “Part II: Build the Admin Panel”Step 4: Create the AdminDashboardModel
Section titled “Step 4: Create the AdminDashboardModel”Objective: Create a model that queries aggregate statistics from the database for the admin dashboard.
Instructions:
-
Navigate to your
app/Domain/Models/directory -
Create a new file named
AdminDashboardModel.php -
Set up the class:
- Use the namespace
App\Domain\Models - Import
App\Helpers\Core\PDOService - Create a class
AdminDashboardModelthat extendsBaseModel - Add a constructor that accepts a
PDOServiceparameter and passes it to the parent constructor
- Use the namespace
-
Implement the following two methods in the class:
-
getTotalProducts(): int: Returns the total number of products in the database. Use$this->count()with aSELECT COUNT(*)query on theproductstable. -
getTotalCategories(): int: Returns the total number of categories. Follow the same pattern asgetTotalProducts()but query thecategoriestable.
-
-
Save the file
Hints:
- Refer to the BaseModel documentation for method signatures and examples
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:
- Navigate to your
app/Controllers/directory - Create a new file named
AdminDashboardController.php - Create a class named
AdminDashboardControllerthat extendsBaseController - The constructor should accept a
Containerinstance (passed to the parent constructor) and anAdminDashboardModelinstance (stored as a property via dependency injection), following the same pattern you used forShopsControllerin Lab 2 - Implement the following method, using the standard Slim callback signature
(Request $request, Response $response, array $args): Response:
| Method | What It Should Do |
|---|---|
dashboard() | Call the model’s getTotalProducts() and getTotalCategories() methods. Build a $data array with the keys listed below. Render the admin/dashboardView.php view by calling $this->render($response, 'admin/dashboardView.php', $data) and return the response. The $data array must be passed as the third argument, otherwise the view will not have access to the statistics. |
The $data array must contain the following keys:
| Key | Value |
|---|---|
title | 'Admin Dashboard' |
totalProducts | The value returned by getTotalProducts() |
totalCategories | The value returned by getTotalCategories() |
username | $_SESSION['username'] ?? 'Admin' |
- Save the file
Required use imports:
App\Domain\Models\AdminDashboardModelDI\ContainerPsr\Http\Message\ResponseInterface as ResponsePsr\Http\Message\ServerRequestInterface as Request
Hints:
$_SESSION['username'] ?? 'Admin'safely reads the username and falls back to'Admin'if no session value is set. Since authentication is added in a later lab, you will see'Admin'for now.- The key names in
$datamatter: the view in Step 8 expects exactly$title,$totalProducts,$totalCategories, and$username. Each key becomes a variable of the same name in the view, but only if you pass$datatorender(). - Refer to the BaseController documentation for the render pattern
Step 6: Set Up the Admin Route Group
Section titled “Step 6: Set Up the Admin Route Group”Objective: Create a route group for all admin routes under the /admin prefix.
Instructions:
- Open your
app/Routes/web-routes.phpfile - Add the following import at the top of the file:
use App\Controllers\AdminDashboardController;- Add the admin route group inside the
return static function (Slim\App $app): void { ... }closure, after the existing routes:
// 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- Save the file
Routes Created:
| Method | URI | Controller Method | Route Name |
|---|---|---|---|
| GET | /admin/dashboard | AdminDashboardController::dashboard() | admin.dashboard |
Step 7: Create the Admin Layout (Header + Footer)
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:
- Navigate to your
app/Views/directory - Create a new folder named
admin/(if it doesn’t already exist)
Step 7.1: Create the Admin Header
Section titled “Step 7.1: Create the Admin Header”- Inside
app/Views/admin/, create a file namedadminHeader.php - Copy and paste the following code:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?= htmlspecialchars($page_title) ?></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 ?>/"> ← 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($page_title) ?></h1> <div class="text-muted"> Logged in as: <strong><?= htmlspecialchars($username) ?></strong> </div> </div>
<?php // Flash messages will be rendered here in a later lab. ?>
<!-- Page content starts here -->- Save the file
Step 7.2: Create the Admin Footer
Section titled “Step 7.2: Create the Admin Footer”- Inside
app/Views/admin/, create a file namedadminFooter.php - Copy and paste the following code:
<!-- 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>- 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.
Step 7.3: Add Admin Methods to ViewHelper
Section titled “Step 7.3: Add Admin Methods to ViewHelper”- Open
app/Helpers/ViewHelper.php - Add the following two methods to the class:
public static function loadAdminHeader(string $page_title, string $username = 'Admin'): void{ require_once APP_VIEWS_PATH . '/admin/adminHeader.php';}
public static function loadAdminFooter(): void{ require_once APP_VIEWS_PATH . '/admin/adminFooter.php';}- Save the file
Step 8: Create the Dashboard View
Section titled “Step 8: Create the Dashboard View”Objective: Create the dashboard view that displays e-commerce statistics using the data passed from the controller.
Instructions:
- Inside
app/Views/admin/, create a file nameddashboardView.php - Copy and paste the following code:
<?php
use App\Helpers\ViewHelper;
ViewHelper::loadAdminHeader($title, $username);?>
<!-- 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">📦</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">📁</div> </div> </div> </div> </div></div>
<?php ViewHelper::loadAdminFooter(); ?>- 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:
| Feature | Example | Purpose |
|---|---|---|
ViewHelper | ViewHelper::loadAdminHeader($title, $username) | Reusable layout pattern |
htmlspecialchars() | htmlspecialchars($totalProducts) | Prevent XSS attacks |
Step 9: Test Your Dashboard
Section titled “Step 9: Test Your Dashboard”- Start your server and visit
http://localhost/your-app-name/admin/dashboardin your browser (replaceyour-app-namewith your project’s folder name inhtdocs/). - You should see the dark sidebar, the “Admin Dashboard” title in the top bar, “Logged in as: Admin”, and two stat cards.
- With the sample data from Step 3, the cards should show 4 total products and 3 categories.
- Add another row to the
productstable in phpMyAdmin and refresh the page. The product count should increase.
If you get an error, check the following in order: the route group is inside the closure in web-routes.php, the controller imports AdminDashboardModel, the model’s SQL table names match your database, and the view calls loadAdminHeader($title, $username).
Understanding What You Built
Section titled “Understanding What You Built”Architecture Overview
Section titled “Architecture Overview”Request: GET /admin/dashboard ↓Route Group (/admin) ↓AdminDashboardController::dashboard() ↓AdminDashboardModel (queries database statistics) ↓dashboardView.php (renders HTML with stats) ↓Response sent to browser