BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles PHP 8 — Attributes, Match Expression and Other Improvements

PHP 8 — Attributes, Match Expression and Other Improvements

Bookmarks

Key Takeaways

  • PHP 8 is a major update to PHP that introduces several new features and performance optimizations. 
  • Attributes in PHP 8 are configuration directives used to add syntactic metadata applied to individual classes, functions, methods, and other constructs. 
  • The new match expression in PHP 8 is a control flow structure that matches a given subject expression with one or more alternative conditional expressions using identity comparison and returns a value from the matched branch. 
  • PHP 8.1 adds support for using the new operator for parameter default values, attribute arguments, static variable initializers, and global constant initializers.  
  • PHP 8 adds support for using the instanceof operator with arbitrary expressions. 
     

This article is part of the article series "PHP 8.x". You can subscribe to receive notifications about new articles in this series via RSS.

PHP continues to be one of the most widely used scripting languages on  the web with 77.3% of all the websites whose server-side programming language is known using it according to w3tech. PHP 8 brings many new features and other improvements, which we shall explore in this article series.

PHP 8 is a major update to PHP that introduces several new features and performance optimizations, including attributes, match expression, instanceof operator, new operator, a new JIT compiler, and more. 

Attributes provide a way to add metadata to classes, methods, functions, parameters, properties, and class constants. Attributes are similar to annotations that are supported by some other languages. The new operator, only available starting with PHP 8.1, can be used in initializers for default parameter values, static variables, and attribute arguments. The match expression  is a new control structure that evaluates a subject expression with multiply-branched conditional expressions using the identity operator and executes the matched branch. The instanceof operator, which previously could only be used with a class object, can also be used with any arbitrary expression . The JIT (Just-In-Time) compiler  brings performance and usability improvements. 

Some of the examples are based on  data structures requiring to download and install the php_ds extension from here. On Windows, download the  DLL for 8.1 Non Thread Safe (NTS) x64 from here, and extract the php_ds-1.4.0-8.1-nts-vs16-x64.zip file to a directory. Copy the php_ds.dll from the extracted directory to the .\ext directory within the PHP 8.x installation root, for example C:\php-8.1.9-nts-Win32-vs16-x64\ext.  Add the following line to the php.ini configuration file:

extension=php_ds

Restart the PHP built-in server if it is already running.

Attributes

Attributes are configuration directives used to annotate or decorate classes, methods, functions, parameters, properties, and class constants with structured and machine-readable metadata. “Structured” means the metadata information may be read and parsed, making attributes different from unstructured doc-comments, which are just plain strings.  Attributes can be used to provide configuration and other information that is relevant only some of the time, and therefore embedded, not hard-coded into the PHP script. The following is the typical sequence for using attributes:

  1. Declare an attribute class. An attribute class is a regular PHP class preceded by #[Attribute] on a line by itself. The class may optionally declare a constructor. An attribute is identified by its class; the class name is the attribute name when the attribute is used.  
  2. Apply, or use the attribute on classes, methods, functions, parameters, properties, and class constants declarations. As an example, if the attribute class is called Validate, the attribute is used as  #[Validate]. The same attribute may be used multiple times on different declarations. And multiple attributes may be applied to the same declaration. 
  3. Get or read the attributes, if needed, using the Reflection APIs at runtime.

Attributes can have several uses, such as:

  • Providingn alternative to an interface, the benefit being that whereas a class implementing an interface requires all methods from the interface to be implemented, an attribute may be used only when needed, thus avoiding unnecessary method implementations. 
  • Altering compilation, diagnostics, code generation, and runtime behavior. 
  • Separating PHP engine and extensions. PHP core, and extensions, could have some attributes on some of the declarations. 
  • Embedding configuration information specific to a declaration. As an example, attributes on a method may indicate which events the method listens to.
  • Migrating from docblocks to attributes. 

