Skip to content

Lab 2: 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:

  • 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. Create a class named ShopsModel that extends BaseModel.
  4. The constructor should accept a PDOService instance and pass it to the parent constructor.
  5. Implement the following methods:
MethodParametersReturn TypeDescription
getAllCafes()NonearrayFetch all cafes, ordered by name ascending. Use $this->selectAll().
getCafeById()int $idarray|falseFetch a single cafe by its ID. Use $this->selectOne() with a named parameter.
searchCafes()array $searchParamsarrayBuild 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().
  1. Save the file.

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. Create a class named ShopsController that extends BaseController.
  4. The constructor should accept a Container instance (passed to the parent constructor) and a ShopsModel instance (stored as a property via dependency injection).
  5. Each controller method must follow the standard Slim callback signature: (Request $request, Response $response, array $args): Response.
  6. Implement the following methods:
MethodWhat 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.
  1. Save the file.

Required use imports:

  • App\Domain\Models\ShopsModel
  • DI\Container
  • Psr\Http\Message\ResponseInterface as Response
  • Psr\Http\Message\ServerRequestInterface as Request

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

Instructions:

  1. Open your app/Routes/web-routes.php file.
  2. Add a use import for ShopsController at the top of the file.
  3. Define the following routes:
HTTP MethodURIController MethodRoute Name
GET/cafesShopsController::indexshops.index
GET/cafes/{id}ShopsController::showshops.show
  1. 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:

  1. Create a new folder in app/Views/ named shops/.
  2. Create a new file named index.php inside it.
  3. Build your view to display cafes in a table or card layout using Bootstrap or Bulma.
  4. For each cafe, display at minimum the following columns:
ColumnDisplay As
nameClickable link to the details page (/cafes/{id})
cityText
specialtyText
average_ratingText or stars
price_rangeText
  1. Handle the case where no cafes exist (display a “No cafes found” message).
  2. Use htmlspecialchars() when outputting any database value to prevent XSS.
  3. Save the file.

Implementation Hints:

  • The $data variable contains the data passed from the controller. Access the cafes array using $data['cafes'].
  • Loop through the cafes using foreach to display each row.
  • Link each cafe name to its details page: href="cafes/<?= $cafe['id'] ?>".

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 to display the following information, organized clearly using Bootstrap or Bulma:
Column GroupFields
Basic Infoname, description, specialty, atmosphere, price_range
Location and ContactFull address, contact info, operating hours
AmenitiesWiFi, outdoor seating, pet friendly, parking, roasts own beans (display as Yes/No or with icons)
Ratingsaverage_rating, total reviews
  1. Add a link back to the master list (/cafes).
  2. Use htmlspecialchars() on all database values.
  3. 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:

Update the controller:

  1. Open ShopsController.php.
  2. Modify the index() method to:
    • Read search parameters from the request query string.
    • If search parameters exist, call $this->shopsModel->searchCafes($searchParams). Otherwise, call getAllCafes().
    • Pass both the cafes and the search parameters to the view so the search form can retain the user’s input.

Update the view:

  1. Open app/Views/shops/index.php.

  2. Add a search form above the cafe list with the following requirements:

    • Use method="GET" and action pointing 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.
  3. Implement searchCafes() in your ShopsModel if you haven’t already (the method from Step 1).

  4. 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() 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".


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.