Jaké novinky přinese PHP 7.4
Publikováno: 7.10.2019
Vydání PHP 7.4 je plánováno na 28. listopadu 2019 (viz plán). Přináší hromadu zajímavých věcí – Typed Properties, Array Functions, Preload, FFI a pár dalších vylepšení. Sice přibyly i nějaké deprecations, ale typicky se týkají podivného chování, takže upgrade by měl být snadný. Zároveň díky těm novým funkcím a Typed Properties bude mnohem větší motivace pro upgrade.
Pokud jste ještě na starší verzi PHP, tak by vás mohly zajímat články o novinkách v PHP 7.2 a o novinkách v PHP 7.3. Zároveň připomínám, že pokud běžíte na PHP 7.1, tak to přestane dostávat bezpečnostní updaty 1. 12. 2019 a v PHP 7.2 budou od té doby opravovány jen bezpečnostní chyby. Takže byste teď měli běžet minimálně na PHP 7.2 a pomalu chystat upgrade na 7.3.
Pokud se chcete na PHP 7.4 připravit už teď, tak v první fázi doporučuji updatovat knihovny na aktuální verze, protože s blížícím se vydáním PHP 7.4 všichni postupně opravují případné nekompatibility. A potom si můžete zkusit spustit testy a proklikat základní scénáře na PHP 7.4 (už je k dispozici RC2). Kromě toho PhpStorm už některé funkcionality podporuje v posledním release a zbytek přijde v 2019.3 (které je zatím v EAP). PHPStan už PHP 7.4 podporuje od verze 0.11.13 vydané v srpnu.
Typed Properties 2.0 RFC
Typed Properties jsou podle mě největší trhák PHP 7.4, už kvůli nim bude stát za to brzy upgradovat.
Ještě, než se dostaneme k samotnému návrhu, tak jedna zajímavost. Autor návrhu Nikita Popov dříve pracoval na PHP ve volném čase po večerech, ale nedávno ho najmuli JetBrains (kteří stojí za PhpStormem), aby se mohl rozvoji PHP věnovat na fulltime. A je to vidět.
Návrh na přidání Typed Properties do PHP už se jednou v minulosti objevil, ale kvůli nedostatkům v implementaci nebyl schválen. Tentokrát prošel v poměru pro: 70, proti: 1.
Pojďme se podívat trochu do historie. V PHP 7.0 přibyla možnost používat skalární typy při definici parametrů a návratových hodnot funkcí a metod. Díky tomu za nás PHP kontroluje, jestli nepředáváme nebo nevracíme něco jiného, než je definováno:
<?php declare(strict_types = 1);
class Product
{
/** @var string */
private $name;
public function __construct(string $name)
{
$this->name = $name;
}
}
// Uncaught TypeError: Argument 1 passed to Product::__construct() must be of the type string, int given
new Product(123);
Nicméně, pokud uděláme chybu v přiřazení do property, tak se o tom nedozvíme hned, ale třeba až při volání getteru někdy mnohem později (pokud u něj vůbec máme definovaný return type).
<?php declare(strict_types = 1);
class Product
{
/** @var string */
private $name;
/** @var Price */
private $price;
public function __construct(string $name, Price $price)
{
$this->name = $name;
$this->name = $price; // <-- chyba, ale nedozvíme se o ní
}
// ...
}
Samozřejmě to odhalí třeba PHPStan, ale ještě lepší způsob je využít Typed Properties z PHP 7.4 a místo do PHPDoc napsat typy properties přímo do kódu:
<?php declare(strict_types = 1);
class Product
{
private string $name;
private Price $price;
public function __construct(string $name, Price $price)
{
$this->name = $name;
$this->name = $price;
}
// ...
}
// Uncaught TypeError: Typed property Product::$name must be string, Price used
new Product('FooBar', new Price(10));
U property je možné použít jakýkoliv typ (kromě void
a callable
), včetně nullable pomocí ?
. Chování je velmi předvídatelné – pokud zkusíte do property přiřadit neplatnou hodnotu, PHP vyhodí chybu.
Zajímavý je nový stav atributu – uninitialized
, kdy PHP zaručuje, že z atributu si přečteme hodnotu s definovaným typem nebo nic:
<?php declare(strict_types = 1);
class Number
{
public int $value;
}
/*
object(Number)#1 (0) {
["value"]=>
uninitialized(int)
}
*/
var_dump($number);
// Uncaught Error: Typed property Number::$value must not be accessed before initialization
var_dump($number->value);
Typed Properties jsou super možnost, která umožní psaní kratšího a zároveň robustnějšího kódu. A také se díky nim zbavíme zbytečných PHPDoc, kam bylo potřeba typy uvádět teď.
Arrow Functions (nebo také Short Closures) RFC
Anonymní funkce jsou v PHP dost ukecané, zejména pokud se v nich provádí jen něco primitivního. V následujícím příkladu je tou zajímavou logikou jen $n > 3
:
<?php
$numbers = [1, 2, 3, 4, 5, 6];
$filtered = array_filter($numbers, function ($n) {
return $n > 3;
});
// [4, 5, 6]
Arrow functions přinášejí nové klíčové slovo fn
a možnost zkráceného zápisu. Ten příklad výše bude vypadat takto:
<?php
$filtered = array_filter($numbers, fn($n) => $n > 3);
A samozřejmě je možné použít i typy parametrů a návratových hodnot:
<?php
$filtered = array_filter($numbers, fn(int $n): bool => $n > 3);
Na rozdíl od běžných anonymních funkcí, kde je potřeba proměnné z vnějšího scope explicitně uvést v use
, jsou v short closures automaticky přístupné všechny (ale není možné je předat referencí a měnit):
<?php
$allowedValues = ['a', 'c'];
$inputValues = ['a', 'b', 'c', 'd'];
// standardní zápis s ručním bindováním:
$onlyAllowed = array_filter($inputValues, function ($value) use ($allowedValues) {
return in_array($value, $allowedValues);
});
// short closure, bez explicitního bindování:
$onlyAllowed = array_filter($inputValues, fn ($value) => in_array($value, $allowedValues));
// ['a', 'c']
Je to super způsob na zpřehlednění některých míst v kódu, ale na tu syntaxi si budu chvíli muset zvykat. Zejména v kombinaci s typy parametrů a návratových hodnot to už může být trochu nepřehledné. Osobně to mám zařazené podobně jako ternární operátor – někdy to čitelnosti pomůže, ale je potřeba dávat pozor, abychom kód v honbě za pár ušetřenými znaky zbytečně neznepřehlednili.
Pozor:fn
je nové klíčové slovo, takže mrkněte do svých aplikací, jestli ho nepoužíváte jako název metody, třídy nebo namespace.
Preloading RFC
Preloading rozšiřuje možnosti OPcache o možnost definování preload skriptu v php.ini
v opcache.preload
. Ten bude spuštěn při startu serveru a bude moci pomocí volání opcache_compile_file()
načíst a zkompilovat třídy do sdílené paměti serveru. V jednotlivých requestech ty třídy pak budou přístupné rovnou, bez potřeby volání require_once
nebo autoloadingu.
Oproti standardní OPcache, která pracuje jen na úrovni souborů, umí preloading jednotlivé třídy propojit (třeba v případě traitů) a je tedy výkonnější. Kromě zvýšení rychlosti také sníží množství paměti potřebné pro jednotlivé requesty, protože místo vlastní kompilace tříd request rovnou použije ty již přístupné ve sdílené paměti.
Jak jsem zmínil výše, preload skript se spouští při startu serveru, takže v případě změn v již načtených souborech je potřeba server restartovat. Zavolání opcache_reset()
nebo reload FPM nestačí, je potřeba opravdový FPM restart. Kromě toho nedává smysl preload používat pro servery, kde poběží více různých aplikací nebo více různých verzí stejné aplikace.
Uvidíme, jak se preload osvědčí v praxi. Potřeba restartu PHP serveru použití docela komplikuje. Každopádně už se chystá automatické generování preload skriptu v Composeru a Symfony 4.4 bude také umět generovat preload skript.
Spread Operator in Array Expression RFC
Od PHP 5.6 bylo možné používat argument unpacking (spread operator) v definici nebo volání funkce:
<?php
function test(...$args) { var_dump($args); }
test(1, 2, 3); // [1, 2, 3]
test(...[1, 2, 3]); // [1, 2, 3]
Od PHP 7.4 je možné to stejné dělat při práci s poli:
<?php
$parts = ['apple', 'pear'];
$fruits = ['banana', 'orange', ...$parts, 'watermelon'];
// ['banana', 'orange', 'apple', 'pear', 'watermelon'];
Pozor: spread operator je možné použít jen pro pole s numerickými indexy (pokus použít ho pro pole se stringovými vrátí chybu).
FFI – Foreign Function Interface RFC
FFI umožňuje psát PHP kód, který volá funkce a datové struktury z C. Teoreticky by tedy mělo být možné napsat extension pro PHP přímo v PHP.
Jako ukázku připravil autor RFC Dmitry Stogov ukázkovou integraci TensorFlow knihovny. Kouzla začínají tady.
New custom object serialization mechanism RFC
Vzhledem k problémům existujících serializačních mechanismů (__sleep()
+__wakeup()
a rozhraní Serializable
) přibyla do PHP nová možnost ovlivnění serializace objektů – magické metody __serialize(): array
a __unserialize(array $data): void
.
Použití vypadá takto – definujete způsob, kterým se převodou property na vlastnosti serializované objektu a při deserializaci hodnoty z pole nastavíte do properties:
<?php declare(strict_types = 1);
class DemoClass
{
private string $prop1 = 'aaa';
public function __serialize(): array
{
return [
'myprop1' => $this->prop1,
];
}
public function __unserialize(array $data): void
{
$this->prop1 = $data['myprop1'];
}
}
$a = new DemoClass();
/*
object(DemoClass)#1 (1) {
["prop1":"DemoClass":private]=>
string(3) "aaa"
}
*/
// "O:9:"DemoClass":1:{s:7:"myprop1";s:3:"aaa";}"
$serializedString = serialize($a);
$restoredObject = unserialize($serializedString);
/*
object(DemoClass)#2 (1) {
["prop1":"DemoClass":private]=>
string(3) "aaa"
}
*/
Covariant Returns and Contravariant Parameters RFC
PHP teď umožňuje v potomkovi odebrat typ parametru (tzn. akceptovat vše co parent a něco navíc) nebo přidat return type (vracet podmnožinu toho, co parent).
<?php
interface ParentTyp {
function foo(PotomekTyp $z);
}
interface PotomekTyp extends ParentTyp {
function foo($z): ParentTyp; // OK
}
Nicméně nebylo možné použít volnější typ jako parametr a striktnější typ jako návratovou hodnotu. Následující kód by hodil Fatal error: Declaration of PotomekTyp::foo(ParentTyp $z): PotomekTyp must be compatible with ParentTyp::foo(PotomekTyp $z): ParentTyp
. Přitom to je typově bezpečné a neporušuje LSP.
<?php
interface ParentTyp {
function foo(PotomekTyp $z): ParentTyp;
}
interface PotomekTyp extends ParentTyp {
function foo(ParentTyp $z): PotomekTyp; // v PHP <7.4: Fatal Error
}
Od PHP 7.4 to je toto použití možné.
Null Coalescing Assignment Operator RFC
Použití operátoru ??
pro případné nastavení výchozí hodnoty je již k dispozici nějakou dobu:
<?php
$username = $_GET['user'] ?? 'nobody';
Nicméně, pokud byl název proměnné delší než $username
a zároveň se měla měnit její hodnota, byl zápis docela nepřehledný s dlouhým opakujícím se kusem kódu:
<?php
$this->request->data['comments']['user_id'] = $this->request->data['comments']['user_id'] ?? 'value';
Pomocí nového operátoru ??=
je možné předchozí zápis zkrátit na:
<?php
$this->request->data['comments']['user_id'] ??= 'value';
V případě, že vlevo bude hodnota null
, bude přepsána hodnotou vpravo.
Allow throwing exceptions from __toString() RFC
Pokud jste teď v metodě __toString()
vyhodili Exception, tak jste se dozvěděli, že __toString()
výjimku vyhazovat nesmí. Od PHP 7.4 je toto umělé omezení zrušené a vyhozená výjimka se bude chovat podle očekávání.
<?php
class Foo
{
public function __toString(){
throw new Exception('Hello!');
}
}
// Dříve: Fatal error: Method Foo::__toString() must not throw an exception, caught Exception: Hello!
// Od PHP 7.4: Fatal error: Uncaught Exception: Hello!
echo (new DemoClass());
Deprecate curly brace syntax for accessing array elements and string offsets RFC
Kromě běžně používaných [ ]
bylo možné k prvkům pole a bytům ve stringu přistupovat pomocí { }
:
<?php
$array = [1, 2];
echo $array[1]; // 2
echo $array{1}; // 2
$string = 'foo';
echo $string[0]; // 'f'
echo $string{0}; // 'f'
Přístup pomocí složených závorek není možné použít pro všechny situace jako hranaté závorky (třeba přidání prvku do pole) a zároveň je jejich použití málo běžné, takže je tato možnost od PHP 7.4 deprecated a v budoucnosti bude zrušena.
E_WARNING for invalid container read array-access RFC
Pokud jste teď omylem přistupovali jako k poli k něčemu, co není pole, dostali jste NULL
a nijak jste se to nedozvěděli.
<?php
$a = false;
// NULL
var_dump($a[0]);
Nově to bude vyhazovat Notice. Možná jste si všimli, že podle názvu RFC by to mělo házet Warning. Sice ho tak nejdříve schválili, ale pak se nemohli domluvit, jak by se měly chovat nějaké edge-cases při použití list()
, tak se implementace zadrhla. Nakonec Nikita připravil zjednodušenou variantu, která hází Notice na těch místech, kde panovala shoda.
<?php
$a = false;
// Notice: Trying to access array offset on value of type bool
// NULL
var_dump($a[0]);
Numeric Literal Separator RFC
Drobnost, která umožní v číslech používat podtržítka pro zvýšení čitelnosti. Důvodem je, že pokud je někde v kódu uvedeno 1000000000
, tak není na první pohled jasné, kolik tam těch nul je. Pokud je to zapsané jako 1_000_000_000
, tak to na první pohled jasné je.
Podtržítka nemají žádný vliv na zpracování kódu, lexer je odebere při zpracování kódu. Oddělovače je možné používat u jakýchkoliv čísel, nejen těch v desítkové soustavě.
Deprecations for PHP 7.4 RFC
V PHP 7.4 opět přibylo pár věcí označených jako deprecated. Typicky jde o podivnosti, o kterých jste ani netušili, že existují.
Ty nejzajímavější:
- datový typ
real
(byl to jen alias profloat
) - použití
array_key_exists()
pro zjištění existence property objektu - volání
implode()
s prohozenými parametryimplode($pieces, $glue)
- funkce
money_format()
– mělo by se používatNumberFormatter::formatCurrency()
Change the precedence of the concatenation operator RFC
Objeví se drobná změna týkající se neuzávorkovaných výrazů, kde je nejdříve spojování řetězců a teprve pak sčítání nebo odečítání. Teď se vyhodnocují zleva, což je typicky chyba:
<?php
// takto zapsaný výraz
echo 'sum: ' . $a + $b;
// se vyhodnocuje jako:
echo ('sum: ' . $a) + $b;
// větší smysl by dávalo vyhodnocovat takto:
echo 'sum :' . ($a + $b);
Od PHP 7.4 bude házet deprecations a v 8.0 se chování změní.
Uvádím to tu hlavně kvůli zajímavosti z RFC procesu – dost často se teď rozhodují podle analýzy knihoven na Githubu (a případného dopadu na ně). Nikita Popov analyzoval 2000 nejpopulárnějších knihoven a našel pět výskytů toho problematického výrazu. A ve všech to byl bug a ne zamýšlené chování :-)
Závěrem
PHP 7.4 přináší hromadu nových možností, pro mě jsou nejzajímavější ty Typed Properties, ale ani ty ostatní nejsou k zahození. Takže se pomalu můžete připravovat na upgrade – minimálně stojí za to se postupně zbavit případných deprecated věcí, které používáte.
Další verze PHP bude pravděpodobně PHP 8.0, na které se již pracuje. Nejzajímavější zatím diskutovaná věc jsou Union Types (tedy možnost definovat typy jako int|float
, což se už teď používá v PHPDoc). A to jsme teprve na začátku vývojového cyklu, takže se máme na co těšit.