PHP
Article

Practical OOP: Building a Quiz App – Bootstrapping

By Moshe Teutsch

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.

  • Clark

    Please fix your docblock’s in your QuizInterface they are all incorrect

    • http://www.bitfalls.com/ Bruno Skvorc

      Fixed, thanks!

  • Fab G

    There are some underscores missing for __construct

    • http://www.bitfalls.com/ Bruno Skvorc

      Thanks, fixed!

      • Dante

        private $questions; should be
        private $question;

        “psr-O” should be “psr-0”

  • http://www.BeginWithDisbelief.com Ananda Rajeshwaran.J

    Hi thanks for the excellent write up. I am building a web app with javascript front end(UI) and a REST api back end using Slim framework and mysql for db.
    I don’t want to render html in the server side as explained in this post.
    Please let me know how I can modify the code and make REST calls [avoiding the V-VIEW from the MVC]. I am happy to make use of the Model and the controller(do I require one) as it brings more structure to my code.

    • Moshe Teutsch

      That’s a good idea: separate the JavaScript front-end logic from the server-side logic of querying the database, authentication, and so on.

      To do so, I suggest you use the Service Layer pattern, which I described in this series (take a look at part 2 if you haven’t already!), to implement your service-side business logic. You can then use Slim’s JSON capabilities to create a RESTful API. “dogancelik/slim-json” is a good library for this.

      The advantage of the Service Layer pattern is that you can combine it with any number of different controllers or front-ends with ease. Everyone wants an HTML interface, but with the Service Layer pattern you can easily have others. Want a RESTful interface to integrate with a Single-Page Application? No problem. Want to make a command-line interface for in-house maintenance? Easy as pie.

  • rkyrk

    hi Moshe, I think there may be some problems with the namespace declaration in our first example for QuizeInterface.php. Should it not be backwards slash?
    Also, if possible, I think it would helpful and make it easier for non experienced people to follow along if you had a screenshot with the project directory layout.

  • http://www.bitfalls.com/ Bruno Skvorc

    Fixed the typo, thanks!

  • Eterad

    Hello, I think you don’t know that composer autoloader generates paths that are the same case as Loaded namespace so while your “service” directory works for Service namespace then you still have in article an error when this will be used on case-sensitive partitions. You should change that. And for future reference make a list of exact steps you’ve taken to achieve something by copying commands not writing it from your brain.

    • Moshe Teutsch

      Yes, the lower case “service” path was a typo. If you take a look at the accompanying Github repository, you’ll see that it’s upper case. Thanks for the catch.

  • Frederik Krautwald

    “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.”

    – Sorry, but this is simply not correct. The Domain Model is not just some fancy way of saying object-oriented approach. The Domain Model holds all business rules and logic. It is not constrained to Entities, but business validation and internal services lives here, too. The “Service Layer” acts as an outer shell, protecting domain knowledge from leaking to clients. In effect, the Service Layer is the API which clients can interact with. These application services then calls internal domain services after sanitizing the client requests, and when responses are appropriate, they sanitize domain responses, often by mapping Entities to simple DTOs, again to prevent leakage, before, finally, sending the client response. It is a common mistake to put business logic in the Service Layer a.k.a API and violates Separation of Concerns. Client flow is not a cross-cutting concern, and neither is business logic. You shouldn’t mix the two in your Service Layer.

    That out the way, I think you’re on to a good start.

    • Moshe Teutsch

      I definitely agree with you. Of course, a two-part DIY series can’t hold all that information, so I tried to impart the basic tenants without being too technical. Very good points, though.

      • Frederik Krautwald

        Because Sitepoint is seen as an authority site for many new PHP developers, wrongly describing the different architectural parts could lead to these developers making wrong assumptions about clean separation and architectural patterns. The point of these architectural patterns is to create good software. When they become misunderstood, software will suffer, and, thus, the industry as whole will suffer.

        I think that if you need to cut corners, then you should emphasize that you are doing so, while still describing the true meaning of the architectural part, so developers don’t get the wrong impression.

        I hope you don’t get me wrong. It is not a criticism minded at you personally. It is simply a heads-up because of the implications the information conveyed carry. I’ll be looking forward to part two.

  • http://github.com/sinisbad123 Marcus Ang

    “It has been taken out of context and used as a miracle cure. The trouble is that developers stop there, building
    quasi-object-oriented systems at best and procedural code wrapped in
    classes–often controllers–at worst.”

    TRUE ALL THE TIME.

  • Puneeth

    Hi Moshe, Does PHP and MongoDB 2.6.11 is enough to get this working? I have both installed with Apache server. DO we need both Part1 and Part2 to make it run? If you could give me the process to make the code work be grateful. Thanks in Advance.

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in PHP, once a week, for free.