PHP - - By Moshe Teutsch

Practical OOP: Building a Quiz App – Bootstrapping

Building a SOLID MVC Quiz App with Slim

At a certain point of my development as a PHP programmer, I was building MVC applications by-the-book, without understanding the ins-and-outs. I did what I was told: fat model, thin controller. Don’t put logic in your views. What I didn’t understand was how to create a cohesive application structure that allowed me to express my business ideas as maintainable code, nor did I understand how to really separate my concerns into tight layers without leaking low-level logic into higher layers. I’d heard about SOLID principles, but applying them to a web app was a mystery.

In this series, we’ll build a quiz application using these concepts. We’ll separate the application into layers, allowing us to substitute components: for example, it’ll be a breeze to switch from MongoDB to MySQL, or from a web interface to a command-line interface.

Why MVC Isn’t Enough:

MVC, which stands for Model-View-Controller, is a powerful design pattern for web applications. Unfortunately, with its rise to buzzword status, it has been taken out of context and used as a miracle cure. It’s become standard practice to use MVC frameworks, and many developers have succeeded in using them to seperate display logic and domain logic. The trouble is that developers stop there, building quasi-object-oriented systems at best and procedural code wrapped in classes–often controllers–at worst.

In building our quiz app, we’ll be using the Domain Model pattern described in Martin Fowler’s Patterns of Enterprise Application Architecture. Domain Model is just a fancy way of saying that we’ll be using an object-oriented approach to designing the system: a web of objects with different responsibilities that as a whole, will comprise our application.

The Domain Model approach uses “entity” objects to represent information in the database; but instead of having our object-oriented code mimic the database, we’ll have the database mimic our object-oriented design. Why? Because it allows us to build good object-oriented code. This mapping, called Object-Relational Mapping, is a large subject, and outside of the scope of this article. Luckily, there are several mature libraries available in PHP that solve this problem. The most mature of these, and my personal favorite, is Doctrine. We’ll be side-stepping the issue entirely by manually writing the specific mapping code we need for this article.

Even when using the Domain Model pattern, there is still the problem of performing operations that require multiple classes to work together. We’ll be solving this with the Service Layer pattern.

The Service Layer Pattern:

Correct object-oriented design dictates that you should write decoupled code. Each class should have a single responsiblity. How, then, do we combine these independent classes to perform our business logic?

The Service Layer pattern addresses this problem. We group all our system’s operations (signing up, purchasing a product, solving a quiz) into service classes, one service per operation or group of closely-related operations. We decouple these service classes from the classes to which they delegate. This allows us to reuse the services between different use-cases, say the web interface and the CLI interface, the front- and back-end interfaces, and so on.

Getting Started:

We’ll be using Slim as our MVC framework. Slim is a light-weight framework that’s easy to learn, and perfect for our simple app. As you’ll see in the next article, when we write controller code, it’ll be easy for you to replace Slim with any framework you prefer. We’ll install Slim with Composer. Create a directory for the project with the following composer.json file:

{
        "require": {
            "slim/slim": "2.*"
        }
        "autoload": {
            "psr-O": {"QuizApp\\": "./lib/"}
        }
    }

Coding the Service Class:

We’ll need a service for handling the quiz flow: choosing a quiz, checking the user’s answers, and so on. This service will contain the bulk of the business logic of the application. The rest of the code will solve more technical, specific problems, such as accessing the database.

Let’s define an interface for the service. Create a file lib/QuizApp/service/QuizInterface.php with the following contents:

<?php

namespace QuizApp\Service;

interface QuizInterface
{
    /** @return Quiz[] */
    public function showAllQuizes();

    public function startQuiz($quizOrId);

    /** @return Question */
    public function getQuestion();

    /** @return bool */
    public function checkSolution($id);

    /** @return bool */
    public function isOver();

    /** @return Result */
    public function getResult();
}

Most of the operations should speak for themselves, but getQuestion() and getResult() might not be so clear. getQuestion() returns the next question for the user to answer. getResult() returns an object with information about the number of correct and incorrect answers, and whether the user passed the quiz.

Before we implement this service, we should define the mapper interface, as the service will need to use it. The service needs two operations: find() which returns a single quiz by ID, and findAll().

<?php

    namespace QuizApp\Mapper;

    interface QuizInterface
    {
        /** @return \QuizApp\Entity\Quiz[] */
        public function findAll();

        /**
         * @param int $i
         * @return \QuizApp\Entity\Quiz
         */
        public function find($i);
    }

These operations return objects of the class \QuizApp\Entity\Quiz, which represents a single quiz. The Quiz class, in turn, contains \QuizApp\Entity\Question objects, which represent quiz questions. Let’s implement these before returning to the service.

