PHP

Mainly contains new language features introduced since php 7.
TODO https://www.adimeo.com/blog/forum-php-2019-clean-architecture

Quick reminder

in_array(mixed $findme, array $array, bool $strict = false): bool
strpos(string $string, string $findme, int $offset = 0): int|false
substr(string $string, int $offset, ?int $length = null): string

str_replace(array|string $search, array|string $replace, string|array $subject, int &$count = null): string|array
str_contains(string $haystack, string $needle)
str_starts_with(string $haystack, string $needle): bool
str_ends_with(string $haystack, string $needle): bool

<?= 'hello here' ?>

$str = <<<TEXT
My heredoc text, with variable substitution
TEXT;

$str = <<<'TEXT'
My nowdoc text, without variable substitution
TEXT;


Install, manage

sudo apt update
sudo apt install lsb-release ca-certificates apt-transport-https software-properties-common -y
sudo add-apt-repository ppa:ondrej/php

sudo apt install php8.5 php8.5-{apcu,bcmath,calendar,curl,fpm,gd,gmp,intl,mbstring,mysql,sqlite3,xdebug,xml,yaml,zip}

apt search php8.5 | grep bz

Multiple versions

https://www.linuxbabe.com/ubuntu/php-multiple-versions-ubuntu
sudo update-alternatives --config php
# check
php -v

Run

php -r  --run           code            # Run PHP code without using script tags '<?..?>'
                                        # php -r "echo 'toto';"
php -S  --server        addr:port       # Start built-in web server on the given local address and port
        [-t docroot]
        [-c /path/to/php.ini]
        [-c "short_open_tag=On"]
php -a  --interactive                   # since 8.1 ; like like jshell, ipython
php -l  --syntax-check                  # Syntax check only (lint)
php -T  --timing        count           # Measure execution time of script repeated count times (CGI only)

php -m  --modules                       # Show compiled in modules
php -i  --info                          # PHP information and configuration
php --ini                               # Show configuration file names
php -c  --php-ini       path|file       # Look for php.ini file in the directory path or use the specified file
php -n  --no-php-ini                    # No php.ini file will be used
php -d  --define        foo[=bar]       # Define INI entry foo with value bar

sudo phpenmod -s cli xdebug
sudo phpdismod -s fpm -v 8.2 xdebug
    # -s cli|fpm|apache2
    # -v not specified => all installed versions

Types

https://www.php.net/manual/en/language.types.php
declare(strict_types=1);    # per-file declaration - applies to scalars, function parameters and return.

PHP built-in types:
    null
Scalar types:
    bool        php 8.2
    int
    float       (floating-point number, aka double)
    string
Compound types:
    array
    object      php 7.2
    iterable    PHP 7.1     # pseudo-type
Relative class types
    self                    # return object of the current class
    static                  # return object of the current class - but late static binding
    parent                  # return object of parent of the current class
Value types:
    true        php 8.2
    false       php 8.2
Special types:
    resource
    void        php 7.1
    never       php 8.1     # must exit, throw exception or enter in infinite loop
    mixed       php 8.0     # accepts every value ; equivalent to the union type object|resource|array|string|float|int|bool|null
Callable type:
    callable
     
Nullable type   php 7.1
function testReturn(): ?string  # nullable - equivalent to string|null

|   union
&   intersection

Type casts

(int), (integer) - cast to int
(bool), (boolean) - cast to bool
(float), (double), (real) - cast to float
(string) - cast to string
(array) - cast to array
(object) - cast to object 

Disjunctive Normal Form (DNF) Types - 8.2

BEFORE
class Foo {
    public function bar(mixed $entity) {
        if ((($entity instanceof A) && ($entity instanceof B)) || ($entity === null)) {
            return $entity;
        }
        throw new Exception('Invalid entity');
    }
}
SINCE 8.2
class Foo {
    public function bar((A&B)|null $entity) {
        return $entity;
    }
}

nullable - 7.1

// types can be marked as nullable by prefixing the type name with a question mark (?).
// This signifies that the function returns either the specified type or NULL. 
function get_item(): ?string {
    if (isset($_GET['item'])) {
        return $_GET['item'];
    } else {
        return null;
    }
}

Functions

Typed arguments and typed return

class C {}
function sum(array &$params, C $c, DateInterval ...$intervals): float {
    $p = $params[0];
}

named arguments - 8.0.0

$tmp = myFunction(paramName: $paramName);

Argument promotion - 8.0.0

