Skip to content

Lab 1: Implementing the Master-Details Pattern using MVC

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.

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.

  • Wampoon and VS Code.
  • Composer (bundled with Wampoon).
  • The brew_finder database imported into MariaDB.

You will be using the Slim MVC starter template to scaffold your project.

  1. Open a terminal and navigate to your htdocs/ directory:
Terminal window
cd C:/wampoon/htdocs
  1. Run the following command to create a new project:
Terminal window
composer create-project frostybee/slim-mvc brew-finder-app
  1. Open the config/env.php file and update your database credentials to connect to the brew_finder database.
  2. 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.json


Objective: Create the model layer with methods to query the cafes table from the brew_finder database.

Instructions:

  1. Navigate to your app/Domain/Models/ directory
  2. Create a new file named ShopsModel.php
  3. Copy and paste the following skeleton code:
app/Domain/Models/ShopsModel.php
<?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()
}
}
  1. Save the file

What you just created:

  • A ShopsModel class that extends BaseModel for database access
  • Three method signatures: getAllCafes(), getCafeById(), and searchCafes()
  • You need to implement each method body using $this->selectAll() and $this->selectOne()

Objective: Create the controller to handle cafe list and details page requests.

Instructions:

  1. Navigate to your app/Controllers/ directory
  2. Create a new file named ShopsController.php
  3. Copy and paste the following skeleton code:
app/Controllers/ShopsController.php
<?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'
}
}
  1. Save the file

What you just created:

  • A ShopsController class that extends BaseController
  • Dependency injection of ShopsModel through the constructor
  • Two action methods: index() for the master list and show() 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

Objective: Register routes that map URIs to your controller methods.

Instructions:

  1. Open your app/Routes/web-routes.php file
  2. Add the following import at the top of the file:
use App\Controllers\ShopsController;
  1. 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'
  1. 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:

  1. Create a new folder in app/Views/ named shops/

  2. Create a new file named index.php inside it

  3. 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)
  4. Save the file

Implementation Hints:

  • The $data variable contains the data passed from the controller. Access the cafes array using $data['cafes']
  • Loop through $cafes using foreach to 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

Objective: Build the details view that shows comprehensive information about a selected cafe.

Instructions:

  1. Navigate to your app/Views/shops/ directory

  2. Create a new file named show.php

  3. 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)
  4. Save the file

Implementation Hints:

  • The $data variable 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.


Objective: Add city-based search to the master view so users can filter cafes.

Instructions:

  1. Update ShopsController::index() to handle search parameters:
app/Controllers/ShopsController.php - index() method
// 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
  1. Implement searchCafes() in your ShopsModel (the TODO from Step 1)

  2. Add a search form to app/Views/shops/index.php:

app/Views/shops/index.php - Search form
<!-- 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
-->
  1. 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() or searchCafes() based on whether search params exist
  • If you run into issues with relative paths in the form’s action attribute, you can use the APP_BASE_URL constant defined in config/settings.dev.php. For example: action="<?= APP_BASE_URL ?>/cafes"

  1. Build a view template to display the cafes in a list or table format.
  2. Make each cafe entry clickable, linking to its details page.
  3. Add search and filtering options (at minimum: filter by city).
  1. Create a route that accepts a cafe ID as a parameter.
  2. Fetch and display the following data:
    • Cafe details: name, description, address, contact info, operating hours, amenities.
    • Ratings: average rating and total number of reviews.
  3. Design a details view template that organizes this information clearly and responsively.

Model Layer:

  • Create a ShopsModel class with a method searchCafes($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:

  1. Accept and validate search parameters from GET/POST requests.
  2. Pass them to the model’s searchCafes() method.
  3. Forward results to the view.
  4. Handle empty results and error cases gracefully.
  • 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.

Submit a .ZIP file containing your Slim app.


CriteriaDetails
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.