The Developer Fastlane

« 365 days to become a developer » challenge

PHP OOP: Exceptions

November 28, 2020

Summury

Exercise

Let's apply the lesson's learnings to the previous chapter's exercise code: "Use an API with cURL".

1. Version with extended classes

Result

Click on one of the images above to access the exercise's website

CurlException.php

Exception extended class #1
Code
<?php

class CurlException extends Exception {

    public function __construct($curl) 
    {
        $error = curl_error($curl);
        curl_close($curl);
        $this->message = $error;
    }

}

HTTPException.php

Exception extended class #2
Code
<?php

class HTTPException extends Exception {

    public function __construct($data)
    {
        $data = json_decode($data, true);
        $this->message = $data['message'];
        $this->code = $data['cod'];
    }

}

OpenWeather.php

Main class containing methods and properties
Code
<?php 

class OpenWeather {

    const ICON_SIZE = '2x'; // '1x', '2x' or '4x'
    const UNITS = 'metric';
    private $api_key;

    public function __construct(string $var)
    {
        $this->api_key = $var;
    }

    public function getCurrent(array $coordinates): array
    {
        $result = [];
        $data = $this->callAPI($coordinates);
        if ($data !== null) {
            $result = $this->getResult($data["current"], $data["timezone"]);
        }
        return $result;
    }

    public function getLastHours(array $coordinates): array
    {
        $result = [];
        $data = $this->callAPI($coordinates);
        if ($data !== null) {
            foreach ($data["hourly"] as $hour) {
                $result[] = $this->getResult($hour, $data["timezone"]);
            }
        }
        return $result;
    }

    public static function getName(string $location): array
    {
        $parts = explode('_', $location);
        $loc_array['city'] = implode( '-', array_map( 'ucfirst', explode('-', $parts[0]) ) );
        $loc_array['country'] = strtoupper($parts[1]);
        return $loc_array;
    }

    private function callAPI(array $coordinates): ?array
    {
        $time = time()-1;
        $latitude = $coordinates[0];
        $longitude = $coordinates[1];
        $unit = self::UNITS;
        $curl = curl_init("https://api.openweathermap.org/data/2.5/onecall/timemachine?lat={$latitude}&lon={$longitude}&dt=$time&units={$unit}&appid={$this->api_key}");
        curl_setopt_array($curl, [
            CURLOPT_CAINFO          => dirname(__DIR__) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'certificates' . DIRECTORY_SEPARATOR . 'openweather.cer',
            CURLOPT_RETURNTRANSFER  => true,
            CURLOPT_TIMEOUT_MS      => 3500
        ]);
        $data = curl_exec($curl);
        if ($data === false) {
            throw new CurlException($curl);
        }
        $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        if ($code !== 200) {
            curl_close($curl);
            throw new HTTPException($data);
        }
        curl_close($curl);
        return json_decode($data, true);
    }

    private function getResult($data_when, $timezone): array
    {
        if (self::ICON_SIZE === '2x' || self::ICON_SIZE === '4x') {
            $icon_size = '@' . self::ICON_SIZE;
        } else {
            $icon_size = '';
        }
        return [
            'temp'          => $data_when["temp"] . '°C',
            'description'   => ucfirst($data_when["weather"][0]["description"]),
            'icon'          => 'http://openweathermap.org/img/wn/' . $data_when["weather"][0]["icon"] . $icon_size . '.png',
            'time'          => $this->getTimeFormated($data_when["dt"], $timezone),
        ];
    }
    
    private function getTimeFormated($timestamp, $timezone): array
    {
        $date = new DateTime("@" . $timestamp);
        $date->setTimeZone(new DateTimeZone($timezone));
        return  [
            'all' => $date->format('M d g:i a'),
            'date' => $date->format('M d'),
            'hour' => $date->format('g a'),
        ];
    }

}

weather.php

Content display page
Code
<?php 

$locations = [
    'paris_fr'         => [48.85, 2.35],
    'new-york_us'      => [40.71, -74.00],
    'kerikeri_nz'      => [-35.22, 173.94]
];
$title = 'Weather forecast';
require_once 'includes/header.php';
require_once 'class/OpenWeather.php';
require_once 'class/Exceptions/CurlException.php';
require_once 'class/Exceptions/HTTPException.php';