When a constructor argument includes a modifier, PHP will interpret it as both an object property and a constructor argument, and assign the argument value to the property.
Any additional statements will be executed after the argument values have been assigned to the corresponding properties.
// since 8.0
class Point {
    public function __construct(protected int $x, protected int $y = 0) {
    }
}
// before 8.0
class Point {
    protected int $x;
    protected int $y;
    public function __construct(int $x, int $y = 0) {
        $this->x = $x;
        $this->y = $y;
    }
}

return

If the return is omitted the value NULL will be returned.

Returning a reference from a function
function &returns_reference(){
    return $someref;
}
$newref =& returns_reference();

arrow functions - 7.4

fn(argument_list) => expr.
Arrow functions support the same features as anonymous functions,
except that using variables from the parent scope is always automatic. 

$y = 1;
$fn1 = fn($x) => $x + $y;

// equivalent to using $y by value:
$fn2 = function ($x) use ($y) {
    return $x + $y;
};
var_export($fn1(3)); // 4

Closure::call() - 7

A more performant, shorthand way of temporarily binding an object scope to a closure and invoking it.
class A {private $x = 1;}

// Pre PHP 7 code
$getX = function() {return $this->x;};
$getXCB = $getX->bindTo(new A, 'A'); // intermediate closure
echo $getXCB();

// PHP 7+ code
$getX = function() {return $this->x;};
echo $getX->call(new A);

short ternary operator - 5.3

$result = $initial ? $initial : 'default';
// equivalent to
$result = $initial ?: 'default';

// since 7.4 parentheses mandatory for nested ternary - because ternary expressions are left-associative (counter-intuitive)
$res = $firstCondition ? 'truth' : ($elseCondition ? 'elseTrue' : 'elseFalse');

// but chained short ternary are allowed (because intuitive)
$res = 0 ?: 1 ?: 2 ?: 3; // $res = 1
$res = 0 ?: 0 ?: 2 ?: 3; // $res = 2
$res = 0 ?: 0 ?: 0 ?: 3; // $res = 3

null coalescing - 7.0, 7.4

// same as ternary but boolean test replaced by isset
$username = isset($_GET['user']) ? $_GET['user'] : 'nobody';
// now equivalent to:
$username = $_GET['user'] ?? 'nobody';

// Coalescing can be chained: this will return the first defined value out of $_GET['user'], $_POST['user'], and 'nobody'.
$username = $_GET['user'] ?? $_POST['user'] ?? 'nobody';

// null coalescing assignment operator - 7.4
$params['property'] = $params['property'] ?? 'default';
// equivalent to
$params['property'] ??= 'default'; // if(!isset($params['property'])) $params['property'] = 'default';

spaceship operator - 7.0

// used for comparing two expressions.
// It returns -1, 0 or 1 when $a is respectively less than, equal to, or greater than $b
// works with integer, float, string
echo 1 <=> 1; // 0
echo 1.5 <=> 2.5; // -1
echo "b" <=> "a"; // 1
[[1, 2], [2, 2]] <=> [[1, 2], [1, 2]]; // 1

usort($array, function ($a, $b) { return $a <=> $b; });
usort($array, function ($a, $b) { return -($a <=> $b); }); // descending sort

// Built-in PHP classes can define their own comparison
$dateA = DateTime::createFromFormat('Y-m-d', '2000-02-01');
$dateB = DateTime::createFromFormat('Y-m-d', '2000-01-01');
$dateA <=> $dateB; // Returns 1

match - 8.0

switch ($statusCode) {
    case 200:
    case 300:
        $message = null;
        break;
    case 400:
        $message = 'not found';
        break;
    case 500:
        $message = 'server error';
        break;
    default:
        $message = 'unknown status code';
        break;
}
// now equivalent to
$message = match ($statusCode) {
    200, 300 => null,                       // grouping several conditions
    400 => 'not found',
    500 => throw new ServerError(),         // 
    default => 'unknown status code',       // default keyword
};

// - match doesn't require a break statement
// - match can combine different arms into one using a comma
// - match returns a value, so you only have to assign value once
// - match will do strict type checks instead of loose ones (like using === instead of ==).
// - if no default specified, a value not matching will throw a UnhandledMatchError exception
// - only single line expression for the moment.

Arrays

array unpacking, +, merge - 7.4, 8.1

array spread operator ...
[...$array_1, ...$array_2, ...['quux']] = array_merge($array_1, $array_2, ['quux'])
Since 8.1, ... also supports associative arrays

Array unpacking overwrites duplicate string keys, while the + operator ignores duplicate string keys.
var_dump( ['a' => 'foo'] + ['a' => 'bar'] );        // ['a' => 'foo'];
var_dump([ ...['a' => 'foo'], ...['a' => 'bar'] ]); // ['a' => 'bar'];

