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:
To examine and run the code examples provided in this chapter, the minimum recommended hardware is listed here:
In addition, you will need to install the following software:
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:
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:
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.
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
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!
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
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
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
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.
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.
The syntax needed to invoke constructor property promotion is identical to that used in PHP 7 and earlier, with the following differences:
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.
In a conventional OOP PHP class, the following three things need to be done:
/repo/src/Php8/Image/SingleChar.php
namespace Php7Image;
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;
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)
{ $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:
Also, the equivalent PHP 8 class is found here:
For more information on this new feature, have a look at the following:
https://wiki.php.net/rfc/constructor_promotion
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.
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.
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 Php7Entity;
use DoctrineORMMapping as ORM;
/**
* @ORMTable(name="events")
* @ORMEntity("ApplicationEntityEvents")
*/
class Events {
/**
* @ORMColumn(name="id",type="integer",nullable=false)
* @ORMId
* @ORMGeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @ORMColumn(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.
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.
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.
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 Php7Image;
/**
* 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 Php8Image;
#[description("Creates a single image")]
class SingleChar {
#[SingleCharcolorAllocdescription("Allocates color")]
#[SingleCharcolorAllocparam("r","int|array")]
#[SingleCharcolorAllocparam("g","int")]
#[SingleCharcolorAllocparam("b","int")]
#[SingleCharcolorAlloc eturns("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.
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 Php8ImageSingleChar::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 ServerAutoloadLoader();
use Php8ImageSingleChar;
$char = new SingleChar('A', FONT_FILE);
$reflect = new ReflectionObject($char);
$attribs = $reflect->getAttributes();
echo "Class Attributes ";
foreach ($attribs as $obj) {
echo " " . $obj->getName() . " ";
echo implode(" ", $obj->getArguments());
}
echo "Method Attributes for colorAlloc() ";
$reflect = new ReflectionMethod($char, 'colorAlloc');
$attribs = $reflect->getAttributes();
foreach ($attribs as $obj) {
echo " " . $obj->getName() . " ";
echo implode(" ", $obj->getArguments());
}
Here is the output from the code shown in the preceding snippet:
<pre>Class Attributes
Php8ImageSingleChar
Php8Imagedescription
Creates a single image, by default black on whiteMethod
Attributes for colorAlloc()
Php8ImageSingleCharcolorAllocdescription
Allocates a color resource
Php8ImageSingleCharcolorAllocparam
r int|array
Php8ImageSingleCharcolorAllocparam
g int
Php8ImageSingleCharcolorAllocparam
b int
Php8ImageSingleCharcolorAlloc eturns
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 Php8Image;use Attribute;
use Php8ImageStrategy {PlainText,PlainFill};
#[SingleChar]
#[description("Creates black on white image")]
class SingleChar {
// not all code is shown
#[SingleCharcolorAllocdescription("Allocates color")]
#[SingleCharcolorAllocparam("r","int|array")]
#[SingleCharcolorAllocparam("g","int")]
#[SingleCharcolorAllocparam("b","int")]
#[SingleCharcolorAlloc eturns("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:
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
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 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:
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:
Other than the differences noted, match and switch both allow case aggregation, as well as providing support for a default case.
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) . " ";
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) . " ";
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.
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:
After randomizing the list of strategies to be employed, we use a match expression to execute the results, as follows:
// /repo/ch01/php8_single_strategies.php
// not all code is shown
require_once __DIR__ . '/../src/Server/Autoload/Loader.php';
$loader = new ServerAutoloadLoader();
use Php8ImageSingleChar;
use Php8ImageStrategy {LineFill,DotFill,Shadow,RotateText};
$strategies = ['rotate', 'line', 'line',
'dot', 'dot', 'shadow'];
$phrase = strtoupper(bin2hex(random_bytes(NUM_BYTES)));
$length = strlen($phrase);
$images = [];
for ($x = 0; $x < $length; $x++) {
$char = new SingleChar($phrase[$x], FONT_FILE);
$char->writeFill();
shuffle($strategies);
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);
}
$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:
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
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.
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.
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.
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
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).
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.
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:
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
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 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.
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.
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:
// /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);
}
}
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.
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:class@anonymous/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.
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.
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!
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!
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:
// /repo/ch01/php7_prop_reduce.php
declare(strict_types=1);
class Test {
protected $id = 0;
protected $token = 0;
protected $name = '';o
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;
}
}
$test = new Test();
$test->setId(111);
$test->setToken(999999);
$test->setName('Fred');
$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:
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.
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.
3.236.100.210