Setting up a PHP Application

When setting up a new PHP application one should consider how to manage certain housekeeping tasks. The most important tasks to my mind are:

  1. Exception handling. Uncaught exceptions should be taken care of in a top level exception handler, logged, and the website administrator should be notified.
  2. Logging. It is important to log not only errors and exceptions, but also normally occurring events of importance, such as payloads of API responses.
  3. Mail notification. Although your application may not send out transactional emails, it is a good idea to email administrator about critical events or exceptions that may happen when application is running.

If you use a framework such as Laravel, the above tasks are usually taken care for you by the framework. Although frameworks are great, sometimes they can be an overkill and the best way to go is to use piece meal approach. Let’s look at how to approach the above tasks by using PHP libraries. We will be using exception handling application as an example.

Let’s take a look at the application structure and composer.json file.

Application structure and composer.json file

The application has a simple composer.json file that defines PSR-4 autoloading and requires Whoops, Monolog, SwiftMailer, and Mandrill libraries. Before jumping into the application internals let’s take a look at configuration file config.php located in config folder.

<?php

return [
    'env' => 'production',
    // values for mail_driver: smtp, mandrill, array
    'mail_driver' => 'smtp',
    'mandrill_key' => 'mandrill-key',
    'mail_host' => 'smtp.mailtrap.io',
    'mail_port' => '2525',
    'mail_username' => 'username',
    'mail_password' => 'password',
    'mail_from_address' => 'info@app.com',
    'mail_to_admin' => 'admin@admin.com'
];

The configuration file defines values for application environment, mail driver, and SMTP and API credentials for the mailers. In order to use the configuration values in the application, we will use config class rather than a global config variable. Below is the code for the Config.php class.

<?php 

namespace Apr\ExceptionHandling;

abstract class Config
{
    private static $config;

    private static function getConfig() {

        if (self::$config) {
            return self::$config;
        }
        
        return self::$config = require __DIR__ . '/../config/config.php';
      
    }

    public static function get($property) 
    {
        if (!array_key_exists($property, self::getConfig())) {
            return;
        }

        return self::getConfig()[$property];
    }
}

Config.php is a utility helper class that wraps configuration file. Now we can get values from configuration file by statically calling get function. For example to get ‘env’ variable we can call Config::get(‘env’).

A similar helper Log.php class wraps an instance of Monolog logger.

<?php 

namespace Apr\ExceptionHandling;

use DateTime;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\LineFormatter;

abstract class Log
{
    private static $logger;

    private static function getLogger()
    {
        if (self::$logger) {
            return self::$logger;
        }

        $logger = new Logger(Config::get('env'));

        $formatter = new LineFormatter(null, null, false, true);
        $now = (new DateTime("now"))->format('m_d_Y');

        $handler = new StreamHandler(__DIR__ . "/../logs/app_log_$now.log", Logger::INFO);
        $handler->setFormatter($formatter);

        $logger->pushHandler($handler);

        return self::$logger = $logger;
    }

    public static function __callStatic($name, $arguments)
    {
        if (empty($arguments)) {
           throw new \InvalidArgumentException("There is no message to log");
        }

        $message = $arguments[0];

        $logger = self::getLogger();

        $logger->$name($message);
    }
}

The above class creates an instance of Monolog logger and uses __callStatic magic method to dynamically call logging methods on the logger. So, we can use Log::info(‘Logging…’) or Log::alert(‘Error…’). You can also create another stream handler to log errors in a separate error log file. Another thing to note is that if no message is passed to a logging function, InvalidArgumentException will be thrown.

Now it is a good time to look at index.php file located in public folder.

<?php

require __DIR__ . '/../bootstrap.php';

use Apr\ExceptionHandling\Log;

Log::info('test log');

Log::info();

echo 'Hello';

index.php file contains a simple sample code that first will write a log entry in our log file and them attempt to illegally use Log::info() method that will cause InvalidArgumentException to be thrown. Since the code does not user try..catch block, this exception will bubble up and its handling will take place in bootstrap.php file.

<?php

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

use Apr\ExceptionHandling\Config;
use Apr\ExceptionHandling\Exceptions\ExceptionHandler;

$whoops = new \Whoops\Run;

if (Config::get('env') === 'develop') {
    $whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
}

$whoops->pushHandler(function ($exception, $inspector, $run) {
    $exceptionHandler = new ExceptionHandler;
    $exceptionHandler->report($exception);
});