// Warning + is a union by keys - use array_merge() for a union by values
$a = ['one', 'two'];
$b = ['three', 'four', 'five'];
$a + $b             = [ 0 => one, 1 => two, 2 => five ]
array_merge($a, $b) = [ 0 => one, 1 => two, 2 => 'three', 3 => 'four', 4 => five ]

array_is_list() - 8.1

array_is_list($a) returns true if $a is empty or $a is a regular array with index starting at 0

array destructuring

$array = [1, 2, 3]; 
[$a, $b, $c] = $array;          // shorthand for : list($a, $b, $c) = $array;
                                // $a = 1; $b = 2; $c = 3;
[, , $c] = $array;              // $c = 3;
WARNING: list() starts at index 0
$array = [1 => 'a', 2 => 'b', 3 => 'c'];
list($a, $b, $c) = $array;      // will give: $a = null; $b = 1; $c = 2;

Since 7.1 $array can have non numerical keys
$array = ['a' => 1, 'b' => 2, 'c' => 3];
['c' => $c, 'a' => $a] = $array;
// ex of convenient usage:
['basename' => $file, 'dirname' => $dir] = pathinfo('/path/to/file');

// usage in loops
$array = [ ['name' => 'a', 'id' => 1], ['name' => 'b', 'id' => 2 ] ];
foreach ($array as ['id' => $id, 'name' => $name]) { ... }

Constant arrays using define()

Array constants can now be defined with define(). In PHP 5.6, they could only be defined with const

Group use declarations

use some\namespace\{ClassA, ClassB, ClassC as C};
use function some\namespace\{fn_a, fn_b, fn_c};
use const some\namespace\{ConstA, ConstB, ConstC};

Unicode codepoint escape syntax

TODO

Expectations

TODO A backwards compatible enhancement to the older assert() function. They allow for zero-cost assertions in production code, and provide the ability to throw custom exceptions when the assertion fails.

Attributes - 8

Attributes offer the ability to add structured, machine-readable metadata information on declarations in code: Classes, methods, functions, parameters, properties and class constants can be the target of an attribute.
// a.php
namespace MyExample;
use Attribute;
#[Attribute]
class MyAttribute {
    const VALUE = 'value';
    private $value;
    public function __construct($value = null) {
        $this->value = $value;
    }
}

// b.php
namespace Another;
use MyExample\MyAttribute;
#[MyAttribute]
#[\MyExample\MyAttribute]
#[MyAttribute(1234)]
#[MyAttribute(value: 1234)]
#[MyAttribute(MyAttribute::VALUE)]
#[MyAttribute(array("key" => "value"))]
#[MyAttribute(100 + 200)]
class Thing {
}

#[MyAttribute(1234), MyAttribute(5678)]
class AnotherThing {
}

// Built-in attributes
class Foo implements ArrayAccess {
    #[\ReturnTypeWillChange]
    public function offsetGet(mixed $offset) {}
    // ...
}

// Symfony example of the definition of an attribute
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class AsEventListener
{
    public function __construct(
        public ?string $event = null,
        public ?string $method = null,
        public int $priority = 0,
        public ?string $dispatcher = null,
    ) {
    }
}

    #[\Deprecated(
        message: "use PhpVersion::getVersion() instead",
        since: "8.4",
    )]
    
Possible targets:
Attribute::TARGET_CLASS
Attribute::TARGET_FUNCTION
Attribute::TARGET_METHOD
Attribute::TARGET_PROPERTY
Attribute::TARGET_CLASS_CONSTANT
Attribute::TARGET_PARAMETER
Attribute::TARGET_ALL

Exceptions

Non-capturing exceptions - 8.0

try {
    changeImportantData();
} catch (PermissionException) { // The intention is clear: exception details are irrelevant
    echo "You don't have permission to do this";
}

Predefined exceptions

OOP

Magic methods

__construct()
__destruct()
__call()
__callStatic()
__get()
__set()
__isset()
__unset()
__sleep()
__wakeup()
__serialize()
__unserialize()
__toString()
__invoke()          # called when a script tries to call an object as a function
__set_state()
__clone()
__debugInfo()

nullsafe operator ?-> - 8.0

Returns null instead of throwing an exception
$result = $repository?->getUser(5)?->name;
 // Is equivalent to the following code block:
if (is_null($repository)) {
    $result = null;
} else {
    $user = $repository->getUser(5);
    if (is_null($user)) {
        $result = null;
    } else {
        $result = $user->name;
    }
}

types - class variables - 7.4

class User{
    public int $id;
    public ?string $name;   // nullable
}

Constructor property promotion - 8.0