By default, an attribute can be used on any or all of the supported declaration types. But an attribute use could be limited to one or more types of declarations using selected bitmask flags Attribute::TARGET_CLASS, Attribute::TARGET_FUNCTION, Attribute::TARGET_METHOD, Attribute::TARGET_PROPERTY, Attribute::TARGET_CLASS_CONSTANT. Attribute::TARGET_PARAMETER, Attribute::TARGET_ALL. The Attribute::TARGET_ALL flag is the default. An attribute can be used multiple times on a single declaration using the bitmask flag Attribute::IS_REPEATABLE. 

Next, we shall explore the use of attributes with an example. Consider that you want to sort an array using one or more sort functions, such as sort() for sorting in ascending order, rsort() for sorting in descending order, and shuffle() for shuffling in random order. You may also want to validate the input array to verify that it is not an empty array, or that it has at least two elements to make sorting relevant. The sort type information and the validation information could be provided in the form of attributes. 

First, declare an attribute class for validation.

#[Attribute]
class Validate {}

Then, declare another attribute class for the sort type. The bitmask flags Attribute::TARGET_CLASS, and Attribute::IS_REPEATABLE indicate that the attribute is only meant to be used with a class declaration, and that the attribute is repeatable. 

#[Attribute(Attribute::TARGET_CLASS|Attribute::IS_REPEATABLE)]
class SortType {

function __construct($sortType){
        $this->sortType = $sortType;
    }
}

Declare now an interface with a single method called sortArray().

interface Sort 
{   
    public function sortArray();
}

Finally, declare a class that implements the Sort interface.

class SortImpl implements Sort
{

…}

Within the class, declare two class properties, one for the sort type, and the second for the array to be sorted.

public string $sortType="";
public $arrayToSort=array("B", "A", "f", "C");

Declare a method to validate that the array to be sorted is not an empty array. Annotate the method with the #[Validate] attribute. Applying the attribute to the method makes the method discoverable using the Reflection APIs.

#[Validate]
    public function arrayEmpty()
    {
        if (count($this->arrayToSort) === 0) {
            throw new RuntimeException("Array is empty; please provide a non-empty array");
        }
    }

Declare a second method for validating that the array has at least two elements, and apply the #[Validate] attribute to it.

#[Validate]
    public function arraySize()
    {
        if (sizeof($this->arrayToSort) < 2) {
            throw new RuntimeException("Please provide an array of size 2 or more");
        }
    }

Implement the sortArray function. Based on the value of the SortType, make an ascending/descending/shuffle sort.