$alert_class = $error = null;
$weather = new OpenWeather('a4f34eb4cec4aa5200aa8b415963e297c');
try {
    foreach ($locations as $location => $coordinates) {
        $locations_current[$location] = $weather->getCurrent($coordinates);
        $locations_lastHours[$location] = $weather->getLastHours($coordinates); 
    }
} catch (CurlException $e) {
    $error = '<b>API error info:</b> ' . $e->getMessage() . '.';
    $alert_class = 'danger';
} catch (HTTPException $e) {
    $error = '<b>' . $e->getCode() . ' Error status:</b> ' . $e->getMessage();
    $alert_class = 'primary';
}
if($error) {
    $error = '<b>Oops, this was not supposed to happen.</b>
    <div class="small mb-2">' . $error . '</div>
    Please try again later or <a href="contact.php">contact us</a>.';
    $error_sub = '<a href="index.php">Go back to homepage</a>';
} ?>

<p class="lead mb-5">The list below has been made through the OpenWeather API. I links the API to this website thanks to the curl extension for PHP and following <a href="/lessons/20201127_php_Use%20an%20API%20with%20cURL/">this lesson</a>.</p>

<?php if($error): ?>
    <div class="alert <?= $alert_class ? 'alert-'.$alert_class : '' ?>"><?= $error ?></div>
    <div><?= $error_sub ?></div>
<?php else: ?> 
    <div class="row">
        <?php foreach ($locations_current as $location => $data): ?>
            <?php $title = OpenWeather::getName($location) ?>
            <div class="col-md-<?= count($locations) === 4 ? '3' : '4' ?> mb-5">
                <div class="card">
                    <div class="card-body text-center">
                        <h4><?= $title['city'] . ' <span class="small text-muted">(' . $title['country'] . ')</span>' ?></h4>
                        <div class="small text-muted"><?= $data['time']['all'] ?></div>
                        <img src="<?= $data['icon']; ?>"/>
                        <div><?= $data['description'] ?></div>
                        <div><b><?= $data['temp'] ?></b></div>
                    </div>
                </div>
                <div class="mt-3">
                    <ul class="list-unstyled">
                        <?php 
                        $lastHours = $locations_lastHours[$location];
                        for ($i = count($lastHours) - 1; $i >= count($lastHours) - 15; $i -= 2): ?>
                            <?php if (!empty($lastHours[$i])): ?>
                                <li class="small text-muted"><?= $lastHours[$i]['time']['hour'] . ' : <b>' . $lastHours[$i]['temp'] . '</b>  - ' . $lastHours[$i]['description'] ?></li>
                            <?php endif; ?>
                            <?php
                        endfor; ?>
                    </ul>
                </div>
            </div>
        <?php endforeach; ?>
    </div>
<?php endif; ?>

<?php require 'includes/footer.php'; ?>

2. Version with basic Exception class only

Result

Click on one of the images above to access the exercise's website

OpenWeather.php

Main class containing methods and properties
Code
<?php 

class OpenWeather {

    const ICON_SIZE = '2x'; // '1x', '2x' or '4x'
    const UNITS = 'metric';
    private $api_key;

    public function __construct(string $var)
    {
        $this->api_key = $var;
    }

    public function getCurrent(array $coordinates): array
    {
        $result = [];
        $data = $this->callAPI($coordinates);
        if ($data !== null) {
            $result = $this->getResult($data["current"], $data["timezone"]);
        }
        return $result;
    }

    public function getLastHours(array $coordinates): array
    {
        $result = [];
        $data = $this->callAPI($coordinates);
        if ($data !== null) {
            foreach ($data["hourly"] as $hour) {
                $result[] = $this->getResult($hour, $data["timezone"]);
            }
        }
        return $result;
    }

    public static function getName(string $location): array
    {
        $parts = explode('_', $location);
        $loc_array['city'] = implode( '-', array_map( 'ucfirst', explode('-', $parts[0]) ) );
        $loc_array['country'] = strtoupper($parts[1]);
        return $loc_array;
    }

