Practical OOP: Building a Quiz App – MVC

In part one of this series we began, using the bottom-up design approach, by creating our Quiz and Question entities, writing a dummy Data Mapper class for the Quiz entities, and sketching the interface for our main service, \QuizApp\Service\Quiz, which will define the flow for a user solving a quiz. If you haven’t yet read the first part, I suggest you quickly skim through it before continuing with part two and/or download the code from here.

This time we’ll create and flesh out the \QuizApp\Service\Quiz service that will be the backbone of our quiz app. We’ll then write our controllers and views using the Slim MVC framework, and, finally, create a MongoDB mapper to take the place of the dummy mapper we wrote last time.

Coding the Service:

Okay, now that we’ve defined the interface for the mapper and created the entity classes, we have all the building blocks we need for implementing a concrete service class.

The showAllQuizes() method wraps the QuizMapper::findAll() method. We could make $mapper public, but that would break encapsulation, leaking low-level operations to high-level classes.

The startQuiz() method begins the quiz that is passed as an argument by storing the quiz in the session for future reference. It accepts either a quiz entity object or a quiz ID. In the latter case it tries to find the quiz using the $mapper. The method uses the $_SESSION superglobal directly, which isn’t best practice–the service would break if used in a command-line context, for instance–but there’s no need to over-complicate the service yet. If we wanted to make the service runnable on the command-line, we’d extract the operations we used for storing data in the session to an interface. The web controller would pass an implementation that internally used the $_SESSION superglobal, while the command-line controller might store the “session” variables in as class properties.

The getQuestion() method tries getting the next question of the current quiz from the database, delegating to other helpful methods, and throws an exception if the quiz is over or the user isn’t in the middle of a quiz. The checkSolution() method returns whether the user’s solution is correct, and updates the session to reflect the state of the quiz after the question is answered.

The isOver() method returns true if the current quiz is over or if no quiz is underway.

The getResult() method returns a \QuizApp\Service\Quiz\Result object that tells the user whether he passed the quiz and how many questions he answered correctly.

Controllers and Views with Slim:

Now that we’ve finished setting up the “M” of our MVC application, it’s time to write our controllers and views. We’re using the Slim framework, but it’s easy to replace Slim with any other MVC framework as our code is decoupled. Create an index.php file wih the following contents:

This is the base of our Slim application. We create our service and start the PHP session, since we use the $_SESSION superglobal in our service. Finally, we set up our Slim application. For more information about Slim, read the extensive documentation on the Slim project website.

Let’s create the homepage first. The homepage will list the quizzes the user can take. The controller code for this is straightforward. Add the following by the comment in our index.php file.

We define the homepage route with the $app->get() method. We pass the route as the first parameter and pass the code to run as the second parameter, in the form of an anonymous function. In the function we render the choose-quiz.phtml view file, passing to it the list of our quizzes we retrieved from the service. Let’s code the view.

At this point, if you navigate to the home page of the app with your browser, you’ll see the two quizes we hard-coded earlier, “Quiz 1” and “Quiz 2.”
The quiz links on the home page point to choose-quiz/:id where :id is the ID of the quiz. This route should start the quiz that the user chose and redirect him to his first question. Add the following route to index.php:

This time we’re defining a route for “POST” requests, so we use the $app->post() method. To get the solution ID sent by the user we call $app->request->post('id'). The service returns whether this answer was correct. If there are more questions to answer, we redirect him back to the “solve-question” route. If he’s finished the quiz, we send him to the “end” route. This should tell the user whether he passed the quiz and how many questions he answered correctly.

Writing a Real Mapper with MongoDB:

At this point the app is complete, and will run properly–but we should write a real \QuizApp\Mapper\QuizInterface instance to connect to MongoDB. Right now we’re fetching our quizes from our Hardcoded mapper.

Install MonogoDB if you don’t have it installed already.

We need to create a database, a collection, and propagate the collection with a dummy quiz. Run Mongo–mongo – and inside the terminal run the following commands:

Let’s see what’s going on here. The class accepts a \MongoCollection as a constructor parameter. It then uses the collection to retrieve rows from the datbase in the find() and findAll() methods. Both methods follow the same steps: retrieve the row or rows from the database, convert the rows into our \QuizApp\Entity\Quiz and \QuizApp\Entity\Question objects, and caches them internally to avoid having to look up the same entities later.

All we have left to do is pass an instance of the new mapper to our service in the index.php file.

Conclusion:

In this series, we built an MVC web application using the Service Layer and Domain Model design patterns. By doing so we followed MVC’s “fat model, thin controller” best practice, keeping our entire controller code to 40 lines. I showed you how to create an implementation-agnostic mapper for accessing the database, and we created a service for running the quiz regardless of the user-interface. I’ll leave it to you to create a command-line version of the application. You can find the full source for this part here.

Moshe Teutsch is a freelance web developer and entrepreneur. He specializes in PHP programming and is currently available for hire. In his spare time he enjoys playing chess, singing, writing, reading, philosophizing, and coding in esoteric programming languages.