public function sortArray()
    { 
        
         if ($this->sortType == "asc") {
            …

        } elseif ($this->sortType == "desc") {
            …

        } else {
              
             …

    }

Add a function called performSort(Sort $sort) to perform the sort. In the function, apply the validations before performing the sort. 

Declare a class and apply the #[SortType] attribute to it. As the attribute is repeatable, apply the attribute multiple times to perform different types of sort. 

#[SortType(sortType: "desc")] 
#[SortType(sortType: "shuffle")]  
#[SortType(sortType: "asc")]              
class QSort
{
}

Finally, create an instance of the class SortImpl.

$sort = new SortImpl();

Get the class attributes using the Reflection API.

$ref    =   new ReflectionClass(QSort::class);
$attrs  =   $ref->getAttributes();  

Iterate over the attributes array to call the performSort() function to perform the sort for each type of sort type, and output the result of the sort.

foreach ($attrs as $attr) {
…

}

The complete sort.php script to demonstrate the use of attributes is available on GitHub. Copy the sort.php script to the scripts folder, and with the PHP built-in server running and listening at http://localhost:8000 call the sort.php script in a browser with url http://localhost:8000/scripts/sort.php. The result for the sort using the different sort types is displayed in the browser as shown in Figure 1.

Figure 1. Result of sorting using attributes 

Because the shuffle sort is a random sort the result for shuffle is likely to be different  each time the script is run, as shown in Figure 2.

Figure 2. Shuffle sort produces a different result

Because the sample array to sort has 4 elements, none of the validations fails. If the sample array is empty, on the contrary, the same script, with only the sample array changed, generates a runtime exception message : Uncaught RuntimeException: Array is empty; please provide a non-empty array. Similarly, if the sample array has only 1 element a runtime exception is generated with message : Uncaught RuntimeException: Please provide an array of size 2 or more. 

The enhanced new operator  

The new operator is used to create an instance of a class. As of PHP 8.1, the new operator can be used in arbitrary expressions with the following syntax in which the expression must be wrapped in parentheses. 

new (expression)(...$args)

The new operator can be used in initializers for parameter default values, static variable initializers, global constant initializers, and attribute arguments as explored next with examples.

The new operator in function parameter default values

To demonstrate the use of the new operator in function parameter default values, we shall use a variation of the array sort example. Consider a class called SortArray that declares a method sortArray($arrayToSort) to sort an array in ascending order. 

class SortArray {
    public function sortArray($arrayToSort) {
…
}
}

A second class called ReverseSortArray declares a method to sort an array in descending order.

class ReverseSortArray {
    public function sortArray($arrayToSort) {
…
}
}

Declare now a function that accepts any arbitrary array and an array sorter to sort the array. The default array sorter could be defined using  the new operator to create an instance of the SortArray.

function sortAnyArray($arrayToSort, $arraySorter = new SortArray)
{
    return $arraySorter->sortArray($arrayToSort);
}

The complete script is available on GitHub. Run the sample script on the built-in server with url http://localhost:8000/scripts/sample.php

The output is shown here:

arrayToSort[0] = A arrayToSort[1] = B arrayToSort[2] = C arrayToSort[3] = f
arrayToSort[0] = f arrayToSort[1] = C arrayToSort[2] = B arrayToSort[3] = A

The new operator in variable and constant initializers

The new operator may be used in static variable initializers and global constant initializers. The new operator is not supported, though, in static and non-static class property initializers, nor in class constant initializers. The following script demonstrates which variable and constant initializers are supported, and which aren't. 

<?php

class A{ }

class B{
//static $a = new A; //New expressions are not supported in this context
//public $a = new A; //New expressions are not supported in this context 
//const C = new A; //New expressions are not supported in this context
}
static $a = new A;
 
const C = new A;

The new operator in Attribute Arguments

As you would expect, the new operator can be used in attribute arguments, too. We shall use the same example we used to demonstrate the Attributes feature with one difference. Remember that we used the #[SortType] attribute to annotate the QSort class with the attribute arguments being passed to it as strings.

#[SortType(sortType: "desc")] 
#[SortType(sortType: "shuffle")]  
#[SortType(sortType: "asc")]              
class QSort
{
}

For this case, declare a class called Str that accepts a string argument as a constructor argument.

class Str{
    function __construct($str){
        $this->value = $str;
    }

    function __toString(){
        return $this->value;
    }
     
}

Use the new operator in the attribute arguments as follows.

#[SortType(sortType: new Str("desc"))] 
#[SortType(sortType: new Str("shuffle"))]  
#[SortType(sortType: new Str("asc"))]              
class QSort
{
}

 Run the script in the built-in server with the same output as before, as shown in Figure 3.

Figure 3. The result from using new in attribute arguments

The new operator is not allowed in some contexts

Earlier we mentioned some contexts in which the new operator is not supported, that is static and non-static class property initializers,  and class constant initializers. Additionally, the new operator is not supported in the following contexts:

  • Non-string dynamic class name
  • Argument unpacking in constant expression
  • Anonymous class in constant expression
  • Unsupported constant expression

The following script generates an error for the statements commented out:

<?php
 $list = [4, 6];
function fn1(
    $a1 = new ('some_dynamic_class')(),
   //  $a2 = new (some_dynamic_class)(), // non string dynamic class -  Cannot use dynamic class name in constant expression
   //  $b = new  class {}, // Cannot use anonymous class in constant expression
   // $c = new C(...$list), // Argument unpacking in constant expressions is not supported
   //   $d = new D($x), // Constant expression contains invalid operation
) {}

The new match expression

PHP 8 introduces a new match expression as a control flow structure that matches a given subject expression with one or more alternative branches using identity comparison, and returns a value that is the result from the matched branch. The match expression is similar to a switch statement but different as follows:

  • The match expression uses the identity operator (===) for comparison, whereas switch uses the equality (==) operator.
  • The match expression returns a value, whereas a switch doesn't. The return value may be assigned to a variable. 
  • The match expression breaks automatically out of the match after one of the branches is matched. The switch requires a break; statement.
  • The match expression does not fall-through, as a switch does in the absence of break; statement/s.
  • The match expression supports multiple conditions separated by a comma (,) in the same branch, whereas switch doesn't.
  • The match expression must be exhaustive, which implies that it must handle all values of the subject expression.  

Next, we  demonstrate the use of the match expression with some examples. 

Outputting the Return value

To start off with a simple example, the match expression in the following script matches the integer value of 1, given as the subject expression, with multiple conditional expressions including the default pattern.

<?php

echo match (1) {
    0 => 'A',
    1 => 'B',
    2 => 'C',
    default => 'Default',
};

Run the script in the built-in engine by calling the script in a browser with url

http://localhost:8000/scripts/match.php. The output is:

B

In the following script, the subject expression is not matched by any of the non-default conditional expressions.

<?php

echo match (3) {
    0 => 'A',
    1 => 'B',
    2 => 'C',
    default => 'Default',
};

The output is:

Default

The default condition can't be grouped with other conditions as shown in the script:

<?php

echo match (3) {
    0 => 'A',
    1 => 'B',
    2, default => 'C',
     
};

When the script is run, it generates the error:

Parse error: syntax error, unexpected token "default", expecting "=>"

A relatively complex example, the following script has a variable initialized from the  \Ds\Vector  class. The subject expression accesses the vector variable in array notation. 

<?php

$vector = new \Ds\Vector();

$vector->push('a');
$vector->push('b', 'c');

$vector[] = 'd';
 
echo match ($vector[1]) {
  'a' => "a",
  'b' => "b",
};

Call the script in a browser with url  http://localhost:8000/scripts/match.php

The result is:

b

Assigning the return value to a variable

The return value may also be assigned to a variable, and the match conditions may be any arbitrary expressions. In the following script, the match expression matches the boolean value true as the subject expression with conditional expressions that call other built-in string functions. 

<?php

$text = 'A B C D';

$result = match (true) {
    str_word_count($text)==3 || str_word_count($text)==2 => '2 or 3',
    str_word_count($text)==4 || str_word_count($text)==5 => '4 or 5',
    
};

var_dump($result);

The output is:

string(6) "4 or 5"

No type coercion

Unlike the loose comparison made by a switch statement, the match expression makes a strict comparison and no type coercion is made. First, an example of a loose comparison with a switch statement. The following script matches the first condition and outputs the value of a

<?php
$var = 0;
switch((string)$var) 
{
    case  0  : echo 'a'; break; // This tests for NULL or empty string   
    default : echo 'b'; break; // Everything else, including zero
}

In contrast, the match in the following script makes a strict comparison and matches the default condition to output Default.

<?php
$var = 0;
 
echo match ((string)$var) {
    0 => 'a',
    default => 'Default',
};

Even NULL values are matched using identity comparison

The match expression makes an identity comparison using the identity operator (===), which implies that even NULL values are matched. As the Ds\Vector::push() method returns NULL, the match expression in the following script actually matches the NULL values returned by the Ds\Vector::push() method.  

<?php

$vector = new \Ds\Vector();

$vector->push('a'); 
 
match ($vector->push('b')) {
  $vector->push('d') => $vector->push('b'),
  $vector->pop() => $vector->pop(),
};

print_r($vector);

The first condition gets matched and the output is:

Ds\Vector Object ( [0] => a [1] => b [2] => d [3] => b )

Identity comparison ( === ) can be applied to arbitrary values even to when no value is returned. The NULL value returned by match can be assigned to a variable. The following script matches a NULL value, and the first condition gets matched.

<?php

$vector = new \Ds\Vector();

$vector->push('a'); 
 
$vector = match ($vector->push('b')) {
  $vector->push('c') => $vector->push('b'),
  $vector->pop() => $vector->pop(),
};

var_dump($vector);

Call the script with url http://localhost:8000/scripts/sample.php and the output is:

NULL

Multiple conditions can be comma-separated

Multiple conditional expressions may be comma separated. First, an example with single conditions as in the following script. 

<?php

$vector = new \Ds\Vector();

$vector->push('a'); 
 
match ('push') {
  'push' => $vector->push('b'),
  'pop' => $vector->pop(),
};

print_r($vector);

The output is :

Ds\Vector Object ( [0] => a [1] => b )

Take the same example and add multiple conditions separated by a comma.

<?php

$vector = new \Ds\Vector();

$vector->push('a'); 
 
match ('puush') {
  'push','puush' => $vector->push('b') 
   
};

print_r($vector);

The misspelled ‘puush’ that is listed as the second alternative in the first conditional expression list gets matched and the output is:

Ds\Vector Object ( [0] => a [1] => b )

Exhaustiveness

The match expression must be exhaustive. In the following script  the subject expression is not matched by any of the conditions.

<?php


$vector = new \Ds\Vector();

$vector->push('a'); 
 
match ('set') {
  'push' => $vector->push('b'),
  'pop' => $vector->pop(),
};

print_r($vector);

As a result, an error is output:

Uncaught UnhandledMatchError: Unhandled match case 'set'

The instanceof operator supports arbitrary expression

Another new feature is that the instanceof operator accepts any arbitrary expression. The only two requirements are :

  • The expression must be wrapped in parentheses, and
  • The expression must evaluate to a string. 

To demonstrate the new instanceof operator, create a PHP script sample.php. Declare three variables for collections of different types such as \Ds\Vector, and \Ds\Set.

$collection_a = new \Ds\Vector([1, 2, 3]);
$collection_b = new \Ds\Vector();
$collection_c = new \Ds\Set();

Add an arbitrary function that returns a class, such as \Ds\Vector::class as a string. 

function getSomeClass(): string
{
    return \Ds\Vector::class;
}

Output the results for using the instanceof operator with different collection variables. The instanceof operator takes an expression that evaluates to a string in each call. The sample.php is listed.

<?php

$collection_a = new \Ds\Vector([1, 2, 3]);
$collection_b = new \Ds\Vector();
$collection_c = new \Ds\Set();

function getSomeClass(): string
{
    return \Ds\Vector::class;
}

var_dump($collection_a instanceof ('\Ds'.'\Vector'));
var_dump($collection_a instanceof ('\Ds'.'\Hashtable'));

var_dump($collection_b instanceof ('\Ds'.'\Vector'));

var_dump($collection_b instanceof ('\Ds'.'\Set'));
var_dump($collection_c instanceof ('\Ds'.'\Set'));
var_dump(new \Ds\Vector instanceof (getSomeClass()));
var_dump($collection_a instanceof ('\Ds'.'\Set'));

The script runs ok with PHP 8 and generates output:

bool(true) 
bool(false)
bool(true) 
bool(false) 
bool(true) 
bool(true) 
bool(false) 

JIT(Just-In-Time) compiler

PHP 8 introduces Just-in-time (JIT) compilation with the following goals:

  • Improve performance and usability.
  • Make PHP suitable for non-Web CPU-intensive use cases, where substantial performance benefits can be expected.
  • Create the potential for using PHP, instead of C, to develop built-in functions. PHP could be a better alternative as it does not have the same memory management and overflow issues as C does. 

Two JIT compilation engines are introduced: Tracing JIT, and Function JIT. Function JIT only optimizes code within the scope of a single function, whereas Tracing JIT optimizes the whole stack trace. Tracing JIT is shown to improve performance 3 times on synthetic benchmarks, and 1.5 to 2 times on long-running queries. Tracing JIT is shown to improve performance by more than 4 times on the Mandelbrot benchmark

In this article we have introduced some of the most relevant improvements to PHP 8 syntax, including attributes, the operator new, match expressions, and more. In the next article we shall explore improvements to classes and constructors.

 

This article is part of the article series "PHP 8.x". You can subscribe to receive notifications about new articles in this series via RSS.

PHP continues to be one of the most widely used scripting languages on  the web with 77.3% of all the websites whose server-side programming language is known using it according to w3tech. PHP 8 brings many new features and other improvements, which we shall explore in this article series.

About the Author

Rate this Article

Adoption
Style

BT