    private function callAPI(array $coordinates): ?array
    {
        $time = time()-1;
        $latitude = $coordinates[0];
        $longitude = $coordinates[1];
        $unit = self::UNITS;
        $curl = curl_init("https://api.openweathermap.org/data/2.5/onecall/timemachine?lat={$latitude}&lon={$longitude}&dt=$time&units={$unit}&appid={$this->api_key}");
        curl_setopt_array($curl, [
            CURLOPT_CAINFO          => dirname(__DIR__) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'certificates' . DIRECTORY_SEPARATOR . 'openweather.cer',
            CURLOPT_RETURNTRANSFER  => true,
            CURLOPT_TIMEOUT_MS      => 4000
        ]);
        $data = curl_exec($curl);
        if ($data === false) {
            $error = curl_error($curl);
            curl_close($curl);
            throw new Exception($error);
        }
        $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        if ($code !== 200) {
            curl_close($curl);
            throw new Exception($data);
        }
        curl_close($curl);
        return json_decode($data, true);
    }

    private function getResult($data_when, $timezone): array
    {
        if (self::ICON_SIZE === '2x' || self::ICON_SIZE === '4x') {
            $icon_size = '@' . self::ICON_SIZE;
        } else {
            $icon_size = '';
        }
        return [
            'temp'          => $data_when["temp"] . '°C',
            'description'   => ucfirst($data_when["weather"][0]["description"]),
            'icon'          => 'http://openweathermap.org/img/wn/' . $data_when["weather"][0]["icon"] . $icon_size . '.png',
            'time'          => $this->getTimeFormated($data_when["dt"], $timezone),
        ];
    }
    
    private function getTimeFormated($timestamp, $timezone): array
    {
        $date = new DateTime("@" . $timestamp);
        $date->setTimeZone(new DateTimeZone($timezone));
        return  [
            'all' => $date->format('M d g:i a'),
            'date' => $date->format('M d'),
            'hour' => $date->format('g a'),
        ];
    }

}

weather.php

Display page
Code
<?php 

$locations = [
    'paris_fr'         => [48.85, 2.35],
    'new-york_us'      => [40.71, -74.00],
    'kerikeri_nz'      => [-35.22, 173.94]
];

$title = 'Weather forecast';
require_once 'includes/header.php';
require_once 'class/OpenWeather.php';

$error = null;
$weather = new OpenWeather('4f34eb4cec4aa5200aa8b415963e297c');
try {
    foreach ($locations as $location => $coordinates) {
        $locations_current[$location] = $weather->getCurrent($coordinates);
        $locations_lastHours[$location] = $weather->getLastHours($coordinates); 
    }
} catch (Exception $e) {
    $message_line = $e->getMessage();
    if (!empty($message_line)) { 
        if ((substr($message_line, 0, 1) === '{' && substr($message_line, -1, 1))) {
            $message_array = json_decode($message_line, true); 
            $message = 'Code ' . $message_array['cod'] . ': ' . $message_array['message'];
        } else {
            $message = $e->getMessage();
        }
    } else {
        $message = 'no details';
    } 
    $error = "An error occured, please try again later.
        <div class='small'>$message</div>";
} ?>

<p class="lead mb-5">The list below has been made through the OpenWeather API. I links the API to this website thanks to the curl extension for PHP and following <a href="/lessons/20201127_php_Use%20an%20API%20with%20cURL/">this lesson</a>.</p>

<?php if($error): ?>
    <div class="alert alert-danger"><?= $error ?></div>
<?php else: ?> 
    <div class="row">
        <?php foreach ($locations_current as $location => $data): ?>
            <?php $title = OpenWeather::getName($location) ?>
            <div class="col-md-<?= count($locations) === 4 ? '3' : '4' ?> mb-5">
                <div class="card">
                    <div class="card-body text-center">
                        <h4><?= $title['city'] . ' <span class="small text-muted">(' . $title['country'] . ')</span>' ?></h4>
                        <div class="small text-muted"><?= $data['time']['all'] ?></div>
                        <img src="<?= $data['icon']; ?>"/>
                        <div><?= $data['description'] ?></div>
                        <div><b><?= $data['temp'] ?></b></div>
                    </div>
                </div>
                <div class="mt-3">
                    <ul class="list-unstyled">
                        <?php 
                        $lastHours = $locations_lastHours[$location];
                        for ($i = count($lastHours) - 1; $i >= count($lastHours) - 15; $i -= 2): ?>
                            <?php if (!empty($lastHours[$i])): ?>
                                <li class="small text-muted"><?= $lastHours[$i]['time']['hour'] . ' : <b>' . $lastHours[$i]['temp'] . '</b>  - ' . $lastHours[$i]['description'] ?></li>
                            <?php endif; ?>
                            <?php
                        endfor; ?>
                    </ul>
                </div>
            </div>
        <?php endforeach; ?>
    </div>
