Lab 2: Implementing the Master-Details Pattern using MVC
Additional 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.
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:
- 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.jsonLab 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. - Create a class named
ShopsModelthat extendsBaseModel. - The constructor should accept a
PDOServiceinstance and pass it to the parent constructor. - Implement the following methods:
| Method | Parameters | Return Type | Description |
|---|---|---|---|
getAllCafes() | None | array | Fetch all cafes, ordered by name ascending. Use $this->selectAll(). |
getCafeById() | int $id | array|false | Fetch a single cafe by its ID. Use $this->selectOne() with a named parameter. |
searchCafes() | array $searchParams | array | Build a dynamic SQL query with a WHERE clause. Use LIKE for partial matching on city name (hint: '%' . $searchParams['city'] . '%'). Use prepared statements with named parameters for security. Return all matching results using $this->selectAll(). |
- Save the file.
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. - Create a class named
ShopsControllerthat extendsBaseController. - The constructor should accept a
Containerinstance (passed to the parent constructor) and aShopsModelinstance (stored as a property via dependency injection). - Each controller method must follow the standard Slim callback signature:
(Request $request, Response $response, array $args): Response. - Implement the following methods:
| Method | What It Should Do |
|---|---|
index() | Fetch all cafes from the model. Pass the data to the shops/index.php view using $this->render(). The data array should use the key cafes. |
show() | Get the cafe ID from $args['id']. Fetch the cafe by ID from the model. Handle the case where the cafe is not found. Pass the data to the shops/show.php view using $this->render(). The data array should use the key cafe. |
- Save the file.
Required use imports:
App\Domain\Models\ShopsModelDI\ContainerPsr\Http\Message\ResponseInterface as ResponsePsr\Http\Message\ServerRequestInterface as Request
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 a
useimport forShopsControllerat the top of the file. - Define the following routes:
| HTTP Method | URI | Controller Method | Route Name |
|---|---|---|---|
| GET | /cafes | ShopsController::index | shops.index |
| GET | /cafes/{id} | ShopsController::show | shops.show |
- Save the file.
Test Your Progress:
Once you have implemented 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 to display cafes in a table or card layout using Bootstrap or Bulma.
- For each cafe, display at minimum the following columns:
| Column | Display As |
|---|---|
name | Clickable link to the details page (/cafes/{id}) |
city | Text |
specialty | Text |
average_rating | Text or stars |
price_range | Text |
- Handle the case where no cafes exist (display a “No cafes found” message).
- Use
htmlspecialchars()when outputting any database value to prevent XSS. - Save the file.
Implementation Hints:
- The
$datavariable contains the data passed from the controller. Access the cafes array using$data['cafes']. - Loop through the cafes using
foreachto display each row. - Link each cafe name to its details page:
href="cafes/<?= $cafe['id'] ?>".
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 to display the following information, organized clearly using Bootstrap or Bulma:
| Column Group | Fields |
|---|---|
| Basic Info | name, description, specialty, atmosphere, price_range |
| Location and Contact | Full address, contact info, operating hours |
| Amenities | WiFi, outdoor seating, pet friendly, parking, roasts own beans (display as Yes/No or with icons) |
| Ratings | average_rating, total reviews |
- Add a link back to the master list (
/cafes). - Use
htmlspecialchars()on all database values. - 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 the controller:
- Open
ShopsController.php. - Modify the
index()method to:- Read search parameters from the request query string.
- If search parameters exist, call
$this->shopsModel->searchCafes($searchParams). Otherwise, callgetAllCafes(). - Pass both the cafes and the search parameters to the view so the search form can retain the user’s input.
Update the view:
-
Open
app/Views/shops/index.php. -
Add a search form above the cafe list with the following requirements:
- Use
method="GET"andactionpointing to the cafes route. - Include a text input for city name.
- Include a submit button.
- Pre-fill the search input with the current search value to preserve user input.
- Display the search results count (e.g., “Showing 5 results”).
- Show a “No results found” message when the search returns empty.
- (Optional) Show active filters with a “Clear” link back to
/cafes.
- Use
-
Implement
searchCafes()in yourShopsModelif you haven’t already (the method from Step 1). -
Save all modified files.
Implementation Hints:
- Use
method="GET"so search parameters appear in the URL (e.g.,cafes?city=Montreal). - 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".
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. |