Blog

Building Applications with Strict Data Types in PHP

Building Applications with Strict Data Types in PHP

Introduction

Like in any other programming language, a data type represents a type of data that you can use in your computer program. It can be a number, letter, symbol, a combination of them, etc. The type of data you use will determine what values you can assign to it and what you can do with it.

PHP is a loosely typed language, meaning, by default, if a value does not match the expected data type, whenever possible, PHP will try to change the type and value of the data to match the expected type. This is called type juggling and an example would be something like this:

$x = '2'; // $x is a string ("2")
$y = 6; // $y is an integer (6)
$result = $x * $y; // $result is an integer (12)

In the above example, $x * $y expects two numeric values, but since $x is a string, PHP converts the string to a numeric value, in our case, an integer. As a result, the variable $x is an integer (the product of the numbers 2 and 6).

Now what if we wanted to tell PHP that $x and $y should be decimal numbers? In PHP we cannot declare the type of a variable but we can create a function that accepts two numbers and gives two numbers as a result. For example:

/**
 * Calculate the product of two decimal numbers.
 *
 * @param   float   $x  The first number.
 * @param   float   $y  The second number.
 * 
 * @result  float   The result of multiplying $x by $y.
 */
function multiply(float $x, float $y) : float
{
    return $x * $y;
}

This would tell PHP that the values of $x and $y passed to our function should be decimal numbers but PHP does not strictly enforce the rule so we can still do this:

$a = multiply(5.2, '3.0'); // $a is a float (15.6)

Now we need to tell PHP to strictly enforce the data types of our function. But before we get to that, let's look at some of the data types available in PHP.

Basic Data Types

Here are some of the basic data types in PHP that we can use to make our application as strictly typed as possible:

Type Description Example
int Non decimal number. 5
float Decimal number, a number with a decimal point - aka double. 10.5
bool Boolean value - TRUE or FALSE. TRUE
string Sequence of characters "Hello World!"
array List of values. ['hello', 'world', 1234]
callable Function. function () { echo 'Hello World!'; }
object Instance of a class. $obj = new MyClass();

Just as with our multiply function, we can use any of the types from the table above to tell our program what to expect and how to handle the data.

To enforce strict typing in our code, which is encouraged and should always be practiced, we need to declare it at the top of each file in our application:

<?php
declare(strict_types=1);

// rest of your code

Now when you do this, you tell the engine that the type must be as is defined in our functions. Then with our multiply function, if a non numeric value is passed as an argument, a TypeError will be thrown. The only exception to this is that an int value can be used when the expected type is float.

Arrays & Lists

An array or list of values can also be the expected type. All we need to do is set the data type to array and any other value will throw an error.

For example, if we wanted to create a function that would accept an array of numbers and return the sum of those values, we can do it like this:

/**
 * Calculate the sum of a list of values.
 *
 * @param   array<float>    $values
 *
 * @return  float
 */
function addAll(array $values) : float
{
    $sum = 0;

    foreach ($values as $value)
    {
        $sum += $value;
    }

    return $sum;
}

In this case where we can't explicitly specify the data type of the values in the array, properly documenting our code will at least help to let us and other know that values are to be expected. Therefore array<float> tells us that the values in the array should all be decimal numbers.

Classes & Objects

If the expected data type should be an instance of a class, we can either use the object data type or the name of the class that.

For example, if we wanted the option to multiply two fractions instead or two integers or decimal numbers, How do we tell PHP that our values should be fractions and nothing else? First let's create a class called Fraction which as we know has two parts, a numerator and a denominator:

/**
 * Fraction Class
 *
 * Representa a value as a fraction.
 */
class Fraction
{
    /**
     * The value above the line in the fraction.
     *
     * @var int
     */
    private int $numerator;

    /**
     * The value below the line in the fraction.
     *
     * @var int
     */
    private int $denominator;

    /**
     * Constructor
     *
     * Create an instance of the Fraction class.
     *
     * @param   int     $numerator
     * @param   int     $denominator
     */
    public function __construct(int $numerator, int $denominator)
    {
       $this->numerator = $numerator;
       $this->denominator = $denmoniator;
    }