<?php endif; ?>

<?php require 'includes/footer.php'; ?>

Lesson

Purpose

Often in our code, there are unpredictable reactions. For example, this is the case with APIs (which sometimes do not respond or report an error). Exceptions in PHP OOP allow us to create our own types of errors and to precisely define the script's reaction to each of them (display a custom message, skip a part, etc.).

1. $class Exception

Code & result

Code
<?php
function add($a, $b)
{
    if (!is_numeric($a) | !is_numeric($b))
    {
    throw new Exception('Both parameters must be numbers'); // A new exception is thrown if one of the two parameters is not a number.
    }
    
    return $a + $b;
}

try // We will try to perform the instructions located in this block.
{
    echo add(12, 3), '<br />';
    echo add('azerty', 54), '<br />';
    echo add(4, 8);
}
catch (Exception $e) // We will catch the "Exception" exceptions if one is raised
{
    echo 'An exception was thrown. Error message: ', $e->getMessage();
}
Result

Explanation

There are 3 steps to handle an exception:
  1. throw: To be inserted in the condition that will trigger the error. Make a new instance of the Exception class (PHP's default class for handling exceptions). We can create others via extends: subject of the next paragraph). In brackets we will specify the error message: if(error condition here) { throw new Exception('Custom error message here.'); }
  2. try: Include the code in question (the one that could potentially cause a problem). try { // paste here the code that may have unpredictable behavior, like returning an error }
  3. catch: Hide the automatic error message generated by PHP + define an alternative action: catch(Exception $e) { // do something } (By convention, the variable for the exception is $e).
    To display a personalized message, 2 methods:
    • $e->getMessage() : displays the error message present in the instantiation of the exception (step 1 with throw)
    • $e->getCode() : displays PHP error code

Exceptions list

  1. Lists of Exception tree (*)

    As of PHP 7.2.0:
    • ClosedGeneratorException
    • DOMException
    • ErrorException
    • IntlException
    • LogicException
      • BadFunctionCallException
        • BadMethodCallException
      • DomainException
      • InvalidArgumentException
      • LengthException
      • OutOfRangeException
    • PharException
    • ReflectionException
    • RuntimeException
      • OutOfBoundsException
      • OverflowException
      • PDOException
      • RangeException
      • UnderflowException
      • UnexpectedValueException
    • SodiumException

(*) Find the script and output here or there

2. class CustomException extends Exception

You can create extended classes of the default PHP Exception class.
  • Purpose: This allows you to capture different types of errors individually. And thus manage them and display different error messages/contents for each one. Creating a class for each type of error allows much more modularity.
  • To create a new Exception class:
    1. Create a CustomException.php file containing: class CustomException extends Exception { }
    2. Define a custom __construct(): see PHP Doc for details on parameters to insert. Always use a custom error description (\$description). Sometimes the error code will be added as a 2nd parameter.)
      There are 2 ways to do it:
      • Leave empty the class in the CustomException.php file + create an instance in the throw by defining parameters: new CustomException($description, $code);
      • Create a __construct() method in CustomException.php, and define the parameters according to the PHP doc. In the throw, create an instance without defining parameters: new CustomException;
  • Example: Look at the code below (part 2. Version with extended classes)

3. class Error

Basic PHP errors can't be catched with Exception class, but with Error one (PHP Doc).
Go to this part of the video: 21:19

Code & result

Code
try {
// List here actions that may lead to errors
}
catch (Error $e) { 
    $error = $e->getMessage(); 
        // Do some stuff if a basic PHP error occurs
        // (in this example, we fill the $error variable with a custom error message)
}
Result

Explanations

  • Error class works exactly the same as Exception class detailed above, exept it does not catch the same errors' types.
  • Parameters: Check the "Class synopsis" section of the dedicated PHP Doc for a detailed list for the __construct method.
  • declare(): Need to write at the very beginning of the page: declare(strict_types=1);
  • It is possible to include both Exception and Error classes inside the same $e variable with | sign: catch (Exception | Error $e) { // do some stuff }

Errors list

  1. Lists of Throwable (Error) tree (*)

    As of PHP 7.2.0:
    • ArithmeticError
      • DivisionByZeroError
    • AssertionError
    • ParseError
    • TypeError
      • ArgumentCountError

(*) Find the script and output here or there

© 2020 - Edouard Proust | The Developer Fastlane