Po prostu PHP 7

Maj 17th, 2016

PHP 7 zostało wydane w grudniu 2015 i już trochę zdążyło okrzepnąć. Zdążyliśmy zainstalować na naszych serwera już parę patchy i poprawek.
Przyszła pora na zebranie i spisanie z czego możemy się cieszyć w nowej wersji php. Poniżej postaram się zawrzeć rzeczy, które z mojego punktu widzenia mogą się okazać najbardziej użyteczne podczas kodzenia w php7.

Wydajność

Różnorakich porównań, analiz, statystyk, ikonografik w sieci znajdziemy bez liku. Ja ograniczę się tylko do stwierdzenia że php 7 dostaje potężnego kopa wydajności w porównaniu ze swoimi wcześniejszymi wydaniami. Zbliża się również, lub nawet przewyższa wydajnościowe w porównaniu do HHVM. Tutaj jednak testy wypadają różnie, raz php jest lepsze, raz gorsze (ale niewiele). Wszystko zależy od tego jak testy zostały przeprowadzone i kto je robił ;)
W każdym razie różnica pomiędzy PHP7 a PHP 5.6 jest ogromna i nie podlega dyskusji. Natomiast sam fakt „zrównania” się z HHVM stawia pod znakiem zapytania sens przesiadania się na HHVM.

Warto również wspomnieć, że wraz ze wzrostem wydajności idzie również mniejsze zapotrzebowanie na pamięć. „Mała” rzecz a cieszy.
Poczyniono też kroki które umożliwią dalsze optymalizacje w kolejnych wydaniach języka. Jakkolwiek nie brzmiałoby to tajemniczo to trzeba trzymać kciuki aby dalsze optymalizacje przyniosły przynajmniej podobny efekt jak ta w php7.

Podsumowując.
Jeśli dokonamy migracji naszej strony ze starszych wersji php do php7 dostaniemy „za darmo” porządnego kopa do wydajności.

Za darmo użyłem w cudzysłowach ponieważ dochodzą jeszcze problemy z dostosowaniem kodu do php7. Czasami może być tak, że nie trzeba będzie nic zmieniać w kodzie, a czasami jeśli mamy stary kod to może lepiej napisać serwis od początku?

Nowe funkcjonalności

UWAGA: Część przykładów pochodzi z oficjalnej dokumentacji php.

Przełomowe – czyli prawdziwa nowość, której brakowało od dawna

Scalar Type Declarations

Nareszcie, to jest coś czego w php brakowało mi od dawana.

Zanim jednak przejdziemy do samego meritum, koniecznie trzeba wspomnieć o dwóch trybach w których może pracować php 7.
Chodzi o tryby:

  • coercive – domyślny, dopuszczający rzutowanie
  • strict – ścisły, który nie dopuszcza rzutowania

Sprawdzanie trybu w którym pracuje php jest sprawdzane dla każdego pliku z osobna. Takie sprawdzanie dotyczy również typowania wartości zwracanej, ale o tym będzie w innym punkcie.

Aby php dziłał w trybie strict należy na początku pliku dodać linię:

    declare(strict_types=1);

Linia ta musi wystąpić nawet przed deklaracją przestrzeni nazw (namespace).
Jeśli nie dodamy takiej linii to automatycznie php będzie pracowało w trybie coercive.
Nic nie stoi na przeszkodzie aby część naszych bibliotek/klas/plików używała trybu strict a inna część trybu coercive.
Tryby zostały wprowadzone po to aby można było używać starszych bibliotek php (pod warunkiem, że spoełniają pozostałe warunki działąnai w php 7) w swoich nowych serwisach pisanych w php 7.
Odcięcie się grubą linią od tego co jest już dostępne (przykładowo na github’ie) byłoby strzałem w kolano a portowanie tego wszystkiego do php7 zajełoby wieki. Osobiście uważam to za dobrą (choć może odrobinę uciążliwą) drogę, z czasem pewnie zmieni się domyślny tryb, ale to pewnie przy php 9 lub wyżej ;)

Jednak wracamy do tematu.
Do typów skalarnych zaliczamy:
- ciągi znakowe (string)
- liczby całkowite (int)
- liczby zmiennoprzecinkowe (float)
- wartości boolowskie (bool)

Uzupełniają one typy wprowadzone w php 5.x, które można było przekazywać do funcji:
- nazwy klas
- interfejsy
- tablice
- callable

Przykład:

// coercive
function test(int $int1, int $int2, int $int3)
{
    ...
}
test(12, '41', 2.5);

W powyższym przykładzie wszystkie parametry przekazane do funkcji zostaną zrzutowane do wartości całkowitych i takie już będą wewnątrz funkcji.
Gdybyśmy pracowali w trybie „strict” to zamiast automatycznego rzutowania zgłoszony zostałby wyjątek TypeError

