C for controller

It is finally time for the director of the orchestra. Controllers represent the layer in our application that, given a request, talks to the models and builds the views. They act like the manager of a team: they decide what resources to use depending on the situation.

As we stated when explaining models, it is sometimes difficult to decide if some piece of logic should go into the controller or the model. At the end of the day, MVC is a pattern, like a recipe that guides you, rather than an exact algorithm that you need to follow step by step. There will be scenarios where the answer is not straightforward, so it will be up to you; in these cases, just try to be consistent. The following are some common scenarios that might be difficult to localize:

  • The request points to a path that we do not support. This scenario is already covered in our application, and it is the router that should take care of it, not the controller.
  • The request tries to access an element that does not exist, for example, a book ID that is not in the database. In this case, the controller should ask the model if the book exists, and depending on the response, render a template with the book's contents, or another with a "Not found" message.
  • The user tries to perform an action, such as buying a book, but the parameters coming from the request are not valid. This is a tricky one. One option is to get all the parameters from the request without checking them, sending them straight to the model, and leaving the task of sanitizing the information to the model. Another option is that the controller checks that the parameters provided make sense, and then gives them to the model. There are other solutions, like building a class that checks if the parameters are valid, which can be reused in different controllers. In this case, it will depend on the amount of parameters and logic involved in the sanitization. For requests receiving a lot of data, the third option looks like the best of them, as we will be able to reuse the code in different endpoints, and we are not writing controllers that are too long. But in requests where the user sends one or two parameters, sanitizing them in the controller might be good enough.

Now that we've set the ground, let's prepare our application to use controllers. The first thing to do is to update our index.php, which has been forcing the application to always render the same template. Instead, we should be giving this task to the router, which will return the response as a string that we can just print with echo. Update your index.php file with the following content:

<?php

use BookstoreCoreRouter;
use BookstoreCoreRequest;

require_once __DIR__ . '/vendor/autoload.php';

$router = new Router();
$response = $router->route(new Request());
echo $response;

As you might remember, the router instantiates a controller class, sending the request object to the constructor. But controllers have other dependencies as well, such as the template engine, the database connection, or the configuration reader. Even though this is not the best solution (you will improve it once we cover dependency injection in the next section), we could create an AbstractController that would be the parent of all controllers, and will set those dependencies. Copy the following as src/Controllers/AbstractController.php:

<?php

namespace BookstoreControllers;

use BookstoreCoreConfig;
use BookstoreCoreDb;
use BookstoreCoreRequest;
use MonologLogger;
use Twig_Environment;
use Twig_Loader_Filesystem;
use MonologHandlerStreamHandler;

abstract class AbstractController {
    protected $request;
    protected $db;
    protected $config;
    protected $view;
    protected $log;

    public function __construct(Request $request) {
        $this->request = $request;
        $this->db = Db::getInstance();
        $this->config = Config::getInstance();

        $loader = new Twig_Loader_Filesystem(
            __DIR__ . '/../../views'
        );
        $this->view = new Twig_Environment($loader);

        $this->log = new Logger('bookstore');
        $logFile = $this->config->get('log');
        $this->log->pushHandler(
            new StreamHandler($logFile, Logger::DEBUG)
        );
    }

    public function setCustomerId(int $customerId) {
        $this->customerId = $customerId;
    }
}

When instantiating a controller, we will set some properties that will be useful when handling requests. We already know how to instantiate the database connection, the configuration reader, and the template engine. The fourth property, $log, will allow the developer to write logs to a given file when necessary. We will use the Monolog library for that, but there are many other options. Notice that in order to instantiate the logger, we get the value of log from the configuration, which should be the path to the log file. The convention is to use the /var/log/ directory, so create the /var/log/bookstore.log file, and add "log": "/var/log/bookstore.log" to your configuration file.

Another thing that is useful to some controllers—but not all of them—is the information about the user performing the action. As this is only going to be available for certain routes, we should not set it when constructing the controller. Instead, we have a setter for the router to set the customer ID when available; in fact, the router does that already.

Finally, a handy helper method that we could use is one that renders a given template with parameters, as all the controllers will end up rendering one template or the other. Let's add the following protected method to the AbstractController class:

protected function render(string $template, array $params): string {
    return $this->view->loadTemplate($template)->render($params);
}

The error controller

Let's start by creating the easiest of the controllers: the ErrorController. This controller does not do much; it just renders the error.twig template sending the "Page not found!" message. As you might remember, the router uses this controller when it cannot match the request to any of the other defined routes. Save the following class in src/Controllers/ErrorController.php:

<?php

namespace BookstoreControllers;

class ErrorController extends AbstractController {
    public function notFound(): string {
        $properties = ['errorMessage' => 'Page not found!'];
        return $this->render('error.twig', $properties);
    }
}

The login controller

