Статьи

Практическая ООП: создание приложения для викторины — MVC

В первой части этой серии мы начали, используя подход проектирования снизу вверх, с создания сущностей QuizQuestionData Mapper для сущностей Quiz\QuizApp\Service\Quiz\QuizApp\Service\Quiz Если вы еще не прочитали первую часть, я предлагаю вам быстро просмотреть ее, прежде чем продолжить со второй частью и / или загрузить код отсюда .

На этот раз мы создадим и <?php

namespace QuizApp\Service;

use QuizApp\Service\Quiz\Result;

// ...

class Quiz implements QuizInterface
{
const CURRENT_QUIZ = 'quizService_currentQuiz';
const CURRENT_QUESTION = 'quizService_currentQuestion';
const CORRECT = 'quizService_correct';
const INCORRECT = 'quizService_incorrect';

private $mapper;

public function __construct(\QuizApp\Mapper\QuizInterface $mapper)
{
$this->mapper = $mapper;
}

/** @return Quiz[] */
public function showAllQuizes()
{
return $this->mapper->findAll();
}

public function startQuiz($quizOrId)
{
if (!($quizOrId instanceof \QuizApp\Entity\Quiz)) {
$quizOrId = $this->mapper->find($quizOrId);
if ($quizOrId === null) {
throw new \InvalidArgumentException('Quiz not found');
}
}

$_SESSION[self::CURRENT_QUIZ] = $quizOrId->getId();
$_SESSION[self::CORRECT] = $_SESSION[self::INCORRECT] = 0;
}

/**
* @return Question
* @throws \LogicException
*/

public function getQuestion()
{
$questions = $this->getCurrentQuiz()->getQuestions();
$currentQuestion = $this->getCurrentQuestionId();
if ($this->isOver()) {
throw new \LogicException();
}
return $questions[$currentQuestion];
}

/** @return bool */
public function checkSolution($solutionId)
{
$result = $this->getQuestion()->isCorrect($solutionId);
$_SESSION[self::CURRENT_QUESTION] = $this->getCurrentQuestionId() + 1;
$this->addResult($result);
if ($this->isOver()) {
$_SESSION[self::CURRENT_QUESTION] = $_SESSION[self::CURRENT_QUIZ] = null;
}
return $result;
}

/** @return bool */
public function isOver()
{
try {
return $this->getCurrentQuestionId() >= count($this->getCurrentQuiz()->getQuestions());
} catch (\LogicException $e) {
return true;
}
}

/** @return Result */
public function getResult()
{
return new Result(
$_SESSION[self::CORRECT], $_SESSION[self::INCORRECT],
($_SESSION[self::CORRECT] + $_SESSION[self::INCORRECT]) / 2
);
}

private function getCurrentQuiz()
{
if (!isset($_SESSION[self::CURRENT_QUIZ])) {
throw new \LogicException();
}
$quiz = $this->mapper->find($_SESSION[self::CURRENT_QUIZ]);
if ($quiz === null) {
throw new \LogicException();
}
return $quiz;
}

private function getCurrentQuestionId()
{
return isset ($_SESSION[self::CURRENT_QUESTION]) ? $_SESSION[self::CURRENT_QUESTION] : 0;
}

private function addResult($isCorrect)
{
$type = ($isCorrect ? self::CORRECT : self::INCORRECT);
if (!isset($_SESSION[$type])) {
$_SESSION[$type] = 0;
}
$_SESSION[$type] += 1;
}
}
Затем мы напишем наши контроллеры и представления с использованием фреймворка Slim MVC и, наконец, создадим преобразователь MongoDB на место подставного преобразователя, который мы писали в прошлый раз.

Кодирование Сервиса:

Хорошо, теперь, когда мы определили интерфейс для картографа и создали классы сущностей, у нас есть все строительные блоки, необходимые для реализации конкретного класса обслуживания.

 showAllQuizes()

Это долго. Давайте пройдемся по этому методу методом.

Метод QuizMapper::findAll()$mapper Мы могли бы сделать startQuiz()