<?php

    namespace QuizApp\Entity;

    class Question 
    {
        private $id;
        private $questions;
        private $solutions;
        private $correctIndex;

        /**
        * @param string $question
        * @param string[] $solutions
        * @param int $correctSolutionIndex
        */
        public function __construct ($question, array $solutions, $correctSolutionIndex)
        {
            $this->question = $question;
            $this->solutions = $solutions;
            $this->correctIndex = $correctSolutionIndex;
            if (!isset($this->solutions[$this->correctIndex])) {
                throw new \InvalidArgumentException('Invalid index');
            }
        }

        public function setId($id)
        {
            $this->id = $id;
        }

        public function getId()
        {
            return $this->id;
        }

        public function getQuestion()
        {
            return $this->question;
        }

        public function getSolutions()
        {
            return $this->solutions;
        }

        public function getCorrectSolution()
        {
            return $this->solutions[$this->correctIndex];
        }

        public function isCorrect($solutionId)
        {
            return $this->correctIndex == $solutionId;
        }
    }

Notice that, in addition to its getters and setters, \QuizApp\Entity\Question has a method isCorrect() for checking if a certain answer to the question is correct.

And the \QuizApp\Entity\Quiz class:

<?php

    namespace QuizApp\Entity;

    class Quiz
    {
        private $id;
        private $title;
        private $questions;

        /**
        * @param string $title
        * @param Question[] $questions
        */
        public function __construct($title, array $questions)
        {
            $this->title     = $title;
            $this->questions = $questions;
        }

        public function setId($id)
        {
            $this->id = $id;
        }

        public function getId()
        {
            return $this->id;
        }

        public function getTitle()
        {
            return $this->title;
        }

        public function getQuestions()
        {
            return $this->questions;
        }
    }

And the \QuizApp\Service\Quiz\Result class:

<?php

    namespace QuizApp\Service\Quiz;

    class Result
    {
        private $correct;
        private $incorrect;
        private $passScore;

        public function __construct($correct, $incorrect, $passScore)
        {
            $this->correct = $correct;
            $this->incorrect = $incorrect;
            $this->passScore = $passScore;
        }

        public function getCorrect()
        {
            return $this->correct;
        }

        public function getIncorrect()
        {
            return $this->incorrect;
        }

        public function getTotal()
        {
            return $this->correct + $this->incorrect;
        }

        public function getPassScore()
        {
            return $this->passScore;
        }

        public function hasPassed()
        {
            return $this->correct >= $this->passScore;
        }
    }

Writing a Placeholder Mapper:

We need a concrete \QuizApp\MapperMapper\MapperInterface class for the service to use. Let’s define a dummy implementation for the time being, so that we can test the code, before writing a real mapper that’ll access the MongoDB database. The dummy uses hard-coded ‘\QuizApp\Entity\Question’ objects to return from the find($id) and findAll() methods.

<?php

namespace QuizApp\Mapper;

class Hardcoded implements QuizInterface
{
    private static $MAP = array();

    /** @return \QuizApp\Entity\Quiz[] */
    public function findAll()
    {
        return [ $this->find(0), $this->find(1) ];
    }

    /**
     * @param int $id
     * @return \QuizApp\Entity\Quiz
     */
    public function find($id)
    {
        if (isset (self::$MAP[$id])) {
            return self::$MAP[$id];
        }
        $result = new \QuizApp\Entity\Quiz(
            'Quiz' . $id, [
                new \QuizApp\Entity\Question(
                    'What colour was George Washington\'s white horse?',
                    [ 'White', 'Gray', 'Yellow', 'All of the above' ],
                    0
                ),
                new \QuizApp\Entity\Question(
                    'Who\'s buried in Grant\'s tomb?',
                    [ 'Grant', 'George Washington', 'George Washingtion\'s horse', 'All of the above' ],
                    0
                ),
            ]
        );
        $result->setId($id);
        self::$MAP[$id] = $result;
        return $result;
    }
}

The class implements the interface by returning a couple of hard-coded Quiz objects. It uses the $MAP static property as an Identity Map to ensure the class returns the same objects each time it’s called.

Conclusion:

In this first part of our Pratical OOP series we began developing our quiz application. We discussed MVC and why it is not a silver bullet; we covered the Domain Model and Service Layer design patterns; we sketched out the interface for our quiz service, which will contain the logic behind a user solving a quiz; we modeled the quizes and questions as entities; and we created a dummy mapper for looking up quizzes from the “database,” which will come in handy in part two.

Stay tuned! Next time we’ll be be fleshing out our application, writing the service class, and a real database mapper that will connect to MongoDB. Once we have that in place, you’ll see how easy it is to write our controllers and views, and how our elegant design keeps the “M,” the “V,” and the “C” separate, maintainable, and extensible. You can find the full source code for this part here.

Sponsors