$whoops->register();

Besides requiring composer’s autoload.php file, bootsrtap.php also contains logic that will use Whoop’s PrettyPageHandler to display a nice exception page below.

Whoop’s exception page

The code will also use our custom ExceptionHandler class to report the exception. Let’s take a look at that class.

<?php

namespace Apr\ExceptionHandling\Exceptions;

use Apr\ExceptionHandling\Log;
use Apr\ExceptionHandling\Config;
use Apr\ExceptionHandling\Mailer\Mail;

class ExceptionHandler 
{
    public function report($exception) 
    {
        Log::error($exception);

        if (Config::get('env') === 'production') 
        {
            Mail::to(Config::get('mail_to_admin'))
                ->from(Config::get('mail_from_address'))
                ->subject('Exception occured')
                ->message($exception)
                ->send();
            
            throw $exception;
            // or display 500 error page
        }
    }
}

ExceptionHandler::report() method logs “bubbled up” exception and if the application in production emails it to the site administrator. Mail class has a fluent interface to create an email and mail the exception.

<?php

namespace Apr\ExceptionHandling\Mailer;

use Apr\ExceptionHandling\Log;

class Mail
{
    public $to;
    public $from;
    public $subject = 'Email Notification';
    public $message;

    public static function to($addresses) 
    {
        if (!is_array($addresses)) {
            $to = [$addresses];
        } else {
            $to = $addresses;
        }

        $instance = new self;
        $instance->to = $to;
        return $instance;
    }

    public function from($addresses) 
    {
        if (!is_array($addresses)) {
            $this->from = [$addresses];
        } else {
            $this->from = $addresses;
        }

        return $this;
    }

    public function message($message)
    {
        $this->message = $message;
        return $this;
    }

    public function subject($subject)
    {
        $this->subject = $subject;
        return $this;
    }

    public function send()
    {
        $mailer = new Mailer;

        try {
            $mailer->send($this);
        } catch (\Throwable $e) {
            Log::error($e);
        }

        
    }
}

Mail::to() method creates an instance of Mail class and each chained method changes class properties and returns the instance back. Finally Mail::send() creates an instance of a mailer and sends the email notification.

<?php

namespace Apr\ExceptionHandling\Mailer;

use Apr\ExceptionHandling\Config;
use Apr\ExceptionHandling\Log;

class Mailer
{
    public function send($mail)
    {
        if (Config::get('mail_driver') === 'smtp') {

            $transport = (new \Swift_SmtpTransport(Config::get('mail_host'), (int) Config::get('mail_port')))
            ->setUsername(Config::get('mail_username'))
            ->setPassword(Config::get('mail_password'))
            ;

            // Create the Mailer using your created Transport
            $mailer = new \Swift_Mailer($transport);

            // Create a message
            $message = (new \Swift_Message($mail->subject))
            ->setFrom($mail->from)
            ->setTo($mail->to)
            ->setBody($mail->message)
            ;

            return $mailer->send($message);

        } else if (Config::get('mail_driver') === 'mandrill') {
            
            $mandrill = new \Mandrill(Config::get('mandrill_key'));

            $to = [];
            foreach ($mail->to as $address) {
               $to[] = ['email' => $address]; 
            }

            $message = [
                'html' => "<p>$mail->message</p>",
                'text' => $mail->message,
                'subject' => $mail->subject,
                'from_email' => $mail->from[0],
                'to' => $to,
            ];

            $async = false;
            $ip_pool = 'Main Pool';
            $send_at = (new \DateTime("now"))->format('Y-m-d H:m:s');
            return $mandrill->messages->send($message, $async, $ip_pool, $send_at);            
        }

        Log::info(json_encode($mail));
    }
}

Mailer::send() method depending on the configuration mail driver creates either SwiftMailer or Mandrill instance and sends out the mail. If the driver is not specified, or other than ‘mandrill’ or ‘smtp’, the method will json_encode mail and log it to the log file.

It is also worth noting that Monolog has SwiftMailHandler. It is possible to configure the logger it in such a way that Log::critical(‘Error message..’) will also email the exception. Although this approach may be more convenient, creating a separate mailer, to my mind, gives greater flexibility in choosing when to mail exceptions.

To summarize, we created a skeleton for a PHP application that has exception handling, logging and email features.

Share this article

Posted

in

by

Tags: