PHP 8 Programming Tips, Tricks and Best Practices

By Doug Bierer
    Advance your knowledge in tech with a Packt subscription

  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Chapter 1: Introducing New PHP 8 OOP Features

About this book

Thanks to its ease of use, PHP is a highly popular programming language used on over 78% of all web servers connected to the Internet. PHP 8 Programming Tips, Tricks, and Best Practices will help you to get up-to-speed with PHP 8 quickly. The book is intended for any PHP developer who wants to become familiar with the cool new features available in PHP 8, and covers areas where developers might experience backward compatibility issues with their existing code after a PHP 8 update. The book thoroughly explores best practices, and highlights ways in which PHP 8 enforces these practices in a much more rigorous fashion than its earlier versions.

You'll start by exploring new PHP 8 features in the area of object-oriented programming (OOP), followed by enhancements at the procedural level. You'll then learn about potential backward compatible breaks and discover best practices for improving performance. The last chapter of the book gives you insights into PHP async, a revolutionary new way of programming, by providing detailed coverage and examples of asynchronous programming using the Swoole extension and Fibers.

By the end of this PHP book, you'll not only have mastered the new features, but you'll also know exactly what to watch out for when migrating older PHP applications to PHP 8.

Publication date:
August 2021
Publisher
Packt
Pages
528
ISBN
9781801071871

 

Chapter 1: Introducing New PHP 8 OOP Features

In this chapter, you are introduced to new PHP: Hypertext Preprocessor 8 (PHP 8) features specific to Object-Oriented Programming (OOP). The chapter features a set of classes that can be used to generate CAPTCHA images (CAPTCHA is an acronym for Completely Automated Public Turing test to tell Computers and Humans Apart), clearly illustrating new PHP 8 features and concepts. This chapter is critical in helping you quickly incorporate new PHP 8 features into your own practice. In doing so, your code will run faster and more efficiently, with fewer bugs.

The following topics are covered in this chapter:

  • Using constructor property promotion
  • Working with attributes
  • Incorporating match expressions into your program code
  • Understanding named arguments
  • Exploring new data types
  • Improving code using typed properties
 

Technical requirements

To examine and run the code examples provided in this chapter, the minimum recommended hardware is listed here:

  • x86_64-based desktop PC or laptop
  • 1 gigabyte (GB) free disk space
  • 4 GB of random-access memory (RAM)
  • 500 kilobits per second (Kbps) or faster internet connection

In addition, you will need to install the following software:

  • Docker
  • Docker Compose

This book uses a pre-built Docker image that contains all the needed software to create and run the PHP 8 code examples covered in this book. You do not need to install PHP, Apache, or MySQL on your computer: just use Docker and the provided image.

To set up a test environment to run the code examples, proceed as follows:

  1. Install Docker.

    If you are running Windows, start here:

    https://docs.docker.com/docker-for-windows/install/

    If you are on a Mac, start here:

    https://docs.docker.com/docker-for-mac/install/

    If you are on Linux, have a look here:

    https://docs.docker.com/engine/install/

  2. Install Docker Compose. For all operating systems, start here:

    https://docs.docker.com/compose/install/

  3. Install the source code associated with this book onto your local computer.

    If you have installed Git, use the following command:

    git clone https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices.git ~/repo

    Otherwise, you can simply download the source code from this Uniform Resource Locator (URL): https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices/archive/main.zip. You can then unzip into a folder you create, which we refer to as /repo in this book.

  4. You can now start the Docker daemon running. For Windows or Mac, all you need to do is to activate the Docker Desktop app.

    If you are running Ubuntu or Debian Linux, issue this command:

    sudo service docker start

    For Red Hat, Fedora, or CentOS, use this command:

    sudo systemctl start docker

  5. Build a Docker container associated with this book and bring it online. To do so, proceed as follows.

    From your local computer, open Command Prompt (terminal window). Change the directory to /repo. For the first time only, issue the docker-compose build command to build the environment. Note that you might need root (administrator) privileges to run Docker commands. If this is the case, either run as administrator (for Windows) or preface the command with sudo. Depending on your connection speed, the initial build might take quite a bit of time to complete!

  6. To bring the container up, proceed as follows
  7. From your local computer, open Command Prompt (terminal window). Change the directory to /repo. Bring the Docker container online in background mode by running the following command:
    docker-compose up -d

    Note that you actually don't need to build the container separately. If the container is not built when you issue the docker-compose up command, it will be built automatically. On the other hand, it might be convenient to build the container separately, in which case docker build will suffice.

    Here's a useful command to ensure all containers are running:

    docker-compose ps
  8. To access the running Docker container web server, proceed as follows.

    Open the browser on your local computer. Enter this URL to access PHP 8 code:

    http://localhost:8888

    Enter this URL to access PHP 7 code:

    http://localhost:7777

  9. To open a command shell into the running Docker container, proceed as follows.

    From your local computer, open Command Prompt (terminal window). Issue this command to access the PHP 8 container:

    docker exec -it php8_tips_php8 /bin/bash 

    Issue this command to access the PHP 7 container:

    docker exec -it php8_tips_php7 /bin/bash
  10. When you are finished working with the container, to take it offline open Command Prompt (terminal window) from your local computer and issue this command:
    docker-compose down 

The source code for this chapter is located here:

https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices

Important note

If your host computer uses Advanced RISC Machines (ARM) architecture (for example, Raspberry Pi), you will need to use a modified Dockerfile.

Tip

It would be an excellent idea to get a quick overview of Docker technology and terms by reviewing this article: https://docs.docker.com/get-started/.

We can now begin our discussion by having a look at constructor property promotion.

 

Using constructor property promotion

Aside from the Just-In-Time (JIT) compiler, one of the greatest new features introduced in PHP 8 is constructor property promotion. This new feature combines property declarations and argument lists in the __construct() method signature, as well as assigning defaults. In this section, you will learn how to substantially reduce the amount of coding required in property declarations as well as in the __construct() method signature and body.

Property promotion syntax

The syntax needed to invoke constructor property promotion is identical to that used in PHP 7 and earlier, with the following differences:

  • You need to define a visibility level
  • You do not have to explicitly declare the properties in advance
  • You do not need to make assignments in the body of the __construct() method

Here is a bare-bones example of code that uses constructor property promotion:

// /repo/ch01/php8_prop_promo.php
declare(strict_types=1);
class Test {
    public function __construct(
        public int $id,
        public int $token = 0,
        public string $name = '')
    { }
}
$test = new Test(999);
var_dump($test);

When the preceding code block is executed, this is the output:

object(Test)#1 (3) {
  ["id"]=> int(999)
  ["token"]=> int(0)
  ["name"]=> string(0) ""
}

This shows that an instance of Test type has been created using default values. Now, let's have a look at how this feature might save a substantial amount of coding.

Using property promotion for code reduction