Метод $mapper Он принимает либо объект сущности викторины, либо идентификатор викторины. В последнем случае он пытается найти тест, используя $_SESSION В этом методе используется $_SESSION Если бы мы хотели, чтобы служба работала в командной строке, мы извлекли бы операции, которые мы использовали для хранения данных в сеансе, в интерфейс. Веб-контроллер будет передавать реализацию, которая внутренне использует getQuestion()

Метод checkSolution() Метод isOver()

Метод getResult()

Метод \QuizApp\Service\Quiz\Resultindex.php

Контроллеры и представления с Slim:

Теперь, когда мы завершили настройку «M» нашего приложения MVC, пришло время написать наши контроллеры и представления. Мы используем фреймворк Slim, но его легко заменить на любой другой фреймворк MVC, поскольку наш код отделен. Создайте файл <?php

require 'vendor/autoload.php';

session_start();

$service = new \QuizApp\Service\Quiz(
new \QuizApp\Mapper\HardCoded()
);
$app = new \Slim\Slim();
$app->config(['templates.path' => './views']);
// Controller actions here
$app->run();

 $_SESSION

Это основа нашего приложения Slim. Мы создаем наш сервис и запускаем сеанс PHP, так как мы используем index.php Наконец, мы настроили наше приложение Slim. Для получения дополнительной информации о Slim ознакомьтесь с обширной документацией на веб-сайте проекта Slim .

Давайте сначала создадим домашнюю страницу. На главной странице будут перечислены тесты, которые может принять пользователь. Код контроллера для этого прост. Добавьте следующий комментарий в нашем файле $app->get('/', function () use ($service, $app) {
$app->render('choose-quiz.phtml', [
'quizes' => $service->showAllQuizes()
]);}
);

 $app->get()

Мы определяем маршрут домашней страницы с помощью choose-quiz.phtml Мы передаем маршрут в качестве первого параметра и передаем код для запуска в качестве второго параметра в форме анонимной функции. В функции мы визуализируем файл представления <h3>choose a quiz</h3>
<ul>
<?php foreach ($quizes as $quiz) : ?>
<li><a href="choose-quiz/<?php echo $quiz->getId();?>"><?php echo $quiz->getTitle(); ?></a></li>
<?php endforeach; ?>
</ul>
Давайте закодируем представление.

 choose-quiz/:id

На этом этапе, если вы перейдете на домашнюю страницу приложения с помощью браузера, вы увидите два теста, которые мы жестко запрограммировали ранее: «Тест 1» и «Тест 2».
Ссылки на викторину на домашней странице указывают на :idindex.php Этот маршрут должен запустить тест, выбранный пользователем, и перенаправить его на свой первый вопрос. Добавьте следующий маршрут к $app->get('/choose-quiz/:id', function($id) use ($service, $app) {
$service->startQuiz($id);
$app->redirect('/solve-question');
});

 /solve-question

Теперь давайте определим маршрут $app->get('/solve-question', function () use ($service, $app) {
$app->render('solve-question.phtml', [
'question' => $service->getQuestion(),
]);
}
);
Этот маршрут покажет пользователю текущий вопрос викторины, которую он решает.

 solve-question.phtml

Маршрут отображает представление <h3><?php echo $question->getQuestion(); ?></h3>
<form action="check-answer" method="post">
<ul>
<?php foreach ($question->getSolutions() as $id => $solution): ?>
<li><input type="radio" name="id" value="<?php echo $id; ?>"> <?php echo $solution; ?></li>
<?php endforeach; ?>
</ul>
<input type="submit" value="submit">
</form>
Давайте определим вид.

 check-answer

Мы показываем пользователю форму с переключателем на ответ. Форма отправляет результаты в маршрут $app->post('/check-answer', function () use ($service, $app) {
$isCorrect = $service->checkSolution($app->request->post('id'));
if (!$service->isOver()) {
$app->redirect('/solve-question');
} else {
$app->redirect('/end');
}
});

 $app->post()