The second controller that we have to add is the one that manages the login of the customers. If we think about the flow when a user wants to authenticate, we have the following scenarios:

  • The user wants to get the login form in order to submit the necessary information and log in.
  • The user tries to submit the form, but we could not get the e-mail address. We should render the form again, letting them know about the problem.
  • The user submits the form with an e-mail, but it is not a valid one. In this case, we should show the login form again with an error message explaining the situation.
  • The user submits a valid e-mail, we set the cookie, and we show the list of books so the user can start searching. This is absolutely arbitrary; you could choose to send them to their borrowed books page, their sales, and so on. The important thing here is to notice that we will be redirecting the request to another controller.

There are up to four possible paths. We will use the request object to decide which of them to use in each case, returning the corresponding response. Let's create, then, the CustomerController class in src/Controllers/CustomerController.php with the login method, as follows:

<?php

namespace BookstoreControllers;

use BookstoreExceptionsNotFoundException;
use BookstoreModelsCustomerModel;

class CustomerController extends AbstractController {
    public function login(string $email): string {
        if (!$this->request->isPost()) {
            return $this->render('login.twig', []);
        }

        $params = $this->request->getParams();

        if (!$params->has('email')) {
            $params = ['errorMessage' => 'No info provided.'];
            return $this->render('login.twig', $params);
        }

        $email = $params->getString('email');
        $customerModel = new CustomerModel($this->db);

        try {
            $customer = $customerModel->getByEmail($email);
        } catch (NotFoundException $e) {
            $this->log->warn('Customer email not found: ' . $email);
            $params = ['errorMessage' => 'Email not found.'];
            return $this->render('login.twig', $params);
        }

        setcookie('user', $customer->getId());

        $newController = new BookController($this->request);
        return $newController->getAll();
    }
}

As you can see, there are four different returns for the four different cases. The controller itself does not do anything, but orchestrates the rest of the components, and makes decisions. First, we check if the request is a POST, and if it is not, we will assume that the user wants to get the form. If it is, we will check for the e-mail in the parameters, returning an error if the e-mail is not there. If it is, we will try to find the customer with that e-mail, using our model. If we get an exception saying that there is no such customer, we will render the form with a "Not found" error message. If the login is successful, we will set the cookie with the ID of the customer, and will execute the getAll method of BookController (still to be written), returning the list of books.

At this point, you should be able to test the login feature of your application end to end with the browser. Try to access http://localhost:8000/login to see the form, adding random e-mails to get the error message, and adding a valid e-mail (check your customer table in MySQL) to log in successfully. After this, you should see the cookie with the customer ID.

The book controller

The BookController class will be the largest of our controllers, as most of the application relies on it. Let's start by adding the easiest methods, the ones that just retrieve information from the database. Save this as src/Controllers/BookController.php:

<?php

namespace BookstoreControllers;

use BookstoreModelsBookModel;

class BookController extends AbstractController {
    const PAGE_LENGTH = 10;

    public function getAllWithPage($page): string {
        $page = (int)$page;
        $bookModel = new BookModel($this->db);

        $books = $bookModel->getAll($page, self::PAGE_LENGTH);

        $properties = [
            'books' => $books,
            'currentPage' => $page,
            'lastPage' => count($books) < self::PAGE_LENGTH
        ];
        return $this->render('books.twig', $properties);
    }

    public function getAll(): string {
        return $this->getAllWithPage(1);
    }

    public function get(int $bookId): string {
        $bookModel = new BookModel($this->db);

        try {
            $book = $bookModel->get($bookId);
        } catch (Exception $e) {
            $this->log->error(
                'Error getting book: ' . $e->getMessage()
            );
            $properties = ['errorMessage' => 'Book not found!'];
            return $this->render('error.twig', $properties);
        }

        $properties = ['book' => $book];
        return $this->render('book.twig', $properties);
    }

    public function getByUser(): string {
        $bookModel = new BookModel($this->db);

        $books = $bookModel->getByUser($this->customerId);

        $properties = [
            'books' => $books,
            'currentPage' => 1,
            'lastPage' => true
        ];
        return $this->render('books.twig', $properties);
    }
}

There's nothing too special in this preceding code so far. The getAllWithPage and getAll methods do the same thing, one with the page number given by the user as a URL argument, and the other setting the page number as 1—the default case. They ask the model for the list of books to be displayed and passed to the view. The information of the current page—and whether or not we are on the last page—is also sent to the template in order to add the "previous" and "next" page links.

The get method will get the ID of the book that the customer is interested in. It will try to fetch it using the model. If the model throws an exception, we will render the error template with a "Book not found" message. Instead, if the book ID is valid, we will render the book template as expected.

The getByUser method will return all the books that the authenticated customer has borrowed. We will make use of the customerId property that we set from the router. There is no sanity check here, since we are not trying to get a specific book, but rather a list, which could be empty if the user has not borrowed any books yet—but that is not an issue.