In a conventional OOP PHP class, the following three things need to be done:

  1. Declare the properties, as follows:
    /repo/src/Php8/Image/SingleChar.php
    namespace Php7\Image;
    class SingleChar {
        public $text     = '';
        public $fontFile = '';
        public $width    = 100;
        public $height   = 100;
        public $size     = 0;
        public $angle    = 0.00;
        public $textX    = 0;
        public $textY    = 0;
  2. Identify the properties and their data type in the __construct() method signature, as follows:
    const DEFAULT_TX_X = 25;
    const DEFAULT_TX_Y = 75;
    const DEFAULT_TX_SIZE  = 60;
    const DEFAULT_TX_ANGLE = 0;
    public function __construct(
        string $text,
        string $fontFile,
        int $width  = 100,
        int $height = 100,
        int $size   = self::DEFAULT_TX_SIZE,
        float $angle = self::DEFAULT_TX_ANGLE,
        int $textX  = self::DEFAULT_TX_X,
        int $textY  = self::DEFAULT_TX_Y)   
  3. In the body of the __construct() method, assign values to properties, like this:
    {   $this->text     = $text;
        $this->fontFile = $fontFile;
        $this->width    = $width;
        $this->height   = $height;
        $this->size     = $size;
        $this->angle    = $angle;
        $this->textX    = $textX;
        $this->textY    = $textY;
        // other code not shown 
    }

As the number of constructor arguments increases, the amount of work you need to do also increases significantly. When constructor property promotion is applied, the amount of code required to do the same as previously shown is reduced to one-third of the original.

Let's now have a look at the same block of code as shown previously, but rewritten using this powerful new PHP 8 feature:

// /repo/src/Php8/Image/SingleChar.php
// not all code shown
public function __construct(
    public string $text,
    public string $fontFile,
    public int    $width    = 100,
    public int    $height   = 100,
    public int    $size     = self::DEFAULT_TX_SIZE,
    public float   $angle    = self::DEFAULT_TX_ANGLE,
    public int    $textX    = self::DEFAULT_TX_X,
    public int    $textY    = self::DEFAULT_TX_Y)
    { // other code not shown }

Amazingly, what took 24 lines of code in PHP 7 and earlier can be collapsed into eight lines of code using this new PHP 8 feature!

You are completely free to include other code in the constructor. In many cases, however, constructor property promotion takes care of everything normally done in the __construct() method, which means you can literally leave it empty ({ }).

Now, in the next section, you learn about a new feature called attributes.

Tip

Have a look at the full SingleChar class for PHP 7 here:

https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices/tree/main/src/Php7/Image

Also, the equivalent PHP 8 class is found here:

https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices/tree/main/src/Php8/Image

For more information on this new feature, have a look at the following:

https://wiki.php.net/rfc/constructor_promotion

 

Working with attributes

Another significant addition to PHP 8 is the addition of a brand-new class and language construct known as attributes. Simply put, attributes are replacements for traditional PHP comment blocks that follow a prescribed syntax. When the PHP code is compiled, these attributes are converted internally into Attribute class instances.

This new feature is not going to have an immediate impact on your code today. It will start to become more and more influential, however, as the various PHP open source vendors start to incorporate attributes into their code.

The Attribute class addresses a potentially significant performance issue we discuss in this section, pertaining to an abuse of the traditional PHP comment block to provide meta-instructions. Before we dive into that issue and how Attribute class instances address the problem, we first must review PHP comments.

Overview of PHP comments

The need for this form of language construct arose with the increasing use (and abuse!) of the plain workhorse PHP comment. As you are aware, comments come in many forms, including all of the following:

# This is a "bash" shell script style comment
// this can either be inline or on its own line
/* This is the traditional "C" language style */
/**
 * This is a PHP "DocBlock"
 */

The last item, the famous PHP DocBlock, is now so widely used it's become a de facto standard. The use of DocBlocks is not a bad thing. On the contrary—it's often the only way a developer is able to communicate information about properties, classes, and methods. The problem only arises in how it is treated by the PHP interpretation process.

PHP DocBlock considerations

The original intent of the PHP DocBlock has been stretched by a number of extremely important PHP open-source projects. One striking example is the Doctrine Object-Relational Mapper (ORM) project. Although not mandatory, many developers choose to define ORM properties using annotations nested inside PHP DocBlocks.

Have a look at this partial code example, which defines a class interacting with a database table called events:

namespace Php7\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
 * @ORM\Table(name="events")
 * @ORM\Entity("Application\Entity\Events")
 */
class Events {
    /**
     * @ORM\Column(name="id",type="integer",nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;
    /**
     * @ORM\Column(name="event_key", type="string", 
          length=16, nullable=true, options={"fixed"=true})
     */
    private $eventKey;
    // other code not shown

If you were to use this class as part of a Doctrine ORM implementation, Doctrine would open the file and parse the DocBlocks, searching for @ORM annotations. Despite some concerns over the time and resources needed to parse DocBlocks, this is an extremely convenient way to define the relationship between object properties and database table columns, and is popular with developers who use Doctrine.

Tip

Doctrine offers a number of alternatives to this form of ORM, including Extensible Markup Language (XML) and native PHP arrays. For more information, see https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html#annotations-reference.

Hidden dangers associated with the misuse of DocBlocks

There is yet another danger associated with this abuse of the original purpose of a DocBlock. In the php.ini file, there is a setting named opcache.save_comments. If disabled, this would cause the OpCode cache engine (OPcache) to ignore all comments, including DocBlocks. If this setting is in effect, a Doctrine-based application using @ORM annotations in DocBlocks would malfunction.

Another problem has to do with how comments are parsed—or, more to the point, how comments are not parsed. In order to use the contents of a comment, the PHP application needs to open the file and parse it line by line. This is an expensive process in terms of time and resource utilization.

The Attribute class

In order to address hidden dangers, in PHP 8 a new Attribute class is provided. Instead of using DocBlocks with annotations, developers can define the equivalent in the form of attributes. An advantage of using attributes rather than DocBlocks is that they are a formal part of the language and are thus tokenized and compiled along with the rest of your code.

Important note

In this chapter, and also in the PHP documentation, reference to attributes refers to instances of the Attribute class.

Actual performance metrics are not yet available that compare the loading of PHP code containing DocBlocks with the loading of code that contains attributes.

Although the benefits of this approach are not yet seen, as the various open source project vendors start to incorporate attributes into their offerings you will start to see an improvement in speed and performance.

Here is the Attribute class definition:

class Attribute {
    public const int TARGET_CLASS = 1;
    public const int TARGET_FUNCTION = (1 << 1);
    public const int TARGET_METHOD = (1 << 2);
    public const int TARGET_PROPERTY = (1 << 3);
    public const int TARGET_CLASS_CONSTANT = (1 << 4);
    public const int TARGET_PARAMETER = (1 << 5);
    public const int TARGET_ALL = ((1 << 6) - 1);
    public function __construct(
        int $flags = self::TARGET_ALL) {}
}

As you can see from the class definition, the main contribution from this class, used internally by PHP 8, is a set of class constants. The constants represent bit flags that can be combined using bitwise operators.

Attribute syntax

Attributes are enclosed using a special syntax borrowed from the Rust programming language. What goes inside the square brackets is pretty much left to the developer. An example can be seen in the following snippet:

#[attribute("some text")] 
// class, property, method or function (or whatever!)

Returning to our example of the SingleChar class, here's how it might appear using traditional DocBlocks:

// /repo/src/Php7/Image/SingleChar.php
namespace Php7\Image;
/**
 * Creates a single image, by default black on white
 */
class SingleChar {
    /**
     * Allocates a color resource
     *
     * @param array|int $r,
     * @param int $g
     * @param int $b]
     * @return int $color
     */
    public function colorAlloc() 
    { /* code not shown */ } 

Now, have a look at the same thing using attributes:

// /repo/src/Php8/Image/SingleChar.php
namespace Php8\Image;
#[description("Creates a single image")]
class SingleChar {
    #[SingleChar\colorAlloc\description("Allocates color")]
    #[SingleChar\colorAlloc\param("r","int|array")]
    #[SingleChar\colorAlloc\param("g","int")]
    #[SingleChar\colorAlloc\param("b","int")]
    #[SingleChar\colorAlloc\returns("int")]
    public function colorAlloc() { /* code not shown */ }

As you can see, in addition to providing a more robust compilation and avoiding the hidden dangers mentioned, it's also more efficient in terms of space usage.

Tip

What goes inside the square brackets does have some restrictions; for example, although #[returns("int")] is allowed, this is not: #[return("int"). The reason for this is because return is a keyword.

Another example has to do with union types (explained in the Exploring new data types section). You can use #[param("int|array test")] in an attribute, but this is not allowed: #[int|array("test")]. Another peculiarity is that class-level attributes must be placed immediately before the class keyword and after any use statements.

Viewing attributes using Reflection

If you need to get attribute information from a PHP 8 class, the Reflection extension has been updated to include attribute support. A new getAttributes() method that returns an array of ReflectionAttribute instances has been added.

In the following block of code, all the attributes from the Php8\Image\SingleChar::colorAlloc() method are revealed:

<?php
// /repo/ch01/php8_attrib_reflect.php
define('FONT_FILE', __DIR__ . '/../fonts/FreeSansBold.ttf');
require_once __DIR__ . '/../src/Server/Autoload/Loader.php';
$loader = new \Server\Autoload\Loader();
use Php8\Image\SingleChar;
$char    = new SingleChar('A', FONT_FILE);
$reflect = new ReflectionObject($char);
$attribs = $reflect->getAttributes();
echo "Class Attributes\n";
foreach ($attribs as $obj) {
    echo "\n" . $obj->getName() . "\n";
    echo implode("\t", $obj->getArguments());
}
echo "Method Attributes for colorAlloc()\n";
$reflect = new ReflectionMethod($char, 'colorAlloc');
$attribs = $reflect->getAttributes();
foreach ($attribs as $obj) {
    echo "\n" . $obj->getName() . "\n";
    echo implode("\t", $obj->getArguments());
}

Here is the output from the code shown in the preceding snippet:

<pre>Class Attributes
Php8\Image\SingleChar
Php8\Image\description
Creates a single image, by default black on whiteMethod
Attributes for colorAlloc()
Php8\Image\SingleChar\colorAlloc\description
Allocates a color resource
Php8\Image\SingleChar\colorAlloc\param
r    int|array
Php8\Image\SingleChar\colorAlloc\param
g    int
Php8\Image\SingleChar\colorAlloc\param
b    int
Php8\Image\SingleChar\colorAlloc\returns
int

The preceding output shows that attributes can be detected using the Reflection extension classes. Finally, the actual method is shown in this code example:

namespace Php8\Image;use Attribute;
use Php8\Image\Strategy\ {PlainText,PlainFill};
#[SingleChar]
#[description("Creates black on white image")]
class SingleChar {
    // not all code is shown
    #[SingleChar\colorAlloc\description("Allocates color")]
    #[SingleChar\colorAlloc\param("r","int|array")]
    #[SingleChar\colorAlloc\param("g","int")]
    #[SingleChar\colorAlloc\param("b","int")]
    #[SingleChar\colorAlloc\returns("int")]    
    public function colorAlloc(
         int|array $r, int $g = 0, int $b = 0) {
        if (is_array($r))
            [$r, $g, $b] = $r;
        return \imagecolorallocate(
              $this->image, $r, $g, $b);
    }
}

Now that you have an idea of how attributes can be used, let's continue our coverage of new features by discussing match expressions, followed by named arguments.

Tip

For more information on this new feature, have a look at the following web page:

https://wiki.php.net/rfc/attributes_v2

Also, see this update:

https://wiki.php.net/rfc/shorter_attribute_syntax_change

Information on PHP DocBlocks can be found here:

https://phpdoc.org/

For more information about Doctrine ORM, have a look here:

https://www.doctrine-project.org/projects/orm.html

Documentation on php.ini file settings can be found here:

https://www.php.net/manual/en/ini.list.php

Read about PHP Reflection here:

https://www.php.net/manual/en/language.attributes.reflection.php

Information about the Rust programming language can be found in this book: https://www.packtpub.com/product/mastering-rust-second-edition/9781789346572

 

Incorporating match expressions into your program code

Among the many incredibly useful features introduced in PHP 8, match expressions definitely stand out. Match expressions are a more accurate shorthand syntax that can potentially replace the tired old switch statement that came directly from the C language. In this section, you will learn how to produce cleaner and more accurate program code by replacing switch statements with match expressions.

Match expression general syntax

Match expression syntax is much like that of an array, where the key is the item to match and the value is an expression. Here is the general syntax for match:

$result = match(<EXPRESSION>) {
    <ITEM> => <EXPRESSION>,
   [<ITEM> => <EXPRESSION>,]
    default => <DEFAULT EXPRESSION>
};

The expression must be a valid PHP expression. Examples of expressions could include any of the following:

  • A specific value (for example, "some text")
  • An operation (for example, $a + $b)
  • An anonymous function or class

The only limitation is that the expression has to be defined in a single line of code. Major differences between match and switch are summarized here:

Table 1.1 – Differences between match and switch

Table 1.1 – Differences between match and switch

Other than the differences noted, match and switch both allow case aggregation, as well as providing support for a default case.

switch and match examples

Here is a simple example that renders a currency symbol using switch:

// /repo/ch01/php7_switch.php
function get_symbol($iso) {
    switch ($iso) {
        case 'CNY' :
            $sym = '¥';
            break;
        case 'EUR' :
            $sym = '€';
            break;
        case 'EGP' :
        case 'GBP' :
            $sym = '£';
            break;
        case 'THB' :
            $sym = '฿';
            break;
        default :
            $sym = '$';
    }
    return $sym;
}
$test = ['CNY', 'EGP', 'EUR', 'GBP', 'THB', 'MXD'];
foreach ($test as $iso)
    echo 'The currency symbol for ' . $iso
         . ' is ' . get_symbol($iso) . "\n";

When this code is executed, you see the currency symbols for each of the International Organization for Standardization (ISO) currency codes in the $test array. The same result as that shown in the preceding code snippet can be obtained in PHP 8, using the following code:

// /repo/ch01/php8_switch.php
function get_symbol($iso) {
    return match ($iso) {
        'EGP','GBP' => '£',
        'CNY'       => '¥',
        'EUR'       => '€',
        'THB'       => '฿',
        default     => '$'
    };
}
$test = ['CNY', 'EGP', 'EUR', 'GBP', 'THB', 'MXD'];
foreach ($test as $iso)
    echo 'The currency symbol for ' . $iso
         . ' is ' . get_symbol($iso) . "\n";

Both examples produce an identical output, as illustrated here:

The currency symbol for CNY is ¥
The currency symbol for EGP is £
The currency symbol for EUR is €
The currency symbol for GBP is £
The currency symbol for THB is ฿
The currency symbol for MXD is $

As mentioned previously, both code examples produce a list of currency symbols for the list of ISO currency codes stored in the $test array.

Complex match example

Returning to our CAPTCHA project, assume that we wish to introduce distortion to make the CAPTCHA characters more difficult to read. To accomplish this goal, we introduce a number of strategy classes, each producing a different distortion, as summarized in this table:

Table 1.2 – CAPTCHA distortion strategy classes

Table 1.2 – CAPTCHA distortion strategy classes

After randomizing the list of strategies to be employed, we use a match expression to execute the results, as follows:

  1. First we define an autoloader, import the classes to be used, and list potential strategies to employ, as illustrated in the following code snippet:
    // /repo/ch01/php8_single_strategies.php
    // not all code is shown
    require_once __DIR__ . '/../src/Server/Autoload/Loader.php';
    $loader = new \Server\Autoload\Loader();
    use Php8\Image\SingleChar;
    use Php8\Image\Strategy\ {LineFill,DotFill,Shadow,RotateText};
    $strategies = ['rotate', 'line', 'line',
                   'dot', 'dot', 'shadow'];
  2. Next, we generate the CAPTCHA phrase, as follows:
    $phrase = strtoupper(bin2hex(random_bytes(NUM_BYTES)));
    $length = strlen($phrase);
  3. We then loop through each character in the CAPTCHA phrase and create a SingleChar instance. The initial call to writeFill() creates the white background canvas. We also need to call shuffle() to randomize the list of distortion strategies. The process is illustrated in the following code snippet:
    $images = [];
    for ($x = 0; $x < $length; $x++) {
        $char = new SingleChar($phrase[$x], FONT_FILE);
        $char->writeFill();
        shuffle($strategies);
  4. We then loop through the strategies and layer distortions upon the original image. This is where the match expression comes into play. Notice that one strategy needs additional lines of code. Because match can only support a single expression, we simply wrap the multiple lines of code into an anonymous function, as follows:
    foreach ($strategies as $item) {
        $func = match ($item) {    
            'rotate' => RotateText::writeText($char),
            'line' => LineFill::writeFill(
                $char, rand(1, 10)),
            'dot' => DotFill::writeFill($char, rand(10, 20)),
            'shadow' => function ($char) {
                $num = rand(1, 8);
                $r   = rand(0x70, 0xEF);
                $g   = rand(0x70, 0xEF);
                $b   = rand(0x70, 0xEF);
                return Shadow::writeText(
                    $char, $num, $r, $g, $b);},
            'default' => TRUE
        };
        if (is_callable($func)) $func($char);
    }
  5. All that remains to be done is to overlay the image with the actual CAPTCHA phrase by calling writeText() with no arguments. After that, we save the distorted image as a Portable Network Graphics (PNG) file for display, as illustrated in the following code snippet:
        $char->writeText();
        $fn = $x . '_' 
             . substr(basename(__FILE__), 0, -4) 
             . '.png';
        $char->save(IMG_DIR . '/' . $fn);
        $images[] = $fn;
    }
    include __DIR__ . '/captcha_simple.phtml';

Here is the result, running the preceding example from a browser that points to the Docker container associated with this book:

Figure 1.1 – Distorted CAPTCHA using match expression

Figure 1.1 – Distorted CAPTCHA using match expression

Next, we'll have a look at another really great feature: named arguments.

Tip

You can see the original proposal for match expressions here: https://wiki.php.net/rfc/match_expression_v2

 

Understanding named arguments

Named arguments represent a way to avoid confusion when calling functions or methods with a large number of arguments. This not only helps avoid problems with arguments being supplied in an incorrect order, but also helps you to skip arguments with defaults. In this section, you will learn how to apply named arguments to improve the accuracy of your code, reduce confusion during future maintenance cycles, and make your method and function calls more concise. We start by examining the generic syntax required to use named arguments.

Named argument generic syntax

In order to use named arguments, you need to know the names of the variables used in the function or method signature. You then specify that name, without the dollar sign, followed by a colon and the value to be supplied, as follows:

$result = function_name( arg1 : <VALUE>, arg2 : <value>);

When the function_name() function is invoked, the values are passed to the arguments corresponding to arg1, arg2, and so on.

Calling core functions using named arguments

One of the most common reasons to use named arguments is when you call a core PHP function that has a large number of parameters. As an example, here's the function signature for setcookie():

setcookie ( string $name [, string $value = "" 
    [, int $expires = 0 [, string $path = "" 
    [, string $domain = "" [, bool $secure = FALSE 
    [, bool $httponly = FALSE ]]]]]] ) : bool

Let's say that all you really wanted to set were the name, value, and httponly arguments. Before PHP 8, you would have had to look up the default values and supply them, in order, until you got to the one you wished to override. In the following case, we wish to set httponly to TRUE:

setcookie('test',1,0,0,'','',FALSE,TRUE);

Using named arguments, the equivalent in PHP 8 would be as follows:

setcookie('test',1,httponly: TRUE);

Note that we do not need to name the first two parameters as they are supplied in order.

Tip

In PHP extensions, named arguments do not always match the names of variables you see in the PHP documentation for function or method signatures. As an example, the function imagettftext() shows a variable $font_filename in its function signature. If you scroll down a bit further, however, you'll see in the Parameters section, that the named parameter is fontfile.

If you encounter a fatal Error: Unknown named parameter $NAMED_PARAM. Always use the name as listed in the Parameters section of the documentation rather than the name of the variable in the function or method signature.

Order independence and documentation

Another use for named arguments is to provide order independence. In addition, for certain core PHP functions, the sheer number of parameters presents a documentation nightmare.

As an example, have a look here at the function signature for imagefttext() (note that this function is central to the chapter project of producing a secure CAPTCHA image):

imagefttext ( object $image , float $size , float $angle , 
    int $x , int $y , int $color , string $fontfile , 
    string $text [, array $extrainfo ] ) : array 

As you can imagine, trying to remember the names and order of these parameters when reviewing your work 6 months later might be problematic.

Important note

In PHP 8, the image creation functions (for example, imagecreate()) now return a GdImage object instance instead of a resource. All image functions in the GD extension have been rewritten to accommodate this change. There's no need to rewrite your code!

Accordingly, using named arguments, the following function call would be acceptable in PHP 8:

// /repo/ch01/php8_named_args.php
// not all code is shown
$rotation = range(40, -40, 10);
foreach ($rotation as $key => $offset) {
    $char->writeFill();
    [$x, $y] = RotateText::calcXYadjust($char, $offset);
    $angle = ($offset > 0) ? $offset : 360 + $offset;
    imagettftext(
        angle        : $angle,
        color        : $char->fgColor,
        font_filename : FONT_FILE,
        image        : $char->image,
        size         : 60,                
        x            : $x,
        y            : $y,
        text         : $char->text);
    $fn = IMG_DIR . '/' . $baseFn . '_' . $key . '.png';
    imagepng($char->image, $fn);
    $images[] = basename($fn);
}

The code example just shown writes out a string of distorted characters as a set of PNG image files. Each character is rotated 10 degrees clockwise with respect to its neighboring images. Note how named arguments are applied to make arguments to the imagettftext() function easier to understand.

Named arguments can also be applied to functions and methods of your own creation. In the next section, we cover new data types.

Tip

A detailed analysis of named arguments can be found here:

https://wiki.php.net/rfc/named_params

 

Exploring new data types

One thing any entry-level PHP developer learns is which data types PHP has available and how to use them. The basic data types include int (integer), float, bool (Boolean), and string. Complex data types include array and object. In addition, there are other data types such as NULL and resource. In this section, we discuss a few new data types introduced in PHP 8, including union types and mixed types.

Important note

It's extremely important not to confuse a data type with a data format. This section describes data types. A data format, on the other hand, would be a way of representing data used as part of a transmission or for storage. Examples of a data format would include XML, JavaScript Object Notation (JSON), and YAML Ain't Markup Language (YAML).

Union types

Unlike other data types such as int or string, it's important to note that there is no data type explicitly called union. Rather, when you see a reference to union types, what is meant is that PHP 8 introduces a new syntax that allows you to specify a union of types, instead of just one. Let's now have a look at the generic syntax for union types.

Union type syntax

The generic syntax for union types is as follows:

function ( type|type|type $var) {}

In place of type, you would supply any of the existing data types (for example, float or string). There are a few restrictions, however, which for the most part make complete sense. This table summarizes the more important restrictions:

Table 1.3 – Disallowed union types

Table 1.3 – Disallowed union types

As you can see from this list of exceptions, defining a union type is primarily a matter of common sense.

Tip

Best practice: When using union types, type coercion (the process whereby PHP converts a data type internally to satisfy the requirements of the function) can be an issue if strict type checking is not enforced. Accordingly, it's a best practice to add the following at the top of any file where union types are used: declare(strict_types=1);.

For more information, see the documentation reference here:

https://www.php.net/manual/en/language.types.declarations.php#language.types.declarations.strict

Union type example

For a simple illustration, let's return to the SingleChar class used as an example in this chapter. One of the methods is colorAlloc(). This method allocates a color from an image, leveraging the imagecolorallocate() function. It accepts as arguments integer values that represent red, green, and blue.

For the sake of argument, let's say that the first argument could actually be an array representing three values—one each for red, green, and blue. In this case, the argument type for the first value cannot be int otherwise, if an array were provided, an error would be thrown if strict type checking were to be turned on.

In earlier versions of PHP, the only solution would be to remove any type check from the first argument and to indicate that multiple types are accepted in the associated DocBlock. Here's how the method might appear in PHP 7:

/**
 * Allocates a color resource
 *
 * @param array|int $r
 * @param int $g
 * @param int $b]
 * @return int $color
 */
public function colorAlloc($r, $g = 0, $b = 0) {
    if (is_array($r)) {
        [$r, $g, $b] = $r;
    }
    return \imagecolorallocate($this->image, $r, $g, $b);
}

The only indication of the data type for the first parameter, $r, is the @param array|int $r DocBlock annotation and the fact that there is no data type hint associated with that argument. In PHP 8, taking advantage of union types, notice the difference here:

#[description("Allocates a color resource")]
#[param("int|array r")]
#[int("g")]
#[int("b")]
#[returns("int")]
public function colorAlloc(
    int|array $r, int $g = 0, int $b = 0) {
    if (is_array($r)) {
        [$r, $g, $b] = $r;
    }
    return \imagecolorallocate($this->image, $r, $g, $b);
}

In the preceding example, in addition to the presence of attribute that indicates the first argument can accept either an array or an int type, in the method signature itself, the int|array union type makes this choice clear.

mixed type

mixed is another new type introduced in PHP 8. Unlike a union type, mixed is an actual data type that represents the ultimate union of types. It's used to indicate that any and all data types are accepted. In a certain sense, PHP already has this facility: simply omit the data type altogether, and it's an implied mixed type!

Tip

You will see references to a mixed type in the PHP documentation. PHP 8 formalizes this representation by making it an actual data type.

Why use a mixed type?

Hold for a second—you might be thinking at this point: why bother using a mixed type at all? To put your mind at ease, this is an excellent question, and there is no compelling reason to use this type.

However, by using mixed in a function or method signature, you clearly signal your intention for the use of this parameter. If you were to simply leave the data type blank, other developers later using or reviewing your code might think that you forgot to add the type. At the very least, they will be uncertain of the nature of the untyped argument.

The effect of a mixed type on inheritance

As a mixed type represents the ultimate example of widening, it can be used to widen the data type definition when one class extends from another. Here is an example using a mixed type, illustrating this principle:

  1. First, we define the parent class with the more restrictive data type of object, as follows:
    // /repo/ch01/php8_mixed_type.php
    declare(strict_types=1);
    class High {
        const LOG_FILE = __DIR__ . '/../data/test.log';  
        protected static function logVar(object $var) {     
            $item = date('Y-m-d') . ':'
                  . var_export($var, TRUE);
            return error_log($item, 3, self::LOG_FILE);
        }
    }
  2. Next, we define a Low class that extends High, as follows:
    class Low extends High {
        public static function logVar(mixed $var) {
            $item = date('Y-m-d') . ':'
                . var_export($var, TRUE);
            return error_log($item, 3, self::LOG_FILE);
        }
    }

    Note in the Low class that the data type for the logVar()method has been widened into mixed.

  3. Finally, we create an instance of Low and execute it with test data. As you can see from the results shown in the following code snippet, everything works fine:
    if (file_exists(High::LOG_FILE)) unlink(High::LOG_FILE)
    $test = [
        'array' => range('A', 'F'),
        'func' => function () { return __CLASS__; },
        'anon' => new class () { 
            public function __invoke() { 
                return __CLASS__; } },
    ];
    foreach ($test as $item) Low::logVar($item);
    readfile(High::LOG_FILE);

Here is the output from the preceding example:

2020-10-15:array (
  0 => 'A',
  1 => 'B',
  2 => 'C',
  3 => 'D',
  4 => 'E',
  5 => 'F',
)2020-10-15:Closure::__set_state(array(
))2020-10-15:[email protected]/repo/ch01/php8_mixed_type.php:28$1::__set_state(array())

The preceding code block logs a variety of different data types and then displays the contents of the log file. In the process, this shows us there are no inheritance issues in PHP 8 when a child class overrides a parent class method and substitutes a data type of mixed in place of a more restrictive data type, such as object.

Next, we have a look at using typed properties.

Tip

Best practice: Assign specific data types to all arguments when defining functions or methods. If a few different data types are acceptable, define a union type. Otherwise, if none of this applies, fall back to a mixed type.

For information on union types, see this documentation page:

https://wiki.php.net/rfc/union_types_v2

For more information on a mixed type, have a look here: https://wiki.php.net/rfc/mixed_type_v2.

 

Improving code using typed properties

In the first section of this chapter, Using constructor property promotion, we discussed how data types can be used to control the type of data supplied as arguments to functions or class methods. What this approach fails to do, however, is guarantee that the data type never changes. In this section, you will learn how assigning a data type at the property level provides stricter control over the use of variables in PHP 8.

What are typed properties?

This extremely important feature was introduced in PHP 7.4 and continues in PHP 8. Simply put, a typed property is a class property with a data type preassigned. Here is a simple example:

// /repo/ch01/php8_prop_type_1.php
declare(strict_types=1)
class Test {
    public int $id = 0;
    public int $token = 0;
    public string $name = '';
}
$test = new Test();
$test->id = 'ABC';

In this example, if we attempt to assign a value representing a data type other than int to $test->id, a Fatal error is thrown. Here is the output:

Fatal error: Uncaught TypeError: Cannot assign string to property Test::$id of type int in /repo/ch01/php8_prop_type_1.php:11 Stack trace: #0 {main} thrown in /repo/ch01/php8_prop_type_1.php on line 11 

As you can see from the preceding output, a Fatal error is thrown when the wrong data type is assigned to a typed property.

You have already been exposed to one form of property typing: constructor property promotion. All properties defined using constructor property promotion are automatically property typed!

Why is property typing important?

Typed properties is part of a general trend in PHP first seen in PHP 7. The trend is toward making language refinements that restrict and tighten the use of your code. This leads to better code, which means fewer bugs.

The following example illustrates the danger of relying solely upon property-type hinting to control the data type of properties:

// /repo/ch01/php7_prop_danger.php
declare(strict_types=1);
class Test {
    protected $id = 0;
    protected $token = 0;
    protected $name = '';
    public function __construct(
        int $id, int $token, string $name) {
        $this->id = $id;
        $this->token = md5((string) $token);
        $this->name = $name;
    }
}
$test = new Test(111, 123456, 'Fred');
var_dump($test);

In the preceding example, notice in the __construct() method that the $token property is accidentally converted to a string. Here is the output:

object(Test)#1 (3) {
  ["id":protected]=>  int(111)
  ["token":protected]=>
  string(32) "e10adc3949ba59abbe56e057f20f883e"
  ["name":protected]=>  string(4) "Fred"
}

Any subsequent code expecting $token to be an integer might either fail or produce unexpected results. Now, have a look at the same thing in PHP 8 using typed properties:

// /repo/ch01/php8_prop_danger.php
declare(strict_types=1);
class Test {
    protected int $id = 0;
    protected int $token = 0;
    protected string $name = '';
    public function __construct(
        int $id, int $token, string $name) {        
        $this->id = $id;
        $this->token = md5((string) $token);
        $this->name = $name;
    }
}
$test = new Test(111, 123456, 'Fred');
var_dump($test);

Property typing prevents any change to the preassigned data type from occurring, as you can see from the output shown here:

Fatal error: Uncaught TypeError: Cannot assign string to property Test::$token of type int in /repo/ch01/php8_prop_danger.php:12

As you can see from the preceding output, a Fatal error is thrown when the wrong data type is assigned to a typed property. This example demonstrates that not only does assigning a data type to a property prevent misuse when making direct assignments, but it also prevents misuse of the property inside class methods as well!

Property typing can lead to a reduction in code

Another beneficial side effect of introducing property typing to your code is a potential reduction in the amount of code needed. As an example, consider the current practice of marking properties with a visibility of private or protected, and then creating a series of get and set methods to control access (also called getters and setters).

Here is how that might appear:

  1. First, we define a Test class with protected properties, as follows:
    // /repo/ch01/php7_prop_reduce.php
    declare(strict_types=1);
    class Test {
     protected $id = 0;
     protected $token = 0;
     protected $name = '';o
  2. Next, we define a series of get and set methods to control access to the protected properties, as follows:
        public function getId() { return $this->id; }
        public function setId(int $id) { $this->id = $id; 
        public function getToken() { return $this->token; }
        public function setToken(int $token) {
            $this->token = $token;
        }
        public function getName() {
            return $this->name;
        }
        public function setName(string $name) {
            $this->name = $name;
        }
    }
  3. We then use the set methods to assign values, as follows:
    $test = new Test();
    $test->setId(111);
    $test->setToken(999999);
    $test->setName('Fred');
  4. Finally, we display the results in a table, using the get methods to retrieve property values, as follows:
    $pattern = '<tr><th>%s</th><td>%s</td></tr>';
    echo '<table width="50%" border=1>';
    printf($pattern, 'ID', $test->getId());
    printf($pattern, 'Token', $test->getToken());
    printf($pattern, 'Name', $test->getName());
    echo '</table>';

Here is how that might appear:

Table 1.4 – Output using Get methods

Table 1.4 – Output using Get methods

The main purpose achieved by marking properties as protected (or private) and by defining getters and setters is to control access. Often, this translates into a desire to prevent the property data type from changing. If this is the case, the entire infrastructure can be replaced by assigning property types.

Simply changing the visibility to public alleviates the need for get and set methods; however, it does not prevent the property data from being changed! Using PHP 8 property types achieves both goals: it eliminates the need for get and set methods and also prevents the data type from being accidentally changed.

Notice here how much less code is needed to achieve the same results in PHP 8 using property typing:

// /repo/ch01/php8_prop_reduce.php
declare(strict_types=1);
class Test {
    public int $id = 0;
    public int $token = 0;
    public string  $name = '';
}
// assign values
$test = new Test();
$test->id = 111;
$test->token = 999999;
$test->name = 'Fred';
// display results
$pattern = '<tr><th>%s</th><td>%s</td></tr>';
echo '<table width="50%" border=1>';
printf($pattern, 'ID', $test->id);
printf($pattern, 'Token', $test->token);
printf($pattern, 'Name', $test->name);
echo '</table>';

The preceding code example shown produces exactly the same output as the previous example and also achieves even better control over property data types. Using typed properties, in this example, we achieved a 50% reduction in the amount of code needed to produce the same result!

Tip

Best practice: Use typed properties whenever possible, except in situations where you explicitly want to allow the data type to change.

 

Summary

In this chapter, you learned how to write better code using the new PHP 8 data types: mixed and union types. You also learned about how using named arguments can not only improve the readability of your code but can also help prevent accidental misuse of class methods and PHP functions, as well as providing a great way to skip over default arguments.

This chapter also taught you how the new Attribute class can be used as an eventual replacement for PHP DocBlocks, serving to improve the overall performance of your code while providing a solid means of documenting classes, methods, and functions.

In addition, we looked at how PHP 8 can greatly reduce the amount of code needed by earlier PHP versions by taking advantage of constructor argument promotion and typed properties.

In the next chapter, you will learn about new PHP 8 features at the functional and procedural level.

About the Author

  • Doug Bierer

    Doug Bierer has been hooked on computers since his first program, written on a DEC PDP-8, in 1971. In his wide-ranging career, he has been a professional contract programmer since 1978, having written applications in BASIC, PL/I, assembler, FORTH, C, C++, dBase/FoxBase/Clipper, Pascal, Perl, Java, and PHP. He deployed his first website in 1993 while living in San Francisco. He speaks four languages, has traveled extensively, and now resides in Thailand. He also spent some years doing system administration and TCP/IP networking. Some of his technical works include PHP 7 Programming Cookbook and Learning MongoDB 4.x (Packt), as well as Learning PHP and MySQL, Learning PHP Security and Learning Doctrine (O'Reilly Media).

    Browse publications by this author
PHP 8 Programming Tips, Tricks and Best Practices
Unlock this book and the full library for $5 a month*
Start now