UWAGA
Jeśli używamy trybu „strict” to automatyczna konwersja zachodzi tylko w jednej sytuacji, mianowicie wtedy gdy przekazujemy wartość całkowitą (integer) a w definicji funkcji określiliśmy, że będziemy używać wartości zmiennoprzecinkowej (float). Tutaj język idzie nam na rękę, ponieważ nie tracimy nic jeśli chodzi o precyzję obliczeń. Rzutowanie liczby całkowitej na zmiennoprzecinkową nie powoduje utraty jakichkolwiek danych.

    declare(strict_types=1);

    function multiply1(float $x, float $y)
    {
        return $x * $y;
    }

    function multiply2(int $x, int $y)
    {
        return $x * $y;
    }

    var_dump(multiply1(2, 3.5)); // float(7) - tutaj pierwszy parametr przekazaliśmy jako liczbę całkowitą
    var_dump(multiply2('2', 3)); // Fatal error: Uncaught TypeError: Argument 1 passed to multiply2() must be of the type integer, string given...

Gdy dopasowanie typu nie powiedzie się, zostanie rzucony wyjątek TypeError, co także jest nowością wprowadzoną w PHP7.

Miejsce deklaracji funkcji/metody nie ma wpływu na tryb w którym będzie działać funkcja/metoda podczas wywołania. Można powiedzieć, że funkcje deklarujemy jednocześnie w obu trybach. Tryb php zależy od kontekstu wywołania funkcji.
Jeśli w pliku, którym funkcja jest wywoływana mamy określony tryb „strict” to będzie działać w trybie „strict”

Return Type Declarations

Naturalnym rozwinięciem tematu jest deklarowanie typów zwracanych przez funkcję/metodę.
Podobnie jak w przypadku deklarowania wartości przekazywanych do funkcji, tutaj również znaczenie ma tryb w którym pracuje php.

Zwracanie typu możemy deklarować dla:
- funkcji
- metod
- domknięć

Obsługiwane typy:
- bool
- string
- int
- float
- array
- callable
- self (tylko dla metod)
- parent (tylko dla metod)
- Closure
- nazwa klasy
- nazwa interfejsu


// przykład deklaracji zwracania typu prostego
function fun1(int $a, int $b): int
{
    ...
}

Nie możemy przedefiniować zwracanego typu podczas dziedziczenia jak i implementacji metody pochodzącej z interfejsu.

class Parent {}
class Child extends Parent {}

class Exp1
{
    public function fun() : Parent
    {
        return new Parent;
    }
}

class Exp2 extends Exp1
{
    // nadpisujemy metodę "fun"
    public function fun() : Child
    {
        return new Child;
    }
}

Nadpisanie metody „fun” w klasie „Exp2″ spowoduje zgłoszenie E_COMPILE_ERROR. Aby nasz kod działał metoda Exp2::fun() powinna zwracać obkiekt klasy Parent.

W przypadku gdy metoda będzie zwracać inny typ niż zadeklarowany zostanie rzucony wyjątek TypeError.

Closure call() Method

Jest to uproszczony sposób na wywołanie funkcji domknięcia w kontekście danego obiektu. We wcześniejszych wersjach php również była taka możliwość, tutaj dostajemy jej skróconą składnie przypominająca nieco to znaną z javascript.


class Test {private $x = 1;}

// PHP 5.x
$getProp = function() {return $this->prop;};
$getNewProp = $getProp->bindTo(new Test, 'Test');
echo $getNewProp(); // 1

// PHP 7
$getProp = function() {return $this->prop;};
echo $getProp->call(new Test); // 1

Throwable Interface

Aby móc przejść do wyjątków najpierw musimy wspomnieć o nowym interfejsie (throwable) wprowadzonym w php 7.

Hierarchia wyjątków zostało mocno przebudowania w porównaniu do hierarchii w php 5.x. Wprowadzono nowy interfejs „Throwable” który jest implementowany przez wszystkie klasy obsługi błędów (tak, tak … błędów) i wyjątków.

Nowa hierarchia prezentuje się następująco:

interface Throwable
    |- Exception implements Throwable
        |- ... // tutaj znajduje się hierarchia z php 5.x
    |- Error implements Throwable
        |- TypeError extends Error
        |- ParseError extends Error
        |- AssertionError extends Error
        |- ArithmeticError extends Error
            |- DivisionByZeroError extends ArithmeticError

Klasy bazowe Exception i Error implementują interfejs Throwable, który przedstawia się następująco:

interface Throwable
{
    final public string getMessage ( void )
    final public mixed getCode ( void )
    final public string getFile ( void )
    final public int getLine ( void )
    final public array getTrace ( void )
    final public string getTraceAsString ( void )
    public string __toString ( void )
}