- prefix the constructor parameters with public, protected or private => replaces the declaration of class members
- untyped properties are allowed
- allowed in traits ; not allowed in abstract classes
- variadic parameters can't be promoted (ex : public string ...$a )
class CustomerDTO {
    public function __construct(
        /** Doc comments can go here => same as for regular properties */
        public string $name = 'toto',           // default value
        #[MyAttribute]                          // attributes also possible here
        public DateTimeImmutable $birth_date,   // expressions not allowed ; " = new DateTimeImmutable()" is illegal
    ) {
        // in constructor body, promoted properties can be accessed
    }
}

Readonly properties - 8.1

- Readonly properties do not change after an object is constructed
- Can be normal properties or promoted in constructor
- Readonly properties can only be used in combination with typed properties
- Normal readonly properties can not have a default value - but possible if promoted in constructor
- Unset not allowed on readonly properties
- ReflectionProperty::isReadOnly() and ReflectionProperty::IS_READONLY
class BlogData {
    public readonly string $description,
    public function __construct(
        public readonly string $title,
        public readonly ?DateTimeImmutable $publishedAt = null,
    ) {}
}

Readonly classes - 8.2

- A class where all readonly properties are readonly
readonly class BlogData {
    public function __construct(
        public string $title,
        public ?DateTimeImmutable $publishedAt = null,
    ) {}
}

enum - 8.1

enum Suit{
    case Hearts;
    case Diamonds;
    case Clubs;
    case Spades;
}
function pick_a_card(Suit $suit) { ... }
$val = Suit::Diamonds;                              
pick_a_card($val);
pick_a_card(Suit::Clubs);

// Backed enum ; backed by int or string
enum Suit: string {
    case Hearts = 'H';
    case Diamonds = 'D';
    case Clubs = 'C';
    case Spades = 'S';
}

To get all scalar equivalents values of Backed Enum as an array you could define a method in your Enum:
public static function values(): array {
   return array_column(self::cases(), 'value');
}

Anonymous classes

TODO

Late static binding

TODO

Session

session_start()
=> a session id is generated and is sent to the browser as a cookie named PHPSESSID
If cookies not allowed, php uses the url to keep trace of the session id ; cookie is favoured.
Session data are available through $_SESSION
session_id()                # permits to retrieve the session id
session_set_save_handler()  # implement custom session handler
session_write_close()       # if the session handler is "file", unlock the session file (eg useful for concurrent acces via ajax)

DOM parser

$dom = new DOMDocument();
@$dom->loadHTML($text);
foreach ($dom->getElementsByTagName('h2') as $h2) {
    echo "Section: " . $h2->textContent . "\n";
    $content = '';
    for ($node = $h2->nextSibling; $node; $node = $node->nextSibling) {
        if ($node->nodeName === 'h2') break;
        $content .= $dom->saveHTML($node);
    }
    echo "Content:\n$content\n";
}

Composer

composer require flowjs/flow-php-server
composer install
composer update
composer update monolog/monolog
composer audit

composer.json
{
    "require": {
        "monolog/monolog": "2.0.*"
    }
}

Package version constraints:
"6.2.*"
">=8.1"
"^2"                # from version 2 up to next major version 
"^2.12|^3.0"

# In composer.json :
{
    "autoload": {
        "psr-4": {"Acme\\": "src/"}
    }
}
# then run :
composer dump-autoload

composer self-update --2.2

# install new packages without modifying composer.lock:
composer install

PHP Docker images

<PHP version>-<variant>-<OS>

<PHP version> is the semantic version number corresponding to the PHP release shipped with that particular Docker image; it is formatted as MAJOR.MINOR.PATCH.

<variant>
    apache      supplies a PHP interpreter compiled as a shared Apache 2.0 module, bundled together with a complete installation of the Apache 2.0 web server.
    cli         supplies a PHP interpreter compiled for command-line usage (e.g., for executing PHP scripts from the terminal).
    fpm         supplies a PHP interpreter compiled as a FastCGI processor (e.g., for integrating with third-party web servers supporting the FastCGI protocol).
    zts         is almost the same as cli but with Zend Thread Safety enabled in the PHP interpreter (hence zts), allowing users to run multithreaded applications that rely on extensions such as pthreads (now deprecated) or parallel.

docker pull php:8.4.7-alpine3.21

phpunit

Without phpunit.xml
# execute all tests
phpunit --bootstrap autoload.php .
# execute one test class
phpunit --bootstrap autoload.php time/HHMM2minutesTest.php
With phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    bootstrap="autoload.php"
    displayDetailsOnTestsThatTriggerDeprecations="true"
>
</phpunit>
# execute all tests
phpunit .
# execute one test class
phpunit time/HHMM2minutesTest.php

php-cs-fixer

composer require --dev friendsofphp/php-cs-fixer