Jaké novinky přinese PHP 7.2

Publikováno: 19.9.2017

Vydání PHP 7.2 je plánováno na 30. listopadu 2017. Přinese dvě nové bezpečnostní funkcionality, několik menších vylepšení a také trochu vyčistí nepořádek ze starých verzí PHP.

Celý článek

Článek jsem původně vydal v angličtině na svém blogu.

PHP 7.2 vyjde 30. listopadu 2017 (viz plán). Přinese dvě velké bezpečnostní funkcionality, několik menších vylepšení a trochu pročistí historický nepořádek v jazyce. Přečetl jsem RFCčka, diskuze v internals a PullRequesty na Githubu, takže vy už nemusíte.

Testovací verzi je možné zkoušet už teď, stačí si ji stáhnout pomocí odkazů v PHP 7.2.0 Release Candidate 2 Released. Používám ji pro vývoj lokálně už od verze 7.2.0-beta1 a zatím jsem nenarazil na žádný problém.

Funkce password_hash byla k dispozici už od PHP 5.5. Od začátku byla navržena pro použití s různými algoritmy pro hashování hesel, ale zatím byl k dispozici jen jeden, PASSWORD_BCRYPT.

DŮLEŽITÉ: Pokud na hashování hesel používáte sha1() nebo md5(), přestaňte prosím číst článek a běžte opravit svoji aplikaci tak, aby používala password_hash(). Doporučuji článek Michala Špačka Změna hashování existujících hesel.

PHP 7.2 umožňuje použít algoritmus Argon2i pomocí konstanty PASSWORD_ARGON2I:

password_hash('password', PASSWORD_ARGON2I)

Použití PASSWORD_BCRYPT je stále úplně bezpečné. Argon2i je jen další možnost, která možná někdy v budoucnu nahradí bcrypt jako výchozí volbu.

Náročnost výpočtu hashe Argon2i lze ovlivnit nastavením následujících parametrů (podobně jako pro bcrypt můžeme nastavit vlastní cost):

$options = [
    'memory_cost' => PASSWORD_ARGON2_DEFAULT_MEMORY_COST,
    'time_cost' => PASSWORD_ARGON2_DEFAULT_TIME_COST,
    'threads' => PASSWORD_ARGON2_DEFAULT_THREADS,
];
password_hash('password', PASSWORD_ARGON2I, $options);

Vhodné nastavení náročnosti pro Argon2i zjistíte experimentováním na cílovém serveru, kde pak aplikace poběží (což je to stejné, co byste měli dělat při nastavení cost v bcryptu).

Teď je ta vhodná chvíle pro kontrolu délky sloupce pro ukládání hashe hesla. PASSWORD_BCRYPT totiž generuje hashe dlouhé 60 znaků, ale hash PASSWORD_ARGON2I je dlouhý 96 znaků. Dokumentace password_hash doporučuje zvolit jako délku sloupce 255 znaků, aby se do něj vešel jakýkoliv další hash v budoucnu (což je důležité zejména, pokud máte jako algoritmus nastavený PASSWORD_DEFAULT):

It is recommended to store the result in a database column that can expand beyond 60 characters (255 characters would be a good choice).

Na blogu Zend Framework vyšel o Argon2i detailnější článek: Protecting passwords with Argon2 in PHP 7.2.

Poznámka: Argon2i je k dispozici pouze pokud bylo PHP zkompilováno s přepínačem --with-password-argon2. (V binárkách pro Windows na php.net už zkompilovaný).

V PHP 7.2 do jádra PHP přibyla kryptografická knihovna libsodium. Dříve byla dostupná prostřednictvím PECL. Je k dispozici i polyfill, takže s použitím nemusíte čekat až na vydání nové verze PHP (polyfill podporuje dokonce i PHP 5.2.4!).

Přiznám se, že toho o kryptografii moc nevím, ale ta nejdůležitější věc, kterou vím, je: Nevymýšlejte si to po svém! (V této odpovědi na StackExchange je to pěkně vyargumentované.).

Takže prostě používejte funkce sodium_ místo toho, abyste si kryptografické algoritmy implementovali sami (nebo je kopírovali ze StackOverflow).

Doporučuji k přečtení tyto dva články o změnách v kryptografii v PHP (oba napsal Scott Arciszewski, autor Libsodium RFC):

  1. PHP 7.2: The First Programming Language to Add Modern Cryptography to its Standard Library

    When version 7.2 releases at the end of the year, PHP will be the first programming language to adopt modern cryptography in its standard library.

  2. It Turns Out, 2017 is the Year of Simply Secure PHP Cryptography

Další zajímavé změny

Nově bude možné použít object jako typ parametru a jako návratový typ.

<?php declare(strict_types = 1);

function foo(object $definitelyAnObject): object
{
    return 'another string';
}