UWAGA
Interfejs Throwable nie może być implementowany przez klasy zdefiniowane przez użytkownika. Musimy dziedziczyć po istniejących klasach Exception lub Error lub ich klasach potomnych.

Engine Expectations

Rozwinięciem poprzedniego tematu są błędy silnika wprowadzone w nowej wersji php. W php 7 błędy krytyczne są zgłaszane jako wyjątki silnika i mogą być przechwytywane za pomocą konstuykcji try … catch. Jak już przechwycimy dany wyjątek to możemy go odpowiednio obsłużyć.

function fun(int $int)
{
    return $int/0;
}

try {
    fun(3);
} catch (DivisionByZeroError $terror) {
    // obsługa błędu
}

Dzięki wprowadzeniu nowej hierarchii wyjątków/błędów w php 7 dostajemy możliwość uzyskania jeszcze większej świadomości co się dzieje w naszej aplikacji. Myślę że to jest krok w dobrą stronę.

Te do których muszę się przekonać

Anonymous Classes – Klasy anonimowe w PHP 7

Z klasami anonimowymi zetknąłem się w Javie, teraz pojawiają się również w PHP.
Dzięki klasom anonimowym możemy w jednej chwili zdefiniować klasę i zwrócić jej instancję na przykład do funkcji/metody. Ich stosowanie ma sens gdy instancja takiej klasy używana jest tylko w jednym miejscu w kodzie.

// PHP 5.x
class MyLogger
{
    public function log($msg)
    {
        echo $msg;
    }
}

$system->setLogHandler(new MyLogger());

// PHP 7
$system->setLogHandler(new class {
    public function log($msg)
    {
        echo $msg;
    }
});

Klasa anonimowa może dziedziczyć po innych klasach, implementować interfejsy, używać Traits oraz można przekazywać zmienne do jej konstruktora zupełnie tak jak w normalnej klasie.


class TestClass {}
interface TestInterface {}
trait TestTrait {}

$testParam = "test param";
$instance = new class($testParam) extends TestClass implements TestInterface {
    private $param;

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

    use TestTrait;
});

Zagnieżdżanie anonimowej klasy w innej klasie jest jak najbardziej możliwe, jednak nie daje to nam dostępu z poziomu klasy anonimowej do prywatnych i chronionych właściwości i metod klasy zewnętrznej (obejmującej). Jeśli chcielibyśmy mieć dostęp do chronionych (protected) właściwości lub metod klasy zewnętrznej to klasa anonimowa musi dziedziczyć po klasie zewnętrznej. Natomiast jeśli chcemy mieć dostęp do prywatnych właściwości (można stosować również z właściwościami chronionymi) należy je przekazać do klasy anonimowej poprzez konstruktor.

Group use Declarations

W sumie można powiedzieć, że to drobnostka, ale jednak przydatna drobnostka.
W php 7 możemy grupować wiele przestrzeni nazw, które posiadają wspólną przestrzeń rodzica. Dzięki takiemu rozwiązaniu pozbywamy się dużo nadmiarowego kodu.
Oczywiście rozwiązanie dotyczy zarówno klas, funkcji jak i stałych.


// PHP 5.3 +
use package\test\ClassA;
use package\test\ClassB;
use package\test\ClassC as C;

use function package\test\fnA;
use function package\test\fnB;
use function package\test\fnC;

use const package\test\ConstA;
use const package\test\ConstB;
use const package\test\ConstC;

// PHP 7+
use package\test\{ClassA, ClassB, ClassC as C};
use function package\test\{fnA, fnB, fnC};
use const package\test\{ConstA, ConstB, ConstC};

Z pewnością na początku rozwiązanie może wydawać się mniej czytelne, jednak wydaje mi się że to kwestia przyzwyczajenia i z czasem przywykniemy do nowego sposobu importowania przestrzeni nazw.

Generator Return Expressions

Generatory pojawiły się w php 5.5, w php 7 zostają rozbudowane o dodatkowe opcje. Jedno z nich jest możliwość konstrukcji „return”, czyli generator jest w stanie zwracać jakąś wartość/wyrażenie. Z generatora nadal jednak nie można zwrócić referencji. Zwracana wartość może być pobrana z generatora za pomocą metody „getReturn()” wykonanej na generatorze.


$gen = (function() {
    yield 1;
    yield 2;

    return 3;
})();

foreach ($gen as $val) {
    echo $val, PHP_EOL;
}

echo $gen->getReturn(), PHP_EOL;

// output:
// 1
// 2
// 3

