В первой части этой серии мы начали, используя подход проектирования снизу вверх, с создания сущностей Quiz
Question
Data 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\Result
index.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».
Ссылки на викторину на домашней странице указывают на :id
index.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 строк. Я показал вам, как создать независимый от реализации маппер для доступа к базе данных, и мы создали сервис для запуска теста независимо от пользовательского интерфейса. Я оставлю это вам, чтобы создать версию приложения для командной строки. Вы можете найти полный источник этой части здесь .
Комментарии? Вопросов? Оставь их ниже!