Another getter controller is the one that searches for a book by its title and/or author. This method will be triggered when the user submits the form in the layout template. The form sends both the title and the author fields, so the controller will ask for both. The model is ready to use the arguments that are empty, so we will not perform any extra checking here. Add the method to the BookController class:

public function search(): string {
    $title = $this->request->getParams()->getString('title');
    $author = $this->request->getParams()->getString('author');

    $bookModel = new BookModel($this->db);
    $books = $bookModel->search($title, $author);

    $properties = [
        'books' => $books,
        'currentPage' => 1,
        'lastPage' => true
    ];
    return $this->render('books.twig', $properties);
}

Your application cannot perform any actions, but at least you can finally browse the list of books, and click on any of them to view the details. We are finally getting something here!

Borrowing books

Borrowing and returning books are probably the actions that involve the most logic, together with buying a book, which will be covered by a different controller. This is a good place to start logging the user's actions, since it will be useful later for debugging purposes. Let's see the code first, and then discuss it briefly. Add the following two methods to your BookController class:

public function borrow(int $bookId): string {
    $bookModel = new BookModel($this->db);

    try {
        $book = $bookModel->get($bookId);
    } catch (NotFoundException $e) {
        $this->log->warn('Book not found: ' . $bookId);
        $params = ['errorMessage' => 'Book not found.'];
        return $this->render('error.twig', $params);
    }

    if (!$book->getCopy()) {
        $params = [
            'errorMessage' => 'There are no copies left.'
       ];
        return $this->render('error.twig', $params);
    }

    try {
        $bookModel->borrow($book, $this->customerId);
    } catch (DbException $e) {
        $this->log->error(
            'Error borrowing book: ' . $e->getMessage()
        );
        $params = ['errorMessage' => 'Error borrowing book.'];
        return $this->render('error.twig', $params);
    }

    return $this->getByUser();
}

public function returnBook(int $bookId): string {
    $bookModel = new BookModel($this->db);

    try {
        $book = $bookModel->get($bookId);
    } catch (NotFoundException $e) {
        $this->log->warn('Book not found: ' . $bookId);
        $params = ['errorMessage' => 'Book not found.'];
        return $this->render('error.twig', $params);
    }

    $book->addCopy();

    try {
        $bookModel->returnBook($book, $this->customerId);
    } catch (DbException $e) {
        $this->log->error(
            'Error returning book: ' . $e->getMessage()
        );
        $params = ['errorMessage' => 'Error returning book.'];
        return $this->render('error.twig', $params);
    }

    return $this->getByUser();
}

As we mentioned earlier, one of the new things here is that we are logging user actions, like when trying to borrow or return a book that is not valid. Monolog allows you to write logs with different priority levels: error, warning, and notices. You can invoke methods such as error, warn, or notice to refer to each of them. We use warnings when something unexpected, yet not critical, happens, for example, trying to borrow a book that is not there. Errors are used when there is an unknown problem from which we cannot recover, like an error from the database.

The modus operandi of these two methods is as follows: we get the book object from the 3database with the given book ID. As usual, if there is no such book, we return an error page. Once we have the book domain object, we make use of the helpers addCopy and getCopy in order to update the stock of the book, and send it to the model, together with the customer ID, to store the information in the database. There is also a sanity check when borrowing a book, just in case there are no more books available. In both cases, we return the list of books that the user has borrowed as the response of the controller.

The sales controller

We arrive at the last of our controllers: the SalesController. With a different model, it will end up doing pretty much the same as the methods related to borrowed books. But we need to create the sale domain object in the controller instead of getting it from the model. Let's add the following code, which contains a method for buying a book, add, and two getters: one that gets all the sales of a given user and one that gets the info of a specific sale, that is, getByUser and get respectively. Following the convention, the file will be src/Controllers/SalesController.php:

<?php

namespace BookstoreControllers;

use BookstoreDomainSale;
use BookstoreModelsSaleModel;

class SalesController extends AbstractController {
    public function add($id): string {
        $bookId = (int)$id;
        $salesModel = new SaleModel($this->db);

        $sale = new Sale();
        $sale->setCustomerId($this->customerId);
        $sale->addBook($bookId);

        try {
            $salesModel->create($sale);
        } catch (Exception $e) {
            $properties = [
                'errorMessage' => 'Error buying the book.'
           ];
            $this->log->error(
                'Error buying book: ' . $e->getMessage()
            );
            return $this->render('error.twig', $properties);
        }

        return $this->getByUser();
    }

    public function getByUser(): string {
        $salesModel = new SaleModel($this->db);

        $sales = $salesModel->getByUser($this->customerId);

        $properties = ['sales' => $sales];
        return $this->render('sales.twig', $properties);
    }

    public function get($saleId): string {
        $salesModel = new SaleModel($this->db);

        $sale = $salesModel->get($saleId);

        $properties = ['sale' => $sale];
        return $this->render('sale.twig', $properties);
    }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.142.85