    /**
     * Get the value of the numerator.
     *
     * @return int
     */
    public function getNumerator() : void
    {
       return $this->numerator;
    }

    /**
     * Get the value of the denominator.
     *
     * @return int
     */
    public function getDenominator() : void
    {
       return $this->denominator;
    }
}

We can now create our function which will accept two fractions as it's arguments and return the product of those two fractions which would in itself be another fraction.

/**
 * Calculate the sum of two fractions.
 *
 * @param   Fraction    $f1
 * @param   Fraction    $f2
 *
 * @return  Fraction    The sum of the fractions $f1 and $f2.
 */
function multiplyFractions(Fraction $f1, Fraction $f2) : Fraction
{
    $productNumerators = $f1->getNumerator() * $f2->getNumerator();
    $productDenominators = $f1->getDenominator() * $f2->getDenominator();

    $result = new Fraction($productNumerators, $productDenominators);

    return $result;
}

As you can see, to tell PHP that the data type of the arguments to our multiplyFractions function should be objects of the class Fraction, we simply set the type as the name of our class. And the result of our function will also be a fraction.

Nullables

In the case the possibility exists that our datatype can be NULL, let's say in the case of a function where one of the arguments is optional, we can tell PHP to allow NULL values to our data by adding a question mark (?) to the start of the data type.

Since we are working with math operations, let's create a function multiples which will determine the multiples of a number but with the option of specifying how many multiples we would like to find. So if I wanted the first 5 multiples of 2, our function should return an array with the values [2, 4, 6, 8, 10]. The limit, or the number of multiples to find will be our optional (Nullable) value:

/**
 * Determine the multiples of a number given a limit
 * as to the number of multiples to find.
 *
 * @param   int         $n      The number whose multiples are to be discovered.
 * @oaram   int|null    $limit  The number of multiples to find. If NULL, the
 *                              limit will be set to 10.
 *
 * @return  array<int>  List of multiples of the number $n.
 */
function multiples(int $n, ?int $limit = NULL) : array
{
    $multiplesToFind = $limit ?? 10; // $multiplesToFind will be the value of $limit if it's not null, otherwise it will be 10.
    $multiples = [$n]; // Any number is already a multiple of itself.
    $counter = $n + 1; // We start our counter at the next number after $n.

    while (count($multiples) < $multiplesToFind)
    {
        if ($n % $counter == 0)
        {
            $multiples[] = $counter;
        }

        $counter++;
    }
}

The use of ?int tells the engine that the value of the argument $limit can be an integer or NULL which is set to the default value (not required when calling the function). So if we were to test it out:

$mul3 = multiples(3, 7); // $mul3 will be [3, 6, 9, 12, 15, 18, 21]. First 7 multiples.
$mul5 = multiples(5); // $mul5 will be [5, 10, 15, 20, 25, 30, 35, 40, 45, 50]. 

With the variable $mul3, the number of multiples is limited to 7 where as with our second example, $mul5 where the limit is not specified, it will default to 10.

I know you might be saying, "Why not just default the $limit argument to 10 in the first place? Something like this:

/**
 * Determine the multiples of a number given a limit
 * as to the number of multiples to find.
 *
 * @param   int     $n      The number whose multiples are to be discovered.
 * @oaram   int     $limit  The number of multiples to find. Defaults to 10.
 *
 * @return  array<int>  List of multiples of the number $n.
 */
function multiples(int $n, int $limit = 10) : array
{
    // No nullable check is required.
    // The rest of the code...
}

I fully understanding what you are saying but we are talking about Nullable values, no? and I'm out of ideas for examples on our topic.

Tying Things Together

Now, Let's put this all together and create a Math class which would allow us to perform our previous basic calculations with a lil' extra:

<?php
declare(strict_types=1);

/**
 * Fraction Class
 *
 * Representa a value as a fraction.
 */
class Fraction
{
    /**
     * The value above the line in the fraction.
     *
     * @var int
     */
    private int $numerator;

    /**
     * The value below the line in the fraction.
     *
     * @var int
     */
    private int $denominator;

