Lab 1: Implementing the Master-Details Pattern using MVC
Overview
Section titled “Overview”You will be working with the brew_finder database, which contains information about cafes across Quebec.
You will implement a master-details interface with the following views:
- Master View: Displays a searchable, filterable list of all cafes with key information.
- Details View: Shows comprehensive information about a selected cafe, including drinks, reviews, and events.
Learning Objectives
Section titled “Learning Objectives”By completing this lab, you will be able to:
- Implement the master-details design pattern in a PHP web application.
- Define and manage routes using the Slim Framework.
- Work with database queries to retrieve and display related data.
- Handle form submissions with proper validation and error handling.
- Apply the Model-View-Controller (MVC) pattern to structure your application.
- Build a responsive user interface that presents master-details relationships effectively.
- Structure PHP applications with modular, reusable components.
Prerequisites
Section titled “Prerequisites”- Wampoon and VS Code.
- Composer (bundled with Wampoon).
- The
brew_finderdatabase imported into MariaDB.
Getting Started
Section titled “Getting Started”You will be using the Slim MVC ↗ starter template to scaffold your project.
- Open a terminal and navigate to your
htdocs/directory:
cd C:/wampoon/htdocs- Run the following command to create a new project:
composer create-project frostybee/slim-mvc brew-finder-app- Open the
config/env.phpfile and update your database credentials to connect to thebrew_finderdatabase. - Verify the application runs by visiting
http://localhost/brew-finder-app/in your browser.
Project Structure:
brew-finder-app/├── app/│ ├── Controllers/ ← Your controllers go here│ ├── Domain/│ │ ├── Models/ ← Your models go here│ │ └── Services/│ ├── Helpers/│ ├── Middleware/│ ├── Routes/ ← Define your routes here│ └── Views/ ← Your view templates go here├── config/ ← Database and app configuration├── public/│ ├── assets/ ← CSS and JS files│ └── index.php ← Application entry point└── composer.jsonAdditional Resources
Section titled “Additional Resources”- Working with the BaseModel class. Available methods:
selectAll(),selectOne(),count(),execute(). - Working with the BaseController class. Available methods:
render(),redirect(). - Routing Concept. Route parameters, named routes, and route groups.
Lab Steps
Section titled “Lab Steps”Step 1: Create the ShopsModel
Section titled “Step 1: Create the ShopsModel”Objective: Create the model layer with methods to query the cafes table from the brew_finder database.
Instructions:
- Navigate to your
app/Domain/Models/directory - Create a new file named
ShopsModel.php - Copy and paste the following skeleton code:
<?php
namespace App\Domain\Models;
use App\Helpers\Core\PDOService;
class ShopsModel extends BaseModel{ public function __construct(PDOService $db_service) { parent::__construct($db_service); }
/** * Retrieve all cafes from the database. * @return array List of all cafes */ public function getAllCafes(): array { // TODO: Use $this->selectAll() to fetch all cafes // Order results by name ascending }
/** * Retrieve a single cafe by its ID. * @param int $id The cafe ID * @return array|false The cafe record or false if not found */ public function getCafeById(int $id): array|false { // TODO: Use $this->selectOne() with a named parameter // to fetch a single cafe by its ID }
/** * Search cafes by city name. * @param array $searchParams Associative array of search filters * @return array Matching cafes */ public function searchCafes(array $searchParams): array { // TODO: Build a dynamic SQL query with a WHERE clause // Use LIKE for partial matching on city name // Use prepared statements with named parameters for security // Hint: '%' . $searchParams['city'] . '%' for partial matches // Return all matching results using $this->selectAll() }}- Save the file
What you just created:
- A
ShopsModelclass that extendsBaseModelfor database access - Three method signatures:
getAllCafes(),getCafeById(), andsearchCafes() - You need to implement each method body using
$this->selectAll()and$this->selectOne()
Step 2: Create the ShopsController
Section titled “Step 2: Create the ShopsController”Objective: Create the controller to handle cafe list and details page requests.
Instructions:
- Navigate to your
app/Controllers/directory - Create a new file named
ShopsController.php - Copy and paste the following skeleton code:
<?php
namespace App\Controllers;
use App\Domain\Models\ShopsModel;use DI\Container;use Psr\Http\Message\ResponseInterface as Response;use Psr\Http\Message\ServerRequestInterface as Request;
class ShopsController extends BaseController{ public function __construct(Container $container, private ShopsModel $shopsModel) { parent::__construct($container); }
/** * Display the master list of all cafes. */ public function index(Request $request, Response $response, array $args): Response { // TODO: Call the model's getAllCafes() method to get all cafes // TODO: Pass the data to the view using $this->render() // The data array should be structured as: ['cafes' => $cafes] // Refer to: /php/slim-mvc/base-controller/#passing-data-to-views // Render the view: 'shops/index.php' }
/** * Display the details page for a single cafe. */ public function show(Request $request, Response $response, array $args): Response { // TODO: Get the cafe ID from $args['id'] // TODO: Call the model's getCafeById() method // TODO: Handle the case where the cafe is not found // TODO: Pass the data to the view using $this->render() // The data array should be structured as: ['cafe' => $cafe] // Refer to: /php/slim-mvc/base-controller/#passing-data-to-views // Render the view: 'shops/show.php' }}- Save the file
What you just created:
- A
ShopsControllerclass that extendsBaseController - Dependency injection of
ShopsModelthrough the constructor - Two action methods:
index()for the master list andshow()for the details page - You need to implement each method body using
$this->render()and the model methods - Refer to the Passing Data to Views example for how to structure the data array
Step 3: Define the Routes
Section titled “Step 3: Define the Routes”Objective: Register routes that map URIs to your controller methods.
Instructions:
- Open your
app/Routes/web-routes.phpfile - Add the following import at the top of the file:
use App\Controllers\ShopsController;- Add routes for the cafe master list and details views:
// TODO: Define a GET route for '/cafes' that maps to ShopsController::index// Set its name to 'shops.index'
// TODO: Define a GET route for '/cafes/{id}' that maps to ShopsController::show// Set its name to 'shops.show'- Save the file
Implementation Hints:
- Use
$app->get()to define GET routes - Use
[ShopsController::class, 'methodName']as the handler - Use
->setName('route.name')to assign named routes - The
{id}placeholder in the URI will be available as$args['id']in the controller
Test Your Progress:
Once you have implemented the TODOs in Steps 1-3, start your server and visit http://localhost/brew-finder-app/cafes in your browser. You should see your master view rendered (even if it’s empty at first). If you get an error, double-check your model, controller, and route definitions before moving on.
Step 4: Create the Master View (Cafe List)
Section titled “Step 4: Create the Master View (Cafe List)”Objective: Build the view template that displays all cafes in a list or table format.
Instructions:
-
Create a new folder in
app/Views/namedshops/ -
Create a new file named
index.phpinside it -
Build your view with the following requirements:
- Display cafes in a table or card layout using Bootstrap or Bulma
- For each cafe, show at minimum: name (as a clickable link to
/cafes/{id}), city, specialty, average rating, price range - Use
htmlspecialchars()when outputting any database value - Handle the case where no cafes exist (display a “No cafes found” message)
-
Save the file
Implementation Hints:
- The
$datavariable contains the data passed from the controller. Access the cafes array using$data['cafes'] - Loop through
$cafesusingforeachto display each row - Link each cafe name to its details page:
href="cafes/<?= $cafe['id'] ?>" - Remember to escape all output with
htmlspecialchars()to prevent XSS
Step 5: Create the Details View
Section titled “Step 5: Create the Details View”Objective: Build the details view that shows comprehensive information about a selected cafe.
Instructions:
-
Navigate to your
app/Views/shops/directory -
Create a new file named
show.php -
Build your view with the following requirements:
- Display all cafe information: name, description, full address, contact info, operating hours, amenities, specialty, atmosphere, price range, average rating, and total reviews
- Display boolean amenities (WiFi, outdoor seating, pet friendly, parking, roasts own beans) as “Yes”/“No” or with icons
- Use
htmlspecialchars()on all database values - Organize the layout clearly using Bootstrap or Bulma
- Add a link back to the master list (
/cafes)
-
Save the file
Implementation Hints:
- The
$datavariable contains the data passed from the controller. Access the cafe record using$data['cafe'] - Use Bootstrap cards or Bulma boxes to organize sections of information
Test Your Progress:
Visit http://localhost/brew-finder-app/cafes. You should see a list of all cafes. Click on any cafe name and verify that the details page loads at /cafes/{id} with the full cafe information displayed. If the list is empty or the details page shows an error, review your model queries and controller logic.
Step 6: Add Search Functionality
Section titled “Step 6: Add Search Functionality”Objective: Add city-based search to the master view so users can filter cafes.
Instructions:
- Update
ShopsController::index()to handle search parameters:
// TODO: Inside the index() method:// 1. Read search parameters from the request query string// Hint: $request->getQueryParams() returns an array of query parameters// 2. If search parameters exist, call $this->shopsModel->searchCafes($searchParams)// Otherwise, call $this->shopsModel->getAllCafes()// 3. Pass both the cafes AND the search parameters to the view// so the form can retain the user's search input-
Implement
searchCafes()in yourShopsModel(the TODO from Step 1) -
Add a search form to
app/Views/shops/index.php:
<!-- TODO: Add a search form above the cafe list -->
<!-- Requirements: 1. Create a form with method="GET" action="cafes" 2. Add a text input for city name 3. Add a submit button 4. Display the search results count (e.g., "Showing 5 results") 5. Show "No results found" when the search returns empty 6. (Optional) Show active filters with a "Clear" link back to cafes-->- Save all modified files
Implementation Hints:
- Use
method="GET"so search parameters appear in the URL (e.g.,cafes?city=Montreal) - Pre-fill the search input with the current search value to preserve user input
- The controller decides whether to call
getAllCafes()orsearchCafes()based on whether search params exist - If you run into issues with relative paths in the form’s
actionattribute, you can use theAPP_BASE_URLconstant defined inconfig/settings.dev.php↗. For example:action="<?= APP_BASE_URL ?>/cafes"
Requirements
Section titled “Requirements”Master View Implementation (MVC Pattern)
Section titled “Master View Implementation (MVC Pattern)”- Build a view template to display the cafes in a list or table format.
- Make each cafe entry clickable, linking to its details page.
- Add search and filtering options (at minimum: filter by city).
Details View Implementation (MVC Pattern)
Section titled “Details View Implementation (MVC Pattern)”- Create a route that accepts a cafe ID as a parameter.
- Fetch and display the following data:
- Cafe details: name, description, address, contact info, operating hours, amenities.
- Ratings: average rating and total number of reviews.
- Design a details view template that organizes this information clearly and responsively.
Search Functionality (MVC Pattern)
Section titled “Search Functionality (MVC Pattern)”Model Layer:
- Create a
ShopsModelclass with a methodsearchCafes($searchParams). - Implement dynamic SQL queries with WHERE clauses for: city name (use LIKE for partial matches).
- Use prepared statements for security.
View Layer (Shops list):
- Add a search form above the cafe list with at least:
- Text input for city name.
- Display:
- Search results count.
- “No results found” message when applicable.
- Active filters with an option to clear them (optional).
Controller Layer:
Update your ShopsController controller to:
- Accept and validate search parameters from GET/POST requests.
- Pass them to the model’s
searchCafes()method. - Forward results to the view.
- Handle empty results and error cases gracefully.
Technical Implementation/Requirements
Section titled “Technical Implementation/Requirements”- Implement core functionality first, then extend with advanced features.
- Follow this route structure:
/cafes→ Master list./cafes/{id}→ Details view.
- Use prepared statements for all queries.
- Use
htmlspecialchars()when outputting any database values to prevent XSS attacks. - Responsive design and CSS framework integration: Use Bootstrap or Bulma for all styling and responsive design.
What to Submit
Section titled “What to Submit”Submit a .ZIP file containing your Slim app.
Evaluation Rubric
Section titled “Evaluation Rubric”| Criteria | Details |
|---|---|
| Functionality (35%) | All required features implemented and working as specified. Proper error handling. Database integration tested and functional. |
| Code Quality & MVC Structure (30%) | Clean, readable, and well-organized code. Proper use of MVC pattern (separation of concerns between Model, View, and Controller). Clear commenting and adherence to best practices. Secure coding (use of prepared statements, input validation). |
| User Interface (20%) | Professional and consistent appearance. Responsive design across devices. Intuitive navigation between master and details views. |
| Database Integration (15%) | Efficient, optimized queries. Correct use of PDO and prepared statements. |