На этот раз мы определяем маршрут для запросов «POST», поэтому мы используем метод $app->request->post('id') Чтобы получить идентификатор решения, отправленный пользователем, мы вызываем $app->get('end', function () use ($service, $app) {
$app->render('end.phtml', [
'result' => $service->getResult(),
]);
});
Служба возвращает, был ли этот ответ правильным. Если есть еще вопросы, на которые нужно ответить, мы перенаправим его обратно на маршрут «решить вопрос». Если он закончил тест, мы отправим его на «конечный» маршрут. Это должно сказать пользователю, прошел ли он тест и на сколько вопросов он ответил правильно.

 \QuizApp\Service\Quiz\Result

Мы делаем это путем извлечения <?php if ($result->hasPassed()) : ?>
<h3>You passed!</h3>
<?php else: ?>
<h3>You failed!</h3>
<?php endif; ?>
<p>You got <?php echo $result->getCorrect(); ?> out of <?php echo $result->getTotal(); ?> questions right.</p>
<a href="/">Back to quizes</a>

 \QuizApp\Mapper\QuizInterface

Написание настоящего картографа с MongoDB:

На этом этапе приложение завершено и будет работать правильно, но мы должны написать настоящий Hardcoded Прямо сейчас мы получаем наши тесты от нашего картографа.

Установите MonogoDB, если он еще не установлен.

Нам нужно создать базу данных, коллекцию и распространить коллекцию с помощью фиктивной викторины. Запустите mongo

 > use practicaloop
    > db.quizes.insert((
          title: 'First Quiz',
          questions: [{
              question: 'Who\'s buried in Grant\'s tomb?',
              solutions: ['Jack', 'Joe', 'Grant', 'Jill'],
              correctIndex: 2
          }]
      })

Теперь нам нужно написать еще один преобразователь, который реализует QuizInterface.

 <?php

namespace QuizApp\Mapper;

class Mongo implements QuizInterference
{
    private static $MAP = [];

    /** @var \MongoCollection */
    private $collection;

    public function __construct(\MongoCollection $collection)
    {
        $this->collection = $collection;
    }

    /**
     * @return \QuizApp\Entity\Quiz[]
     */
    public function findAll()
    {
        $entities = [];
        $results = $this->collection->find();
        foreach ($results as $result) {
            $entities[] = $e = $this->rowtoEntity($result);
            $this->cacheEntity($e);
        }
        return $entities;
    }

    /**
     * @param int $id
     * @return \QuizApp\Entity\Quiz
     */
    public function find($id)
    {
        $id = (string) $id;
        if (isset(self::$MAP[$id])) {
            return self::$MAP[$id];
        }
        $row = $this->collection->findOne(['_id' => new \MongoId($id)]);
        if ($row === null) {
            return null;
        }
        $entity = $this->rowtoEntity($row);
        $this->cacheEntity($entity);
        return $entity;
    }

    private function cacheEntity($entity)
    {
        self::$MAP[(string) $entity->getId()] = $entity;
    }

    private function rowToEntity($row)
    {
        $result = new \QuizApp\Entity\Quiz(
            $row['title'],
            array_map(function ($question) {
                return new \QuizApp\Entity\Question(
                    $question['question'],
                    $question['solutions'],
                    $question['correctIndex']
                );
            }, $row['questions'])
        );
        $result->setId($row['_id']);
        return $result;
    }
}

Посмотрим, что здесь происходит. Класс принимает \MongoCollection Затем он использует коллекцию для извлечения строк из базы данных в методах find()findAll() Оба метода выполняют одни и те же шаги: извлекают строку или строки из базы данных, преобразуют строки в наши \QuizApp\Entity\Quiz\QuizApp\Entity\Question

Все, что нам осталось сделать — это передать экземпляр нового преобразователя нашему сервису в файле index.php

Вывод:

В этой серии мы создали веб-приложение MVC с использованием шаблонов проектирования Service Layer и Domain Model. Таким образом, мы следовали рекомендациям MVC «толстая модель, тонкий контроллер», сохранив весь код контроллера до 40 строк. Я показал вам, как создать независимый от реализации маппер для доступа к базе данных, и мы создали сервис для запуска теста независимо от пользовательского интерфейса. Я оставлю это вам, чтобы создать версию приложения для командной строки. Вы можете найти полный источник этой части здесь .

Комментарии? Вопросов? Оставь их ниже!