    /**
     * Constructor
     *
     * Create an instance of the Fraction class.
     *
     * @param   int     $numerator
     * @param   int     $denominator
     */
    public function __construct(int $numerator, int $denominator)
    {
       $this->numerator = $numerator;
       $this->denominator = $denmoniator;
    }

    /**
     * Get the value of the numerator.
     *
     * @return int
     */
    public function getNumerator() : void
    {
       return $this->numerator;
    }

    /**
     * Get the value of the denominator.
     *
     * @return int
     */
    public function getDenominator() : void
    {
       return $this->denominator;
    }
}

/**
 * Math Class
 *
 * Perform basic mathematical operations and calculations.
 */
class Math
{
    /**
     * Calculate the product of two decimal numbers.
     *
     * @param   float   $x  The first number.
     * @param   float   $y  The second number.
     * 
     * @result  float   The result of multiplying $x by $y.
     */
    public static function multiply(float $x, float $y) : float
    {
        return $x * $y;
    }

    /**
     * Calculate the sum of a list of values.
     *
     * @param   array<float>    $values
     *
     * @return  float
     */
    public static function addAll(array $values) : float
    {
        $sum = 0;

        foreach ($values as $value)
        {
            $sum += $value;
        }

        return $sum;
    }

    /**
     * Calculate the sum of two fractions.
     *
     * @param   Fraction    $f1
     * @param   Fraction    $f2
     *
     * @return  Fraction    The sum of the fractions $f1 and $f2.
     */
    public static function multiplyFractions(Fraction $f1, Fraction $f2) : Fraction
    {
        $productNumerators = $f1->getNumerator() * $f2->getNumerator();
        $productDenominators = $f1->getDenominator() * $f2->getDenominator();

        $result = new Fraction($productNumerators, $productDenominators);

        return $result;
    }

    /**
     * Determine the multiples of a number given a limit
     * as to the number of multiples to find.
     *
     * @param   int         $n      The number whose multiples are to be discovered.
     * @oaram   int|null    $limit  The number of multiples to find. If NULL, the
     *                              limit will be set to 10.
     *
     * @return  array<int>  List of multiples of the number $n.
     */
    public static function multiples(int $n, ?int $limit = NULL) : array
    {
        $multiplesToFind = $limit ?? 10; // $multiplesToFind will be the value of $limit if it's not null, otherwise it will be 10.
        $multiples = [$n]; // Any number is already a multiple of itself.
        $counter = $n + 1; // We start our counter at the next number after $n.

        while (count($multiples) < $multiplesToFind)
        {
            if ($n % $counter == 0)
            {
                $multiples[] = $counter;
            }

            $counter++;
        }
    }

    /**
     * Calculate the value of a number raised to the power of another number.
     *
     * @param   int     $x      Based number which will be raised to the power of another number.
     * @param   int     $y      Power to which $x will be raised.
     *
     * @return  int     The value of $x raised to the power of $y.
     */
    public static function pow(int $x, int $y) : int
    {
        $result = 1;
        $counter = $y;

        while ($counter > 0)
        {
            $result *= $x;

            $counter --;
        }

        return $result;
    }

    /**
     * Determin the maximum value between two numbers.
     *
     * @param  int     $num1
     * @param  int     $num2
     *
     * @return int
     */
    public static function max(int $num1, int $num2) : int
    {
        if ($num1 > $num2)
        {
            return $num1;
        }

        return $num2;
    }

    /**
     * Determin the factors of a given number.
     *
     * @param  int          $num    Number whose factors are to be determined.
     *
     * @return array<int>   Array containing the factors of the number $num.
     */
    public static function max(int $num) : int
    {
        $factors = [];

        for ($counter = 1; $counter <= $num; $counter++)
        {
            if ($num % $counter == 0)
            {
                $factors[] = $counter;
            }
        }

        return $factors;
    }
}

Now we can use our functions like so:

$max = Math::max(27, 13); // $max is 27.
$factors21 = Math::factors(21); // $factors 21 is [1, 3, 6, 7, 9, 12, 15, 18, 21].

Wraping Up

And there we go. You can add as many mathematical operations to the class as you like. These are just a few.

You should now have a better understanding of how to restrict data types in PHP. This could really help you in ensuring that the input of your program is what you expect in return get the desired result.