foo('what about some string?');
// TypeError: Argument 1 passed to foo() must be an object, string given, called in...


foo(new \stdClass());
// TypeError: Return value of foo() must be an object, string returned

Od 7.2 bude object klíčové slovo, takže si zkontrolujte, že ho nepoužíváte jako název třídy, rozhraní nebo traitu. Ostatně bylo rezervované už od PHP 7.0.

Je to užitečné hlavně pro knihovny (ORMka, DI kontejnery, serializery a další). Ale je možné, že to vám to umožní vyčistit i nějaký vlastní kód. U nás nám to pomůže vylepšit pomocnou metodu v testech:

class TestUtils
{

    /**
     * @param object $object
     * @param string $propertyName
     * @param mixed $value
     */
    public static function setPrivateProperty($object, string $property, $value): void
    {
        if (!is_object($object)) {
            throw new \Exception('An object must be passed');
        }

Zjednoduší se na tohle:

class TestUtils
{
    public static function setPrivateProperty(object $object, string $property, $value): void
    {

Díky této změně je možné vynechat definici typu parametru v metodě zděděné třídy. Vím, že to zní složitě, takže se raději podívejme na příklad:

<?php declare(strict_types = 1);

class LibraryClass
{
    public function doWork(string $input)
    {

    }
}

class UserClass extends LibraryClass
{
    public function doWork($input)
    {

    }
}

// PHP <7.2.0
// Warning: Declaration of UserClass::doWork($input) should be compatible with LibraryClass::doWork(string $input) in …

// PHP 7.2.0+
// no errors

Kromě jiného by to mělo umožnit knihovnám přidat skalární type-hinty bez toho, aby jednotliví uživatelé museli upravit své zděděné třídy. Ale je to spíš teoretická možnost, protože není možné udělat to stejné pro definici návratových typů. A pokud už knihovna přidá typy k parametrům, tak dává smysl rovnou přidat i návratové typy.

Vynechání definice typů je v souladu s Liskovové substitučním principem – LSP protože to zvětšuje rozsah přijímaných hodnot. Ale zděděná třída nemůže vynechat návratový typ, protože to by rozšířilo možné návratové hodnoty (a to už LSP porušuje).

Stejné chování bylo upraveno pro abstraktní třídy v samostatném RFC: RFC: Allow abstract function override.

Věděli jste, že je možné volat count i na skalárních hodnotách? Ono to stejně ve skutečnosti moc nepočítá, jen to vrátí 1.

<?php

var_dump(count(null)); // int(0)
var_dump(count(0)); // int(1)
var_dump(count(4)); // int(1)
var_dump(count('4')); // int(1)

Od PHP 7.2 to naštěstí začne vyhazovat warning:

Warning: count(): Parameter must be an array or an object that implements Countable in /in/4aIl2 on line 3

Z podobných úprav mám radost, protože takový kód nikdo nepíše naschvál. Mnohem pravděpodobněji jde o chybu, kterou je potřeba opravit.

Hádám, že na ten warning narazíte i někde ve své aplikaci. Podívejme se na příklad, kde chyba není vidět na první pohled:

<?php declare(strict_types = 1);

class Data
{
    /** @var string[] */
    private $data;

    public function addOne(string $item)
    {
        if (count($this->data) >= 5) {
            throw new \Exception('too much data');
        }

        $this->data[] = $item;
    }
}

$data = new Data();
$data->addOne('item1');
$data->addOne('item2');
$data->addOne('item3');

// Warning: count(): Parameter must be an array or an object that implements Countable in ...

Zahlásí to warning, protože v if voláte count na hodnotě null.

Následující funkce budou od PHP 7.2 vyhazovat deprecation warning. Odebrány budou pravděpodobně v PHP 8.0. V RFC naleznete detailní vysvětlení důvodů pro jednotlivé deprecations:

  • __autoload() – používejte spl_autoload_register()
  • $php_errormsg – používejte error_get_last()
  • create_function() – používejte raději anonymní funkce
  • mbstring.func_overload (v ini souboru) – používejte přímo funkce mb_*
  • (unset) cast – není to deprecation unset($var) ale $foo = (unset) $bar což to stejné jako volat $foo = null (ano, je to divné)
  • parse_str() bez druhého parametru – přímo vytvářet proměnné, když parsujete query string není něco, co byste měli dělat (nebo snad stále používáte register_globals?)
  • gmp_random() – používejte gmp_random_bits() nebo gmp_random_range()
  • each() – používejte raději foreach (je více než 10× rychlejší)
  • assert() se stringovým parametrem – interně totiž používá eval()!
  • $errcontext jako parametr error handleru – používejte debug_backtrace().

Drobnější změny

Věděli jste, že get_class() bez parametrů vrací stejnou hodnotu jako __CLASS__? Já ne. Ale to se v tomhle RFC nemění.

<?php

class MyClass
{
    public function myInfo()
    {
        var_dump(get_class()); // string(7) "MyClass"
        var_dump(__CLASS__); // string(7) "MyClass"
    }
}

$a = new MyClass();
$a->myInfo();

var_dump(get_class($a)); // string(7) "MyClass"

RFC ruší možnost použít null jako parametr. Mrkněte se na následující příklad z RFC. Pokud z repository načteme objekt a tedy $result není null, vypíše to název třídy načteného objektu. Ale pokud bude vrácená hodnota null, vypíše to Foo (což je název třídy, odkud jsme to zavolali, ne název třídy objektu načteného z repository).

class Foo
{

    function bar($repository)
    {
        $result = $repository->find(100);

        echo get_class($result);
    }
}

// In 7.2: Warning: get_class() expects parameter 1 to be object, null given in ...

Pokud jste konfigurovali extensions před 7.2, museli jste v php.ini použít celý název souboru s extension:

Na Windows:

extension=php_mbstring.dll

Na Linuxu:

extension=mbstring.so

To je v pohodě, pokud používáte pouze jeden systém. Ale je to složitější, pokud chcete vytvářet automatizované skripty podporující více platforem. V 7.2 to je vylepšené a teď můžete na Linuxu i Windows použít:

extension=mbstring

Původní syntaxe je samozřejmě stále podporovaná, ale je doporučené používat tu novou.

Tohle RFC je další krůček k tomu, aby bylo těžší PHP používat omylem špatně. Pokud se překlepnete v konstantě v 7.2, tak to vyhodí warning (v PHP 7.1 je to jen notice). A v PHP 8.0 to dokonce vyhodí error.

Nejjednodušší příklad je tohle:

<?php
var_dump(NONEXISTENT_CONSTANT);


// PHP 7.1
// Notice: Use of undefined constant NONEXISTENT_CONSTANT - assumed 'NONEXISTENT_CONSTANT' in …

// PHP 7.2
// Warning: Use of undefined constant NONEXISTENT_CONSTANT - assumed 'NONEXISTENT_CONSTANT' (this will throw an Error in a future version of PHP) in …

Hodně se mi líbí příklady chyb, kterým se RFC snaží předejít:

$foo = flase; // typo!
// ...
if ( $foo ) {
   var_dump($foo); // string(5) "flase"
}

A tento:

$found = false;
foreach ( $list as $item ) {
   if ( is_null($item) ) {
       contniue; // this statement issues a notice and does nothing
   }
   // lines assuming $item is not null
}

Vždycky bylo možné psát čárku i za posledním prvkem pole:

$foo = [
    'foo',
    'bar',
];

Je to užitečné pro hezky čitelné diffy ve VCS a také se snadněji přidávají nové prvky na konec pole.

Tohle RFC navrhovalo přidat volitelnou čárku na konec všech výčtů:

  • Skupinové namespace use
  • Parametry funkcí/metod (definice i volání)
  • Implementace rozhraní ve třídě
  • Implementace traitu
  • Předávání proměnných do scope anonymní funkce

Nejvíc jsem doufal, že projdou parametry metod a funkcí (jak definice, tak volání). Ale hlasováním to neprošlo (potřebovalo 2/3 většinu, protože to je změna jazyka).

Překvapivě prošly pouze „Skupinové namespace use“:

use Foo\Bar\{
    Foo,
    Bar,
    Baz,
};

// všimněte si čárky po "Baz"

Závěrem

PHP 7.2 bude obsahovat nové bezpečnostní možnosti (sodium, Argon2i), několik vylepšení jazyka (typ object, rozšiřování typů ve zděděných třídách) a spoustu drobnějších změn čistících nepořádek v jazyce.

RC2 byla vydána 14. září, takže teď je dobrý čas ho vyzkoušet. Pokud ho zatím nemůžete nebo nechcete používat lokálně pro vývoj, měli byste na něm alespoň spustit testy, abyste zjistili, jestli bude něco potřeba opravovat. Ale myslím, že v dobře udržované aplikaci na moc problémů nenarazíte.

Pokud spravujete nějaký open-source projekt, tak do TravisCI 7.2 přidejte určitě už teď, aby vaši uživatelé nenarazili na problémy s kompatibilitou. Nebo můžete v další verzi vyžadovat 7.2 jako minimální verzi stejně jako Doctrine ORM.

Na co se v PHP 7.2 těšíte nejvíce? A jestli ještě používáte něco staršího než PHP 7.1, tak budu rád, pokud se v komentářích podělíte o důvody.

Nahoru
Tento web používá k poskytování služeb a analýze návštěvnosti soubory cookie. Používáním tohoto webu s tímto souhlasíte. Další informace