Jest to dość dziwny sposób na pobranie wartości zwracanej, jednak twórcy php’a doszli do wniosku, że to będzie najrozsądniejsze wyjście z sytuacji.
Jeśli generator zwracałby wartość tak jak funkcja to problemem byłoby rozpoznanie przez programistę piszącego kod czy wartość którą zwraca generator pochodzi z konstrukcji „yield” czy „return”. Dzięki temu mamy rozgraniczenie tych dwóch konstrukcji i sami możemy decydować czy chcemy pobierać wartość zwracaną.

Generator Delegation

Konstrukcja yield from

Rozwinięciem tematu generatorów jest możliwość delegowania przez generator iteracji do innego generatora, tablicy lub obiektu implementującego interfejs Traversable.


function gen()
{
    yield 1;
    yield 2;

    return yield from gen2();
}

function gen2()
{
    yield 3;

    return 4;
}

$gen = gen();

foreach ($gen as $val)
{
    echo $val, PHP_EOL;
}

echo $gen->getReturn();

// output
// 1
// 2
// 3
// 4

Reflection Additions

Kolejną nowością dotyczącą generatorów jest wprowadzenie kilku nowych klas reflekcji służących do ich badania.
PHP 7 daje nam do dyspozycji klasy:
* ReflectionGenerator
* ReflectionType
* ReflectionParameter

Mała rzecz a cieszy – czyli umilacze w php 7

Tutaj znajdą się nowe funkcjonalności, które sprawiają, że kod pisze się łatwiej, ładniej i przyjemniej.

Null Coalesce Operator: ??

W sumie jest to krótsza wersja użycia funkcji isset, pozwala nam na szybkie sprawdzenie czy wartość istnieje i nie jest równa null, w przeciwnym przypadku zwracana jest wartość domyślna.

//  PHP 5.3+
$fruit= isset($_GET['fruit']) ? $_GET['fruit'] : 'banana';

// PHP 7
$fruit = $_GET['fruit'] ?? 'banana';

Jak dla mnie świetna sprawa, kod wygląda o wiele spójnej i czytelniej.

Combined Comparison Operator (spaceship operator) < =>

Miłośnicy fantastyki (Star Wars) dostali nowy operator :)

// PHP 7
$a < => $b

Powyższe wyrażenie zwraca wartości:
* -1 – jeśli wartość po prawej stronie jest większa od wartości po lewej
* 0 – jeśli obie strony są sobie równe
* 1 – jeśli wartość po lewej stronie jest większa od wartości po prawej

Zastosowanie, które od razu przychodzi nam na myśl to sortowanie.

Nie można tego operatora łączyć w wywołania łańcuchowe $a < => $b < => $c.
Nie można też używać go do porównywania obiektów

Filtered unserialize()

Otrzymujemy dodatkowe zabezpieczenie podczas unserializacji danych pochodzących z niepewnego źródła, czyli dak naprawdę wszystkich danych przychodzących z zewnętrznych źródeł. Możemy zdefiniować listę zaufanych klas, które możemy unserializować, pozostałe klasy będą skonwertowane do obiektów klasy __PHP_Incomplete_Class.

// PHP 7
// domyślne działanie, akceptujemy wszystkie klasy. Możemy pominąć dgugi parametr funkcji
$data = unserialize($serializeData, ["allowed_classes" => true]);

// wszystkie obiekty z wyjątkiem obiektów typu ClassOne i ClassTwo zostaną skonwertowane do obiektów typu __PHP_Incomplete_Class
$data = unserialize($serializeData, ["allowed_classes" => ["ClassOne", "ClassTwo"]);

// wszystkie obiekty zostaną skonwertowane do obiektów typ: __PHP_Incomplete_Class
$data = unserialize($serializeData, ["allowed_classes" => false]);

Integer Division with intdiv()

W nowej wersji php dostajemy do dyspozycji funkcję „intdiv” zwraca ona w wyniku liczbę całkowitą pochodzącą z dzielenia.

// PHP 7
intdiv(5, 2); // 2

session_start() Options

Dostajemy możliwość przekazania do funkcji „session_start()” tablicy z parametrami. Możemy dzięki temu konfigurować parametry ustawianie w php.ini dotyczące sesji, przykładowo „cach_limiter”

// PHP 7
session_start(['cache_limiter' => 'private']);

Dodatkowo dostajemy nowy parametr: „session.lazy_write”, utawiony domyślnie na włączony „true”.
Dzięki temu dane w sesji zostaną tylko wtedy nadpisane, kiedy zostaną zmienione, jest to kolejna optymalizacja wprowadzona w php 7.

Support for Array Constants in define()

Użycie tablic w konstrukcji „const” zostało wprowadzone w php 5.6.
Teraz możemy również tablic używać w funkcji”define”

// PHP 5.6
class Test {
    const FOO = ['one', 'two'];
}

// PHP 7
define('BAR, ['one', 'two']);

Podsumowanie

Czy warto się przesiadać?
Moim zdaniem tak.

Komentarze są zamknięte